diff --git a/submodules/AsyncDisplayKit/.buckconfig b/submodules/AsyncDisplayKit/.buckconfig new file mode 100644 index 0000000000..821cb2c82f --- /dev/null +++ b/submodules/AsyncDisplayKit/.buckconfig @@ -0,0 +1,22 @@ +[cxx] + default_platform = iphonesimulator-x86_64 + combined_preprocess_and_compile = true + +[apple] + iphonesimulator_target_sdk_version = 8.0 + iphoneos_target_sdk_version = 8.0 + xctool_default_destination_specifier = platform=iOS Simulator, name=iPhone 6, OS=10.2 + +[alias] + lib = //:AsyncDisplayKit + tests = //:Tests + +[httpserver] + port = 8080 + +[project] + ide = xcode + ignore = .buckd, \ + .hg, \ + .git, \ + buck-out, \ diff --git a/submodules/AsyncDisplayKit/.buckversion b/submodules/AsyncDisplayKit/.buckversion new file mode 100644 index 0000000000..437aedac09 --- /dev/null +++ b/submodules/AsyncDisplayKit/.buckversion @@ -0,0 +1 @@ +f399f484bf13b47bcc2bf0f2e092ab5d8de9f6e6 diff --git a/submodules/AsyncDisplayKit/.editorconfig b/submodules/AsyncDisplayKit/.editorconfig new file mode 100644 index 0000000000..0605feef2a --- /dev/null +++ b/submodules/AsyncDisplayKit/.editorconfig @@ -0,0 +1,19 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[**.{h,cc,mm,m}] +indent_style = space +indent_size = 2 + +[*.{md,markdown}] +trim_trailing_whitespace = false + +# Makefiles always use tabs for indentation +[Makefile] +indent_style = tab \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/.github/ISSUE_TEMPLATE.md b/submodules/AsyncDisplayKit/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..c5ab71942b --- /dev/null +++ b/submodules/AsyncDisplayKit/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ +// If you're looking for help, please consider joining our slack channel: +// http://asyncdisplaykit.org/slack (we'll be updating the name to Texture soon) + +// The more information you include, the faster we can help you out! +// Please include: a sample project or screenshots, code snippets +// Texture version, and/or backtraces for any crashes (> bt all). +// Please delete these lines before posting. Thanks! diff --git a/submodules/AsyncDisplayKit/.github_changelog_generator b/submodules/AsyncDisplayKit/.github_changelog_generator new file mode 100644 index 0000000000..5b1ccea517 --- /dev/null +++ b/submodules/AsyncDisplayKit/.github_changelog_generator @@ -0,0 +1,3 @@ +issues=false +since-tag=2.8 +future-release=2.9 diff --git a/submodules/AsyncDisplayKit/.gitignore b/submodules/AsyncDisplayKit/.gitignore new file mode 100644 index 0000000000..eed07e6482 --- /dev/null +++ b/submodules/AsyncDisplayKit/.gitignore @@ -0,0 +1,25 @@ +fastlane/README.md +fastlane/report.xml +fastlane/test_output/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.xcscmblueprint +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +.DS_Store +*.dSYM +*.dSYM.zip +*.ipa +*/xcuserdata/* +AsyncDisplayKit.xcodeproj/* diff --git a/submodules/AsyncDisplayKit/.slather.yml b/submodules/AsyncDisplayKit/.slather.yml new file mode 100644 index 0000000000..ef84e32dea --- /dev/null +++ b/submodules/AsyncDisplayKit/.slather.yml @@ -0,0 +1,5 @@ +ci_service: travis_ci +coverage_service: coveralls +xcodeproj: AsyncDisplayKit.xcodeproj +source_directory: AsyncDisplayKit + diff --git a/submodules/AsyncDisplayKit/.travis.yml b/submodules/AsyncDisplayKit/.travis.yml new file mode 100644 index 0000000000..b493cff43c --- /dev/null +++ b/submodules/AsyncDisplayKit/.travis.yml @@ -0,0 +1,34 @@ +language: objective-c +cache: + - bundler + - cocoapods +osx_image: xcode8.1 +git: + depth: 10 +before_install: + - brew update + - brew outdated xctool || brew upgrade xctool + - brew outdated carthage || brew upgrade carthage + - gem install cocoapods -v 1.0.1 + - gem install xcpretty -v 0.2.2 + - gem install xcpretty-travis-formatter +# - gem install slather + - xcrun simctl list +install: echo "<3" +env: + - MODE=tests + - MODE=tests_listkit + - MODE=examples-pt1 + - MODE=examples-pt2 + - MODE=examples-pt3 + - MODE=life-without-cocoapods + - MODE=framework +script: ./build.sh $MODE + +#after_success: +# - slather + +# whitelist +branches: + only: + - master diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..574f0ec195 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..08de0be8d3 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/ASPagerFlowLayout.m b/submodules/AsyncDisplayKit/AsyncDisplayKit/ASPagerFlowLayout.m new file mode 100644 index 0000000000..df8ce69868 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/ASPagerFlowLayout.m @@ -0,0 +1,96 @@ +// +// ASPagerFlowLayout.m +// AsyncDisplayKit +// +// Created by Levi McCallum on 2/12/16. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#ifndef MINIMAL_ASDK +#import + +@interface ASPagerFlowLayout () { + BOOL _didRotate; + CGRect _cachedCollectionViewBounds; + NSIndexPath *_currentIndexPath; +} + +@end + +@implementation ASPagerFlowLayout + +- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity +{ + NSInteger currentPage = ceil(proposedContentOffset.x / self.collectionView.bounds.size.width); + _currentIndexPath = [NSIndexPath indexPathForItem:currentPage inSection:0]; + + return [super targetContentOffsetForProposedContentOffset:proposedContentOffset withScrollingVelocity:velocity]; +} + + +- (void)prepareForAnimatedBoundsChange:(CGRect)oldBounds +{ + // Cache the current page if a rotation did happen. This happens before the rotation animation + // is occuring and the bounds changed so we use this as an opportunity to cache the current index path + if (_cachedCollectionViewBounds.size.width != self.collectionView.bounds.size.width) { + _cachedCollectionViewBounds = self.collectionView.bounds; + + // Figurring out current page based on the old bounds visible space + CGRect visibleRect = oldBounds; + + CGFloat visibleXCenter = CGRectGetMidX(visibleRect); + NSArray *layoutAttributes = [self layoutAttributesForElementsInRect:visibleRect]; + for (UICollectionViewLayoutAttributes *attributes in layoutAttributes) { + if ([attributes representedElementCategory] == UICollectionElementCategoryCell && attributes.center.x == visibleXCenter) { + _currentIndexPath = attributes.indexPath; + break; + } + } + + _didRotate = YES; + } + + [super prepareForAnimatedBoundsChange:oldBounds]; +} +- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset +{ + // Don't mess around if the user is interacting with the page node. Although if just a rotation happened we should + // try to use the current index path to not end up setting the target content offset to something in between pages + if (_didRotate || (!self.collectionView.isDecelerating && !self.collectionView.isTracking)) { + _didRotate = NO; + if (_currentIndexPath) { + return [self _targetContentOffsetForItemAtIndexPath:_currentIndexPath proposedContentOffset:proposedContentOffset]; + } + } + + return [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; +} + +- (CGPoint)_targetContentOffsetForItemAtIndexPath:(NSIndexPath *)indexPath proposedContentOffset:(CGPoint)proposedContentOffset +{ + if ([self _dataSourceIsEmpty]) { + return proposedContentOffset; + } + + UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:_currentIndexPath]; + if (attributes == nil) { + return proposedContentOffset; + } + + CGFloat xOffset = (CGRectGetWidth(self.collectionView.bounds) - CGRectGetWidth(attributes.frame)) / 2.0; + return CGPointMake(attributes.frame.origin.x - xOffset, proposedContentOffset.y); +} + +- (BOOL)_dataSourceIsEmpty +{ + return ([self.collectionView numberOfSections] == 0 || + [self.collectionView numberOfItemsInSection:0] == 0); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorCell.h b/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorCell.h new file mode 100644 index 0000000000..8e0602911e --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorCell.h @@ -0,0 +1,31 @@ +// +// ASLayoutElementInspectorCell.h +// AsyncDisplayKit +// +// Created by Hannah Troisi on 3/27/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#ifndef MINIMAL_ASDK + +#import + +typedef NS_ENUM(NSInteger, ASLayoutElementPropertyType) { + ASLayoutElementPropertyFlexGrow = 0, + ASLayoutElementPropertyFlexShrink, + ASLayoutElementPropertyAlignSelf, + ASLayoutElementPropertyFlexBasis, + ASLayoutElementPropertySpacingBefore, + ASLayoutElementPropertySpacingAfter, + ASLayoutElementPropertyAscender, + ASLayoutElementPropertyDescender, + ASLayoutElementPropertyCount +}; + +@interface ASLayoutElementInspectorCell : ASCellNode + +- (instancetype)initWithProperty:(ASLayoutElementPropertyType)property layoutElementToEdit:(id)layoutable NS_DESIGNATED_INITIALIZER; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorCell.m b/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorCell.m new file mode 100644 index 0000000000..7e5ca1ad9c --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorCell.m @@ -0,0 +1,570 @@ +// +// ASLayoutElementInspectorCell.m +// AsyncDisplayKit +// +// Created by Hannah Troisi on 3/27/16. +// Copyright © 2016 Facebook. All rights reserved. +// +#ifndef MINIMAL_ASDK +#import +#import + +typedef NS_ENUM(NSInteger, CellDataType) { + CellDataTypeBool, + CellDataTypeFloat, +}; + +__weak static ASLayoutElementInspectorCell *__currentlyOpenedCell = nil; + +@protocol InspectorCellEditingBubbleProtocol +- (void)valueChangedToIndex:(NSUInteger)index; +@end + +@interface ASLayoutElementInspectorCellEditingBubble : ASDisplayNode +@property (nonatomic, strong, readwrite) id delegate; +- (instancetype)initWithEnumOptions:(BOOL)yes enumStrings:(NSArray *)options currentOptionIndex:(NSUInteger)currentOption; +- (instancetype)initWithSliderMinValue:(CGFloat)min maxValue:(CGFloat)max currentValue:(CGFloat)current +;@end + +@interface ASLayoutElementInspectorCell () +@end + +@implementation ASLayoutElementInspectorCell +{ + ASLayoutElementPropertyType _propertyType; + CellDataType _dataType; + id _layoutElementToEdit; + + ASButtonNode *_buttonNode; + ASTextNode *_textNode; + ASTextNode *_textNode2; + + ASLayoutElementInspectorCellEditingBubble *_textBubble; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithProperty:(ASLayoutElementPropertyType)property layoutElementToEdit:(id)layoutElement +{ + self = [super init]; + if (self) { + + _propertyType = property; + _dataType = [ASLayoutElementInspectorCell dataTypeForProperty:property]; + _layoutElementToEdit = layoutElement; + + self.automaticallyManagesSubnodes = YES; + + _buttonNode = [self makeBtnNodeWithTitle:[ASLayoutElementInspectorCell propertyStringForPropertyType:property]]; + [_buttonNode addTarget:self action:@selector(buttonTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; + + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [ASLayoutElementInspectorCell propertyValueAttributedStringForProperty:property withLayoutElement:layoutElement]; + + [self updateButtonStateForProperty:property withLayoutElement:layoutElement]; + + _textNode2 = [[ASTextNode alloc] init]; + _textNode2.attributedText = [ASLayoutElementInspectorCell propertyValueDetailAttributedStringForProperty:property withLayoutElement:layoutElement]; + + } + return self; +} + +- (void)updateButtonStateForProperty:(ASLayoutElementPropertyType)property withLayoutElement:(id)layoutElement +{ + if (property == ASLayoutElementPropertyFlexGrow) { + _buttonNode.selected = layoutElement.style.flexGrow; + } + else if (property == ASLayoutElementPropertyFlexShrink) { + _buttonNode.selected = layoutElement.style.flexShrink; + } +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *horizontalSpec = [ASStackLayoutSpec horizontalStackLayoutSpec]; + horizontalSpec.children = @[_buttonNode, _textNode]; + horizontalSpec.style.flexGrow = 1.0; + horizontalSpec.alignItems = ASStackLayoutAlignItemsCenter; + horizontalSpec.justifyContent = ASStackLayoutJustifyContentSpaceBetween; + + ASLayoutSpec *childSpec; + if (_textBubble) { + ASStackLayoutSpec *verticalSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalSpec.children = @[horizontalSpec, _textBubble]; + verticalSpec.spacing = 8; + verticalSpec.style.flexGrow = 1.0; + _textBubble.style.flexGrow = 1.0; + childSpec = verticalSpec; + } else { + childSpec = horizontalSpec; + } + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(2, 4, 2, 4) child:childSpec]; + insetSpec.style.flexGrow =1.0; + + return insetSpec; +} + ++ (NSAttributedString *)propertyValueAttributedStringForProperty:(ASLayoutElementPropertyType)property withLayoutElement:(id)layoutElement +{ + NSString *valueString; + + switch (property) { + case ASLayoutElementPropertyFlexGrow: + valueString = layoutElement.style.flexGrow ? @"YES" : @"NO"; + break; + case ASLayoutElementPropertyFlexShrink: + valueString = layoutElement.style.flexShrink ? @"YES" : @"NO"; + break; + case ASLayoutElementPropertyAlignSelf: + valueString = [ASLayoutElementInspectorCell alignSelfEnumValueString:layoutElement.style.alignSelf]; + break; + case ASLayoutElementPropertyFlexBasis: + if (layoutElement.style.flexBasis.unit && layoutElement.style.flexBasis.value) { // ENUM TYPE + valueString = [NSString stringWithFormat:@"%0.0f %@", layoutElement.style.flexBasis.value, + [ASLayoutElementInspectorCell ASRelativeDimensionEnumString:layoutElement.style.alignSelf]]; + } else { + valueString = @"0 pts"; + } + break; + case ASLayoutElementPropertySpacingBefore: + valueString = [NSString stringWithFormat:@"%0.0f", layoutElement.style.spacingBefore]; + break; + case ASLayoutElementPropertySpacingAfter: + valueString = [NSString stringWithFormat:@"%0.0f", layoutElement.style.spacingAfter]; + break; + case ASLayoutElementPropertyAscender: + valueString = [NSString stringWithFormat:@"%0.0f", layoutElement.style.ascender]; + break; + case ASLayoutElementPropertyDescender: + valueString = [NSString stringWithFormat:@"%0.0f", layoutElement.style.descender]; + break; + default: + valueString = @"?"; + break; + } + return [ASLayoutElementInspectorCell attributedStringFromString:valueString]; +} + ++ (NSAttributedString *)propertyValueDetailAttributedStringForProperty:(ASLayoutElementPropertyType)property withLayoutElement:(id)layoutElement +{ + NSString *valueString; + + switch (property) { + case ASLayoutElementPropertyFlexGrow: + case ASLayoutElementPropertyFlexShrink: + case ASLayoutElementPropertyAlignSelf: + case ASLayoutElementPropertyFlexBasis: + case ASLayoutElementPropertySpacingBefore: + case ASLayoutElementPropertySpacingAfter: + case ASLayoutElementPropertyAscender: + case ASLayoutElementPropertyDescender: + default: + return nil; + } + return [ASLayoutElementInspectorCell attributedStringFromString:valueString]; +} + +- (void)endEditingValue +{ + _textBubble = nil; + __currentlyOpenedCell = nil; + _buttonNode.selected = NO; + [self setNeedsLayout]; +} + +- (void)beginEditingValue +{ + _textBubble.delegate = self; + __currentlyOpenedCell = self; + [self setNeedsLayout]; +} + +- (void)valueChangedToIndex:(NSUInteger)index +{ + switch (_propertyType) { + + case ASLayoutElementPropertyAlignSelf: + _layoutElementToEdit.style.alignSelf = (ASStackLayoutAlignSelf)index; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[ASLayoutElementInspectorCell alignSelfEnumValueString:index]]; + break; + + case ASLayoutElementPropertySpacingBefore: + _layoutElementToEdit.style.spacingBefore = (CGFloat)index; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.spacingBefore]]; + break; + + case ASLayoutElementPropertySpacingAfter: + _layoutElementToEdit.style.spacingAfter = (CGFloat)index; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.spacingAfter]]; + break; + + case ASLayoutElementPropertyAscender: + _layoutElementToEdit.style.ascender = (CGFloat)index; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.ascender]]; + break; + + case ASLayoutElementPropertyDescender: + _layoutElementToEdit.style.descender = (CGFloat)index; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.descender]]; + break; + + default: + break; + } + + [self setNeedsLayout]; +} + +#pragma mark - gesture handling + +- (void)buttonTapped:(ASButtonNode *)sender +{ + BOOL selfIsEditing = (self == __currentlyOpenedCell); + [__currentlyOpenedCell endEditingValue]; + if (selfIsEditing) { + sender.selected = NO; + return; + } + +// NSUInteger currentAlignSelfValue; +// NSUInteger nextAlignSelfValue; +// CGFloat newValue; + + sender.selected = !sender.selected; + switch (_propertyType) { + + case ASLayoutElementPropertyFlexGrow: + _layoutElementToEdit.style.flexGrow = sender.isSelected ? 1.0 : 0.0; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:sender.selected ? @"YES" : @"NO"]; + break; + + case ASLayoutElementPropertyFlexShrink: + _layoutElementToEdit.style.flexShrink = sender.isSelected ? 1.0 : 0.0; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:sender.selected ? @"YES" : @"NO"]; + break; + + case ASLayoutElementPropertyAlignSelf: + _textBubble = [[ASLayoutElementInspectorCellEditingBubble alloc] initWithEnumOptions:YES + enumStrings:[ASLayoutElementInspectorCell alignSelfEnumStringArray] + currentOptionIndex:_layoutElementToEdit.style.alignSelf]; + + [self beginEditingValue]; +// if ([self layoutSpec]) { +// currentAlignSelfValue = [[self layoutSpec] alignSelf]; +// nextAlignSelfValue = (currentAlignSelfValue + 1 <= ASStackLayoutAlignSelfStretch) ? currentAlignSelfValue + 1 : 0; +// [[self layoutSpec] setAlignSelf:nextAlignSelfValue]; +// +// } else if ([self node]) { +// currentAlignSelfValue = [[self node] alignSelf]; +// nextAlignSelfValue = (currentAlignSelfValue + 1 <= ASStackLayoutAlignSelfStretch) ? currentAlignSelfValue + 1 : 0; +// [[self node] setAlignSelf:nextAlignSelfValue]; +// } + break; + + case ASLayoutElementPropertySpacingBefore: + _textBubble = [[ASLayoutElementInspectorCellEditingBubble alloc] initWithSliderMinValue:0 maxValue:100 currentValue:_layoutElementToEdit.style.spacingBefore]; + [self beginEditingValue]; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.spacingBefore]]; + break; + + case ASLayoutElementPropertySpacingAfter: + _textBubble = [[ASLayoutElementInspectorCellEditingBubble alloc] initWithSliderMinValue:0 maxValue:100 currentValue:_layoutElementToEdit.style.spacingAfter]; + [self beginEditingValue]; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.spacingAfter]]; + break; + + + case ASLayoutElementPropertyAscender: + _textBubble = [[ASLayoutElementInspectorCellEditingBubble alloc] initWithSliderMinValue:0 maxValue:100 currentValue:_layoutElementToEdit.style.ascender]; + [self beginEditingValue]; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.ascender]]; + break; + + case ASLayoutElementPropertyDescender: + _textBubble = [[ASLayoutElementInspectorCellEditingBubble alloc] initWithSliderMinValue:0 maxValue:100 currentValue:_layoutElementToEdit.style.descender]; + [self beginEditingValue]; + _textNode.attributedText = [ASLayoutElementInspectorCell attributedStringFromString:[NSString stringWithFormat:@"%0.0f", _layoutElementToEdit.style.descender]]; + break; + + default: + break; + } + [self setNeedsLayout]; +} + +#pragma mark - cast layoutElementToEdit + +- (ASDisplayNode *)node +{ + if (_layoutElementToEdit.layoutElementType == ASLayoutElementTypeDisplayNode) { + return (ASDisplayNode *)_layoutElementToEdit; + } + return nil; +} + +- (ASLayoutSpec *)layoutSpec +{ + if (_layoutElementToEdit.layoutElementType == ASLayoutElementTypeLayoutSpec) { + return (ASLayoutSpec *)_layoutElementToEdit; + } + return nil; +} + +#pragma mark - data / property type helper methods + ++ (CellDataType)dataTypeForProperty:(ASLayoutElementPropertyType)property +{ + switch (property) { + + case ASLayoutElementPropertyFlexGrow: + case ASLayoutElementPropertyFlexShrink: + return CellDataTypeBool; + + case ASLayoutElementPropertySpacingBefore: + case ASLayoutElementPropertySpacingAfter: + case ASLayoutElementPropertyAscender: + case ASLayoutElementPropertyDescender: + return CellDataTypeFloat; + + default: + break; + } + return CellDataTypeBool; +} + ++ (NSString *)propertyStringForPropertyType:(ASLayoutElementPropertyType)property +{ + NSString *string; + switch (property) { + case ASLayoutElementPropertyFlexGrow: + string = @"FlexGrow"; + break; + case ASLayoutElementPropertyFlexShrink: + string = @"FlexShrink"; + break; + case ASLayoutElementPropertyAlignSelf: + string = @"AlignSelf"; + break; + case ASLayoutElementPropertyFlexBasis: + string = @"FlexBasis"; + break; + case ASLayoutElementPropertySpacingBefore: + string = @"SpacingBefore"; + break; + case ASLayoutElementPropertySpacingAfter: + string = @"SpacingAfter"; + break; + case ASLayoutElementPropertyAscender: + string = @"Ascender"; + break; + case ASLayoutElementPropertyDescender: + string = @"Descender"; + break; + default: + string = @"Unknown"; + break; + } + return string; +} + ++ (NSDictionary *)alignSelfTypeNames +{ + return @{@(ASStackLayoutAlignSelfAuto) : @"Auto", + @(ASStackLayoutAlignSelfStart) : @"Start", + @(ASStackLayoutAlignSelfEnd) : @"End", + @(ASStackLayoutAlignSelfCenter) : @"Center", + @(ASStackLayoutAlignSelfStretch) : @"Stretch"}; +} + ++ (NSString *)alignSelfEnumValueString:(NSUInteger)type +{ + return [[self class] alignSelfTypeNames][@(type)]; +} + ++ (NSArray *)alignSelfEnumStringArray +{ + return @[@"ASStackLayoutAlignSelfAuto", + @"ASStackLayoutAlignSelfStart", + @"ASStackLayoutAlignSelfEnd", + @"ASStackLayoutAlignSelfCenter", + @"ASStackLayoutAlignSelfStretch"]; +} + ++ (NSDictionary *)ASRelativeDimensionTypeNames +{ + return @{@(ASDimensionUnitPoints) : @"pts", + @(ASDimensionUnitFraction) : @"%"}; +} + ++ (NSString *)ASRelativeDimensionEnumString:(NSUInteger)type +{ + return [[self class] ASRelativeDimensionTypeNames][@(type)]; +} + +#pragma mark - formatting helper methods + ++ (NSAttributedString *)attributedStringFromString:(NSString *)string +{ + return [ASLayoutElementInspectorCell attributedStringFromString:string withTextColor:[UIColor whiteColor]]; +} + ++ (NSAttributedString *)attributedStringFromString:(NSString *)string withTextColor:(nullable UIColor *)color +{ + NSDictionary *attributes = @{NSForegroundColorAttributeName : color, + NSFontAttributeName : [UIFont fontWithName:@"Menlo-Regular" size:12]}; + + return [[NSAttributedString alloc] initWithString:string attributes:attributes]; +} + +- (ASButtonNode *)makeBtnNodeWithTitle:(NSString *)title +{ + UIColor *orangeColor = [UIColor colorWithRed:255/255.0 green:181/255.0 blue:68/255.0 alpha:1]; + UIImage *orangeStretchBtnImg = [ASLayoutElementInspectorCell imageForButtonWithBackgroundColor:orangeColor + borderColor:[UIColor whiteColor] + borderWidth:3]; + UIImage *greyStretchBtnImg = [ASLayoutElementInspectorCell imageForButtonWithBackgroundColor:[UIColor darkGrayColor] + borderColor:[UIColor lightGrayColor] + borderWidth:3]; + UIImage *clearStretchBtnImg = [ASLayoutElementInspectorCell imageForButtonWithBackgroundColor:[UIColor clearColor] + borderColor:[UIColor whiteColor] + borderWidth:3]; + ASButtonNode *btn = [[ASButtonNode alloc] init]; + btn.contentEdgeInsets = UIEdgeInsetsMake(5, 5, 5, 5); + [btn setAttributedTitle:[ASLayoutElementInspectorCell attributedStringFromString:title] forState:ASControlStateNormal]; + [btn setAttributedTitle:[ASLayoutElementInspectorCell attributedStringFromString:title withTextColor:[UIColor lightGrayColor]] forState:ASControlStateDisabled]; + [btn setBackgroundImage:clearStretchBtnImg forState:ASControlStateNormal]; + [btn setBackgroundImage:orangeStretchBtnImg forState:ASControlStateSelected]; + [btn setBackgroundImage:greyStretchBtnImg forState:ASControlStateDisabled]; + + return btn; +} + +#define CORNER_RADIUS 3 ++ (UIImage *)imageForButtonWithBackgroundColor:(UIColor *)backgroundColor borderColor:(UIColor *)borderColor borderWidth:(CGFloat)width +{ + CGSize unstretchedSize = CGSizeMake(2 * CORNER_RADIUS + 1, 2 * CORNER_RADIUS + 1); + CGRect rect = (CGRect) {CGPointZero, unstretchedSize}; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:CORNER_RADIUS]; + + // create a graphics context for the following status button + UIGraphicsBeginImageContextWithOptions(unstretchedSize, NO, 0); + + [path addClip]; + [backgroundColor setFill]; + [path fill]; + + path.lineWidth = width; + [borderColor setStroke]; + [path stroke]; + + UIImage *btnImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return [btnImage stretchableImageWithLeftCapWidth:CORNER_RADIUS topCapHeight:CORNER_RADIUS]; +} + +@end + + + +@implementation ASLayoutElementInspectorCellEditingBubble +{ + NSMutableArray *_textNodes; + ASDisplayNode *_slider; +} + +- (instancetype)initWithEnumOptions:(BOOL)yes enumStrings:(NSArray *)options currentOptionIndex:(NSUInteger)currentOption +{ + self = [super init]; + if (self) { + self.automaticallyManagesSubnodes = YES; + self.backgroundColor = [UIColor colorWithRed:255/255.0 green:181/255.0 blue:68/255.0 alpha:1]; + + _textNodes = [[NSMutableArray alloc] init]; + int index = 0; + for (NSString *optionStr in options) { + ASButtonNode *btn = [[ASButtonNode alloc] init]; + [btn setAttributedTitle:[ASLayoutElementInspectorCell attributedStringFromString:optionStr] forState:ASControlStateNormal]; + [btn setAttributedTitle:[ASLayoutElementInspectorCell attributedStringFromString:optionStr withTextColor:[UIColor redColor]] + forState:ASControlStateSelected]; + [btn addTarget:self action:@selector(enumOptionSelected:) forControlEvents:ASControlNodeEventTouchUpInside]; + btn.selected = (index == currentOption) ? YES : NO; + [_textNodes addObject:btn]; + index++; + } + } + return self; +} + +- (instancetype)initWithSliderMinValue:(CGFloat)min maxValue:(CGFloat)max currentValue:(CGFloat)current +{ + if (self = [super init]) { + self.userInteractionEnabled = YES; + self.automaticallyManagesSubnodes = YES; + self.backgroundColor = [UIColor colorWithRed:255/255.0 green:181/255.0 blue:68/255.0 alpha:1]; + + __weak id weakSelf = self; + _slider = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{ + UISlider *slider = [[UISlider alloc] init]; + slider.minimumValue = min; + slider.maximumValue = max; + slider.value = current; + [slider addTarget:weakSelf action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; + + return slider; + }]; + _slider.userInteractionEnabled = YES; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + _slider.style.preferredSize = CGSizeMake(constrainedSize.max.width, 25); + + NSMutableArray *children = [[NSMutableArray alloc] init]; + if (_textNodes) { + ASStackLayoutSpec *textStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + textStack.children = _textNodes; + textStack.spacing = 2; + [children addObject:textStack]; + } + if (_slider) { + _slider.style.flexGrow = 1.0; + [children addObject:_slider]; + } + + ASStackLayoutSpec *verticalStackSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackSpec.children = children; + verticalStackSpec.spacing = 2; + verticalStackSpec.style.flexGrow = 1.0; + verticalStackSpec.style.alignSelf = ASStackLayoutAlignSelfStretch; + + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(8, 8, 8, 8) child:verticalStackSpec]; + + return insetSpec; +} + +#pragma mark - gesture handling +- (void)enumOptionSelected:(ASButtonNode *)sender +{ + sender.selected = !sender.selected; + for (ASButtonNode *node in _textNodes) { + if (node != sender) { + node.selected = NO; + } + } + [self.delegate valueChangedToIndex:[_textNodes indexOfObject:sender]]; + [self setNeedsLayout]; +} + +- (void)sliderValueChanged:(UISlider *)sender +{ + [self.delegate valueChangedToIndex:roundf(sender.value)]; +} + +@end + + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorNode.m b/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorNode.m new file mode 100644 index 0000000000..b71c2f7413 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Debug/ASLayoutElementInspectorNode.m @@ -0,0 +1,426 @@ +// +// ASLayoutElementInspectorNode.m +// Sample +// +// Created by Hannah Troisi on 3/19/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#ifndef MINIMAL_ASDK +#import +#endif +#import +#import +#import +#import +#import + +@interface ASLayoutElementInspectorNode () +#ifndef MINIMAL_ASDK + +#endif +@end + +@implementation ASLayoutElementInspectorNode +{ +#ifndef MINIMAL_ASDK + ASTableNode *_tableNode; +#endif +} + +#pragma mark - class methods ++ (instancetype)sharedInstance +{ + static ASLayoutElementInspectorNode *__inspector = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __inspector = [[ASLayoutElementInspectorNode alloc] init]; + }); + + return __inspector; +} + +#pragma mark - lifecycle +- (instancetype)init +{ + self = [super init]; + if (self) { + +#ifndef MINIMAL_ASDK + _tableNode = [[ASTableNode alloc] init]; + _tableNode.delegate = self; + _tableNode.dataSource = self; + + [self addSubnode:_tableNode]; // required because of manual layout +#endif + } + return self; +} + +- (void)didLoad +{ + [super didLoad]; +#ifndef MINIMAL_ASDK + _tableNode.view.backgroundColor = [UIColor colorWithRed:40/255.0 green:43/255.0 blue:53/255.0 alpha:1]; + _tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; + _tableNode.view.allowsSelection = NO; + _tableNode.view.sectionHeaderHeight = 40; +#endif +} + +- (void)layout +{ + [super layout]; +#ifndef MINIMAL_ASDK + _tableNode.frame = self.bounds; +#endif +} + +#pragma mark - intstance methods +- (void)setLayoutElementToEdit:(id)layoutElementToEdit +{ + if (_layoutElementToEdit != layoutElementToEdit) { + _layoutElementToEdit = layoutElementToEdit; + } +#ifndef MINIMAL_ASDK + [_tableNode reloadData]; +#endif +} + +#pragma mark - ASTableDataSource + +#ifndef MINIMAL_ASDK +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 0) { + NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor colorWithRed:255/255.0 green:181/255.0 blue:68/255.0 alpha:1], + NSFontAttributeName : [UIFont fontWithName:@"Menlo-Regular" size:12]}; + ASTextCellNode *textCell = [[ASTextCellNode alloc] initWithAttributes:attributes insets:UIEdgeInsetsMake(0, 4, 0, 0)]; + textCell.text = [_layoutElementToEdit description]; + return textCell; + } else { + return [[ASLayoutElementInspectorCell alloc] initWithProperty:(ASLayoutElementPropertyType)indexPath.row layoutElementToEdit:_layoutElementToEdit]; + } +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == 0) { + return 1; + } else { + return ASLayoutElementPropertyCount; + } +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 2; +} + +- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + UILabel *headerTitle = [[UILabel alloc] initWithFrame:CGRectZero]; + + NSString *title; + if (section == 0) { + title = @" Item"; + } else { + title = @" Properties"; + } + + NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor whiteColor], + NSFontAttributeName : [UIFont fontWithName:@"Menlo-Bold" size:12]}; + headerTitle.attributedText = [[NSAttributedString alloc] initWithString:title attributes:attributes]; + + return headerTitle; +} + +#endif + +//- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +//{ +// // navigate layout hierarchy +// +// _parentNodeNavBtn.alignSelf = ASStackLayoutAlignSelfCenter; +// _childNodeNavBtn.alignSelf = ASStackLayoutAlignSelfCenter; +// +// ASStackLayoutSpec *horizontalStackNav = [ASStackLayoutSpec horizontalStackLayoutSpec]; +// horizontalStackNav.style.flexGrow = 1.0; +// horizontalStackNav.alignSelf = ASStackLayoutAlignSelfCenter; +// horizontalStackNav.children = @[_siblingNodeLefttNavBtn, _siblingNodeRightNavBtn]; +// +// ASStackLayoutSpec *horizontalStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; +// horizontalStack.style.flexGrow = 1.0; +// ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; +// +// spacer.style.flexGrow = 1.0; +// horizontalStack.children = @[_flexGrowBtn, spacer]; +// _flexGrowValue.alignSelf = ASStackLayoutAlignSelfEnd; // FIXME: make framework give a warning if you use ASAlignmentBottom!!!!! +// +// ASStackLayoutSpec *horizontalStack2 = [ASStackLayoutSpec horizontalStackLayoutSpec]; +// horizontalStack2.style.flexGrow = 1.0; +// horizontalStack2.children = @[_flexShrinkBtn, spacer]; +// _flexShrinkValue.alignSelf = ASStackLayoutAlignSelfEnd; +// +// ASStackLayoutSpec *horizontalStack3 = [ASStackLayoutSpec horizontalStackLayoutSpec]; +// horizontalStack3.style.flexGrow = 1.0; +// horizontalStack3.children = @[_flexBasisBtn, spacer, _flexBasisValue]; +// _flexBasisValue.alignSelf = ASStackLayoutAlignSelfEnd; +// +// ASStackLayoutSpec *itemDescriptionStack = [ASStackLayoutSpec verticalStackLayoutSpec]; +// itemDescriptionStack.children = @[_itemDescription]; +// itemDescriptionStack.spacing = 5; +// itemDescriptionStack.style.flexGrow = 1.0; +// +// ASStackLayoutSpec *layoutableStack = [ASStackLayoutSpec verticalStackLayoutSpec]; +// layoutableStack.children = @[_layoutablePropertiesSectionTitle, horizontalStack, horizontalStack2, horizontalStack3, _alignSelfBtn]; +// layoutableStack.spacing = 5; +// layoutableStack.style.flexGrow = 1.0; +// +// ASStackLayoutSpec *layoutSpecStack = [ASStackLayoutSpec verticalStackLayoutSpec]; +// layoutSpecStack.children = @[_layoutSpecPropertiesSectionTitle, _alignItemsBtn]; +// layoutSpecStack.spacing = 5; +// layoutSpecStack.style.flexGrow = 1.0; +// +// ASStackLayoutSpec *debugHelpStack = [ASStackLayoutSpec verticalStackLayoutSpec]; +// debugHelpStack.children = @[_debugSectionTitle, _vizNodeInsetSizeBtn, _vizNodeBordersBtn]; +// debugHelpStack.spacing = 5; +// debugHelpStack.style.flexGrow = 1.0; +// +// ASStackLayoutSpec *verticalLayoutableStack = [ASStackLayoutSpec verticalStackLayoutSpec]; +// verticalLayoutableStack.style.flexGrow = 1.0; +// verticalLayoutableStack.spacing = 20; +// verticalLayoutableStack.children = @[_parentNodeNavBtn, horizontalStackNav, _childNodeNavBtn, itemDescriptionStack, layoutableStack, layoutSpecStack, debugHelpStack]; +// verticalLayoutableStack.alignItems = ASStackLayoutAlignItemsStretch; // stretch headerStack to fill horizontal space +// +// ASLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(100, 10, 10, 10) child:verticalLayoutableStack]; +// insetSpec.style.flexGrow = 1.0; +// return insetSpec; +//} +// +//#pragma mark - configure Inspector node for layoutable +//- (void)updateInspectorWithLayoutable +//{ +// _itemDescription.attributedText = [self attributedStringFromLayoutable:_layoutElementToEdit]; +// +// if ([self node]) { +// UIColor *nodeBackgroundColor = [[self node] backgroundColor]; +// UIImage *colorBtnImg = [ASLayoutElementInspectorNode imageForButtonWithBackgroundColor:nodeBackgroundColor +// borderColor:[UIColor whiteColor] +// borderWidth:3]; +// [_itemBackgroundColorBtn setBackgroundImage:colorBtnImg forState:ASControlStateNormal]; +// } else { +// _itemBackgroundColorBtn.enabled = NO; +// } +// +// _flexGrowBtn.selected = [self.layoutElementToEdit flexGrow]; +// _flexGrowValue.attributedText = [self attributedStringFromString: (_flexGrowBtn.selected) ? @"YES" : @"NO"]; +// +// _flexShrinkBtn.selected = self.layoutElementToEdit.style.flexShrink; +// _flexShrinkValue.attributedText = [self attributedStringFromString: (_flexShrinkBtn.selected) ? @"YES" : @"NO"]; +// +// // _flexBasisBtn.selected = self.layoutElementToEdit.style.flexShrink; +// // _flexBasisValue.attributedText = [self attributedStringFromString: (_flexBasisBtn.selected) ? @"YES" : @"NO"]; +// +// +// NSUInteger alignSelfValue = [self.layoutElementToEdit alignSelf]; +// NSString *newTitle = [@"alignSelf:" stringByAppendingString:[self alignSelfName:alignSelfValue]]; +// [_alignSelfBtn setAttributedTitle:[self attributedStringFromString:newTitle] forState:ASControlStateNormal]; +// +// if ([self layoutSpec]) { +// _alignItemsBtn.enabled = YES; +//// NSUInteger alignItemsValue = [[self layoutSpec] alignItems]; +//// newTitle = [@"alignItems:" stringByAppendingString:[self alignSelfName:alignItemsValue]]; +//// [_alignItemsBtn setAttributedTitle:[self attributedStringFromString:newTitle] forState:ASControlStateNormal]; +// } +// +// [self setNeedsLayout]; +//} + + +//- (void)enableInspectorNodesForLayoutable +//{ +// if ([self layoutSpec]) { +// +// _itemBackgroundColorBtn.enabled = YES; +// _flexGrowBtn.enabled = YES; +// _flexShrinkBtn.enabled = YES; +// _flexBasisBtn.enabled = YES; +// _alignSelfBtn.enabled = YES; +// _spacingBeforeBtn.enabled = YES; +// _spacingAfterBtn.enabled = YES; +// _alignItemsBtn.enabled = YES; +// +// } else if ([self node]) { +// +// _itemBackgroundColorBtn.enabled = YES; +// _flexGrowBtn.enabled = YES; +// _flexShrinkBtn.enabled = YES; +// _flexBasisBtn.enabled = YES; +// _alignSelfBtn.enabled = YES; +// _spacingBeforeBtn.enabled = YES; +// _spacingAfterBtn.enabled = YES; +// _alignItemsBtn.enabled = NO; +// +// } else { +// +// _itemBackgroundColorBtn.enabled = NO; +// _flexGrowBtn.enabled = NO; +// _flexShrinkBtn.enabled = NO; +// _flexBasisBtn.enabled = NO; +// _alignSelfBtn.enabled = NO; +// _spacingBeforeBtn.enabled = NO; +// _spacingAfterBtn.enabled = NO; +// _alignItemsBtn.enabled = YES; +// } +//} + +//+ (NSDictionary *)alignSelfTypeNames +//{ +// return @{@(ASStackLayoutAlignSelfAuto) : @"Auto", +// @(ASStackLayoutAlignSelfStart) : @"Start", +// @(ASStackLayoutAlignSelfEnd) : @"End", +// @(ASStackLayoutAlignSelfCenter) : @"Center", +// @(ASStackLayoutAlignSelfStretch) : @"Stretch"}; +//} +// +//- (NSString *)alignSelfName:(NSUInteger)type +//{ +// return [[self class] alignSelfTypeNames][@(type)]; +//} +// +//+ (NSDictionary *)alignItemTypeNames +//{ +// return @{@(ASStackLayoutAlignItemsBaselineFirst) : @"BaselineFirst", +// @(ASStackLayoutAlignItemsBaselineLast) : @"BaselineLast", +// @(ASStackLayoutAlignItemsCenter) : @"Center", +// @(ASStackLayoutAlignItemsEnd) : @"End", +// @(ASStackLayoutAlignItemsStart) : @"Start", +// @(ASStackLayoutAlignItemsStretch) : @"Stretch"}; +//} +// +//- (NSString *)alignItemName:(NSUInteger)type +//{ +// return [[self class] alignItemTypeNames][@(type)]; +//} + +//#pragma mark - gesture handling +//- (void)changeColor:(ASButtonNode *)sender +//{ +// if ([self node]) { +// NSArray *colorArray = @[[UIColor orangeColor], +// [UIColor redColor], +// [UIColor greenColor], +// [UIColor purpleColor]]; +// +// UIColor *nodeBackgroundColor = [(ASDisplayNode *)self.layoutElementToEdit backgroundColor]; +// +// NSUInteger colorIndex = [colorArray indexOfObject:nodeBackgroundColor]; +// colorIndex = (colorIndex + 1 < [colorArray count]) ? colorIndex + 1 : 0; +// +// [[self node] setBackgroundColor: [colorArray objectAtIndex:colorIndex]]; +// } +// +// [self updateInspectorWithLayoutable]; +//} +// +//- (void)setFlexGrowValue:(ASButtonNode *)sender +//{ +// [sender setSelected:!sender.isSelected]; // FIXME: fix ASControlNode documentation that this is automatic - unlike highlighted, it is up to the application to decide when a button should be selected or not. Selected is a more persistant thing and highlighted is for the moment, like as a user has a finger on it, +// +// if ([self layoutSpec]) { +// [[self layoutSpec] setFlexGrow:sender.isSelected]; +// } else if ([self node]) { +// [[self node] setFlexGrow:sender.isSelected]; +// } +// +// [self updateInspectorWithLayoutable]; +//} +// +//- (void)setFlexShrinkValue:(ASButtonNode *)sender +//{ +// [sender setSelected:!sender.isSelected]; // FIXME: fix ASControlNode documentation that this is automatic - unlike highlighted, it is up to the application to decide when a button should be selected or not. Selected is a more persistant thing and highlighted is for the moment, like as a user has a finger on it, +// +// if ([self layoutSpec]) { +// [[self layoutSpec] setFlexShrink:sender.isSelected]; +// } else if ([self node]) { +// [[self node] setFlexShrink:sender.isSelected]; +// } +// +// [self updateInspectorWithLayoutable]; +//} +// +//- (void)setAlignSelfValue:(ASButtonNode *)sender +//{ +// NSUInteger currentAlignSelfValue; +// NSUInteger nextAlignSelfValue; +// +// if ([self layoutSpec]) { +// currentAlignSelfValue = [[self layoutSpec] alignSelf]; +// nextAlignSelfValue = (currentAlignSelfValue + 1 <= ASStackLayoutAlignSelfStretch) ? currentAlignSelfValue + 1 : 0; +// [[self layoutSpec] setAlignSelf:nextAlignSelfValue]; +// +// } else if ([self node]) { +// currentAlignSelfValue = [[self node] alignSelf]; +// nextAlignSelfValue = (currentAlignSelfValue + 1 <= ASStackLayoutAlignSelfStretch) ? currentAlignSelfValue + 1 : 0; +// [[self node] setAlignSelf:nextAlignSelfValue]; +// } +// +// [self updateInspectorWithLayoutable]; +//} +// +//- (void)setAlignItemsValue:(ASButtonNode *)sender +//{ +// NSUInteger currentAlignItemsValue; +// NSUInteger nextAlignItemsValue; +// +// if ([self layoutSpec]) { +// currentAlignItemsValue = [[self layoutSpec] alignSelf]; +// nextAlignItemsValue = (currentAlignItemsValue + 1 <= ASStackLayoutAlignSelfStretch) ? currentAlignItemsValue + 1 : 0; +//// [[self layoutSpec] setAlignItems:nextAlignItemsValue]; +// +// } else if ([self node]) { +// currentAlignItemsValue = [[self node] alignSelf]; +// nextAlignItemsValue = (currentAlignItemsValue + 1 <= ASStackLayoutAlignSelfStretch) ? currentAlignItemsValue + 1 : 0; +//// [[self node] setAlignItems:nextAlignItemsValue]; +// } +// +// [self updateInspectorWithLayoutable]; +//} +//- (void)setFlexBasisValue:(ASButtonNode *)sender +//{ +// [sender setSelected:!sender.isSelected]; // FIXME: fix ASControlNode documentation that this is automatic - unlike highlighted, it is up to the application to decide when a button should be selected or not. Selected is a more persistant thing and highlighted is for the moment, like as a user has a finger on it, +// FIXME: finish +//} +// +//- (void)setVizNodeInsets:(ASButtonNode *)sender +//{ +// BOOL newState = !sender.selected; +// +// if (newState == YES) { +// self.vizNodeInsetSize = 0; +// [self.delegate toggleVisualization:NO]; // FIXME +// [self.delegate toggleVisualization:YES]; // FIXME +// _vizNodeBordersBtn.selected = YES; +// +// } else { +// self.vizNodeInsetSize = 10; +// [self.delegate toggleVisualization:NO]; // FIXME +// [self.delegate toggleVisualization:YES]; // FIXME +// } +// +// sender.selected = newState; +//} +// +//- (void)setVizNodeBorders:(ASButtonNode *)sender +//{ +// BOOL newState = !sender.selected; +// +// [self.delegate toggleVisualization:newState]; // FIXME +// +// sender.selected = newState; +//} + +@end diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASChangeSetDataController.h b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASChangeSetDataController.h new file mode 100644 index 0000000000..49df1dc24d --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASChangeSetDataController.h @@ -0,0 +1,31 @@ +// +// ASChangeSetDataController.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#ifndef MINIMAL_ASDK + +#import + +/** + * @abstract Subclass of ASDataController that simulates ordering of operations in batch updates defined in UITableView and UICollectionView. + * + * @discussion The ordering is achieved by using _ASHierarchyChangeSet to enqueue and sort operations. + * More information about the ordering and the index paths used for operations can be found here: + * https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/ManageInsertDeleteRow/ManageInsertDeleteRow.html#//apple_ref/doc/uid/TP40007451-CH10-SW17 + * + * @see ASDataController + * @see _ASHierarchyChangeSet + */ +@interface ASChangeSetDataController : ASDataController + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASChangeSetDataController.mm b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASChangeSetDataController.mm new file mode 100644 index 0000000000..c41820dbb0 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASChangeSetDataController.mm @@ -0,0 +1,212 @@ +// +// ASChangeSetDataController.m +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#ifndef MINIMAL_ASDK + +#import "ASChangeSetDataController.h" +#import "_ASHierarchyChangeSet.h" +#import "ASAssert.h" +#import "ASDataController+Subclasses.h" + +@implementation ASChangeSetDataController { + NSInteger _changeSetBatchUpdateCounter; + _ASHierarchyChangeSet *_changeSet; +} + +- (void)dealloc +{ + ASDisplayNodeCAssert(_changeSetBatchUpdateCounter == 0, @"ASChangeSetDataController deallocated in the middle of a batch update."); +} + +#pragma mark - Batching (External API) + +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + if (_changeSetBatchUpdateCounter <= 0) { + _changeSetBatchUpdateCounter = 0; + _changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[self itemCountsFromDataSource]]; + } + _changeSetBatchUpdateCounter++; +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + _changeSetBatchUpdateCounter--; + + // Prevent calling endUpdatesAnimated:completion: in an unbalanced way + NSAssert(_changeSetBatchUpdateCounter >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call"); + + [_changeSet addCompletionHandler:completion]; + if (_changeSetBatchUpdateCounter == 0) { + void (^batchCompletion)(BOOL) = _changeSet.completionHandler; + + /** + * If the initial reloadData has not been called, just bail because we don't have + * our old data source counts. + * See ASUICollectionViewTests.testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable + * For the issue that UICollectionView has that we're choosing to workaround. + */ + if (!self.initialReloadDataHasBeenCalled) { + if (batchCompletion != nil) { + batchCompletion(YES); + } + _changeSet = nil; + return; + } + + [self invalidateDataSourceItemCounts]; + + // Attempt to mark the update completed. This is when update validation will occur inside the changeset. + // If an invalid update exception is thrown, we catch it and inject our "validationErrorSource" object, + // which is the table/collection node's data source, into the exception reason to help debugging. + @try { + [_changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; + } @catch (NSException *e) { + id responsibleDataSource = self.validationErrorSource; + if (e.name == ASCollectionInvalidUpdateException && responsibleDataSource != nil) { + [NSException raise:ASCollectionInvalidUpdateException format:@"%@: %@", [responsibleDataSource class], e.reason]; + } else { + @throw e; + } + } + + ASDataControllerLogEvent(self, @"triggeredUpdate: %@", _changeSet); + + [super beginUpdates]; + + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { + [super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [super insertSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { + [super insertRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + +#if ASEVENTLOG_ENABLE + NSString *changeSetDescription = ASObjectDescriptionMakeTiny(_changeSet); + batchCompletion = ^(BOOL finished) { + if (batchCompletion != nil) { + batchCompletion(finished); + } + ASDataControllerLogEvent(self, @"finishedUpdate: %@", changeSetDescription); + }; +#endif + + [super endUpdatesAnimated:animated completion:batchCompletion]; + + _changeSet = nil; + } +} + +- (BOOL)batchUpdating +{ + BOOL batchUpdating = (_changeSetBatchUpdateCounter != 0); + // _changeSet must be available during batch update + ASDisplayNodeAssertTrue(batchUpdating == (_changeSet != nil)); + return batchUpdating; +} + +- (void)waitUntilAllUpdatesAreCommitted +{ + ASDisplayNodeAssertMainThread(); + if (self.batchUpdating) { + // This assertion will be enabled soon. +// ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); + return; + } + + [super waitUntilAllUpdatesAreCommitted]; +} + +#pragma mark - Section Editing (External API) + +- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet insertSections:sections animationOptions:animationOptions]; + [self endUpdates]; +} + +- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet deleteSections:sections animationOptions:animationOptions]; + [self endUpdates]; +} + +- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet reloadSections:sections animationOptions:animationOptions]; + [self endUpdates]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; + [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; + [self endUpdates]; +} + +#pragma mark - Row Editing (External API) + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet insertItems:indexPaths animationOptions:animationOptions]; + [self endUpdates]; +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet deleteItems:indexPaths animationOptions:animationOptions]; + [self endUpdates]; +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; + [self endUpdates]; +} + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; + [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; + [self endUpdates]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASCollectionDataController.h b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASCollectionDataController.h new file mode 100644 index 0000000000..adc1d8d957 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASCollectionDataController.h @@ -0,0 +1,57 @@ +// +// ASCollectionDataController.h +// AsyncDisplayKit +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import + +@class ASDisplayNode; +@class ASCollectionDataController; +@protocol ASSectionContext; + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASCollectionDataControllerSource + +/** + The constrained size range for layout. + */ +- (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController sections:(NSIndexSet *)sections; + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +- (nullable id)dataController:(ASCollectionDataController *)dataController contextForSection:(NSInteger)section; + +@optional + +- (ASCellNode *)dataController:(ASCollectionDataController *)dataController supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +- (ASCellNodeBlock)dataController:(ASCollectionDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +@end + +@interface ASCollectionDataController : ASDataController + +- (instancetype)initWithDataSource:(id)dataSource eventLog:(nullable ASEventLog *)eventLog NS_DESIGNATED_INITIALIZER; + +- (nullable ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +- (nullable id)contextForSection:(NSInteger)section; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASCollectionDataController.mm b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASCollectionDataController.mm new file mode 100644 index 0000000000..2dc4bb203c --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -0,0 +1,320 @@ +// +// ASCollectionDataController.mm +// AsyncDisplayKit +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import +#import +#import +#import +#import +#import + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +@interface ASCollectionDataController () { + BOOL _dataSourceImplementsSupplementaryNodeBlockOfKindAtIndexPath; + NSInteger _nextSectionID; + NSMutableArray *_sections; + NSArray *_pendingSections; + + /** + * supplementaryKinds can only be accessed on the main thread + * and so we set this in the -prepare stage, and then read it during the -will + * stage of each update operation. + */ + NSArray *_supplementaryKindsForPendingOperation; +} + +- (id)collectionDataSource; + +@end + +@implementation ASCollectionDataController { + NSMutableDictionary *> *_pendingNodeContexts; +} + +- (instancetype)initWithDataSource:(id)dataSource eventLog:(ASEventLog *)eventLog +{ + self = [super initWithDataSource:dataSource eventLog:eventLog]; + if (self != nil) { + _pendingNodeContexts = [NSMutableDictionary dictionary]; + _dataSourceImplementsSupplementaryNodeBlockOfKindAtIndexPath = [dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:)]; + + ASDisplayNodeAssertTrue(_dataSourceImplementsSupplementaryNodeBlockOfKindAtIndexPath || [dataSource respondsToSelector:@selector(dataController:supplementaryNodeOfKind:atIndexPath:)]); + + _nextSectionID = 0; + _sections = [NSMutableArray array]; + } + return self; +} + +- (void)prepareForReloadDataWithSectionCount:(NSInteger)newSectionCount +{ + ASDisplayNodeAssertMainThread(); + NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newSectionCount)]; + + [_sections removeAllObjects]; + [self _populatePendingSectionsFromDataSource:sections]; + + for (NSString *kind in [self supplementaryKindsInSections:sections]) { + LOG(@"Populating elements of kind: %@", kind); + NSMutableArray *contexts = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableContexts:contexts]; + _pendingNodeContexts[kind] = contexts; + } +} + +- (void)willReloadDataWithSectionCount:(NSInteger)newSectionCount +{ + NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newSectionCount)]; + + [self applyPendingSections:sectionIndexes]; + + // Assert that ASDataController has already deleted all the old sections for us. + ASDisplayNodeAssert([self editingNodesOfKind:ASDataControllerRowNodeKind].count == 0, @"Expected that all old sections were deleted before %@. Sections: %@", NSStringFromSelector(_cmd), [self editingNodesOfKind:ASDataControllerRowNodeKind]); + + [_pendingNodeContexts enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray * _Nonnull contexts, __unused BOOL * _Nonnull stop) { + // Insert each section + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:newSectionCount]; + for (int i = 0; i < newSectionCount; i++) { + [sections addObject:[NSMutableArray array]]; + } + [self insertSections:sections ofKind:kind atIndexSet:sectionIndexes completion:nil]; + + [self batchLayoutNodesFromContexts:contexts batchCompletion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; + }]; + [_pendingNodeContexts removeAllObjects]; +} + +- (void)prepareForInsertSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + [self _populatePendingSectionsFromDataSource:sections]; + + for (NSString *kind in [self supplementaryKindsInSections:sections]) { + LOG(@"Populating elements of kind: %@, for sections: %@", kind, sections); + NSMutableArray *contexts = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableContexts:contexts]; + _pendingNodeContexts[kind] = contexts; + } +} + +- (void)willInsertSections:(NSIndexSet *)sections +{ + [self applyPendingSections:sections]; + + [_pendingNodeContexts enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray * _Nonnull contexts, BOOL * _Nonnull stop) { + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { + [sectionArray addObject:[NSMutableArray array]]; + } + + [self insertSections:sectionArray ofKind:kind atIndexSet:sections completion:nil]; + [self batchLayoutNodesFromContexts:contexts batchCompletion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; + }]; + [_pendingNodeContexts removeAllObjects]; +} + +- (void)willDeleteSections:(NSIndexSet *)sections +{ + [_sections removeObjectsAtIndexes:sections]; +} + +- (void)prepareForInsertRowsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + NSIndexSet *sections = [NSIndexSet as_sectionsFromIndexPaths:indexPaths]; + for (NSString *kind in [self supplementaryKindsInSections:sections]) { + LOG(@"Populating elements of kind: %@, for index paths: %@", kind, indexPaths); + NSMutableArray *contexts = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind atIndexPaths:indexPaths mutableContexts:contexts]; + _pendingNodeContexts[kind] = contexts; + } +} + +- (void)willInsertRowsAtIndexPaths:(NSArray *)indexPaths +{ + [_pendingNodeContexts enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray * _Nonnull contexts, BOOL * _Nonnull stop) { + [self batchLayoutNodesFromContexts:contexts batchCompletion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; + }]; + + [_pendingNodeContexts removeAllObjects]; +} + +- (void)prepareForDeleteRowsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + NSIndexSet *sections = [NSIndexSet as_sectionsFromIndexPaths:indexPaths]; + _supplementaryKindsForPendingOperation = [self supplementaryKindsInSections:sections]; + for (NSString *kind in _supplementaryKindsForPendingOperation) { + NSMutableArray *contexts = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind atIndexPaths:indexPaths mutableContexts:contexts]; + _pendingNodeContexts[kind] = contexts; + } +} + +- (void)willDeleteRowsAtIndexPaths:(NSArray *)indexPaths +{ + for (NSString *kind in _supplementaryKindsForPendingOperation) { + NSArray *deletedIndexPaths = ASIndexPathsInMultidimensionalArrayIntersectingIndexPaths([self editingNodesOfKind:kind], indexPaths); + + [self deleteNodesOfKind:kind atIndexPaths:deletedIndexPaths completion:nil]; + + // If any of the contexts remain after the deletion, re-insert them, e.g. + // UICollectionElementKindSectionHeader remains even if item 0 is deleted. + NSMutableArray *reinsertedContexts = [NSMutableArray array]; + for (ASIndexedNodeContext *context in _pendingNodeContexts[kind]) { + if ([deletedIndexPaths containsObject:context.indexPath]) { + [reinsertedContexts addObject:context]; + } + } + + [self batchLayoutNodesFromContexts:reinsertedContexts batchCompletion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; + } + [_pendingNodeContexts removeAllObjects]; + _supplementaryKindsForPendingOperation = nil; +} + +- (void)_populatePendingSectionsFromDataSource:(NSIndexSet *)sectionIndexes +{ + ASDisplayNodeAssertMainThread(); + + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionIndexes.count]; + [sectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + id context = [self.collectionDataSource dataController:self contextForSection:idx]; + [sections addObject:[[ASSection alloc] initWithSectionID:_nextSectionID context:context]]; + _nextSectionID++; + }]; + _pendingSections = sections; +} + +- (void)_populateSupplementaryNodesOfKind:(NSString *)kind withSections:(NSIndexSet *)sections mutableContexts:(NSMutableArray *)contexts +{ + __weak id environment = [self.environmentDelegate dataControllerEnvironment]; + + [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger sec = range.location; sec < NSMaxRange(range); sec++) { + NSUInteger itemCount = [self.collectionDataSource dataController:self supplementaryNodesOfKind:kind inSection:sec]; + for (NSUInteger i = 0; i < itemCount; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sec]; + [self _populateSupplementaryNodeOfKind:kind atIndexPath:indexPath mutableContexts:contexts environment:environment]; + } + } + }]; +} + +- (void)_populateSupplementaryNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths mutableContexts:(NSMutableArray *)contexts +{ + __weak id environment = [self.environmentDelegate dataControllerEnvironment]; + + NSMutableIndexSet *sections = [NSMutableIndexSet indexSet]; + for (NSIndexPath *indexPath in indexPaths) { + [sections addIndex:indexPath.section]; + } + + [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger sec = range.location; sec < NSMaxRange(range); sec++) { + NSUInteger itemCount = [self.collectionDataSource dataController:self supplementaryNodesOfKind:kind inSection:sec]; + for (NSUInteger i = 0; i < itemCount; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sec]; + [self _populateSupplementaryNodeOfKind:kind atIndexPath:indexPath mutableContexts:contexts environment:environment]; + } + } + }]; +} + +- (void)_populateSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath mutableContexts:(NSMutableArray *)contexts environment:(id)environment +{ + ASCellNodeBlock supplementaryCellBlock; + if (_dataSourceImplementsSupplementaryNodeBlockOfKindAtIndexPath) { + supplementaryCellBlock = [self.collectionDataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath]; + } else { + ASCellNode *supplementaryNode = [self.collectionDataSource dataController:self supplementaryNodeOfKind:kind atIndexPath:indexPath]; + supplementaryCellBlock = ^{ return supplementaryNode; }; + } + + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + ASIndexedNodeContext *context = [[ASIndexedNodeContext alloc] initWithNodeBlock:supplementaryCellBlock + indexPath:indexPath + supplementaryElementKind:kind + constrainedSize:constrainedSize + environment:environment]; + [contexts addObject:context]; +} + +#pragma mark - Sizing query + +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if ([kind isEqualToString:ASDataControllerRowNodeKind]) { + return [super constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + } else { + ASDisplayNodeAssertMainThread(); + return [self.collectionDataSource dataController:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; + } +} + +#pragma mark - External supplementary store and section context querying + +- (ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + NSArray *nodesOfKind = [self completedNodesOfKind:kind]; + NSInteger section = indexPath.section; + if (section < nodesOfKind.count) { + NSArray *nodesOfKindInSection = nodesOfKind[section]; + NSInteger itemIndex = indexPath.item; + if (itemIndex < nodesOfKindInSection.count) { + return nodesOfKindInSection[itemIndex]; + } + } + return nil; +} + +- (id)contextForSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssertTrue(section >= 0 && section < _sections.count); + return _sections[section].context; +} + +#pragma mark - Private Helpers + +- (NSArray *)supplementaryKindsInSections:(NSIndexSet *)sections +{ + return [self.collectionDataSource supplementaryNodeKindsInDataController:self sections:sections]; +} + +- (id)collectionDataSource +{ + return (id)self.dataSource; +} + +- (void)applyPendingSections:(NSIndexSet *)sectionIndexes +{ + [_sections insertObjects:_pendingSections atIndexes:sectionIndexes]; + _pendingSections = nil; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASDataController.mm b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASDataController.mm new file mode 100644 index 0000000000..047b59099a --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASDataController.mm @@ -0,0 +1,1093 @@ +// +// ASDataController.mm +// AsyncDisplayKit +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +#define AS_MEASURE_AVOIDED_DATACONTROLLER_WORK 0 + +#define RETURN_IF_NO_DATASOURCE(val) if (_dataSource == nil) { return val; } +#define ASSERT_ON_EDITING_QUEUE ASDisplayNodeAssertNotNil(dispatch_get_specific(&kASDataControllerEditingQueueKey), @"%@ must be called on the editing transaction queue.", NSStringFromSelector(_cmd)) + +const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; +const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditingQueueKey"; +const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext"; + +NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; +NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; + +#if AS_MEASURE_AVOIDED_DATACONTROLLER_WORK +@interface ASDataController (AvoidedWorkMeasuring) ++ (void)_didLayoutNode; ++ (void)_expectToInsertNodes:(NSUInteger)count; +@end +#endif + +@interface ASDataController () { + NSMutableDictionary *_nodeContexts; // Main thread only. This is modified immediately during edits i.e. these are in the dataSource's index space. + NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available. + NSMutableDictionary *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. + NSMutableDictionary *_editingNodes; // Modified on _editingTransactionQueue only. Updates propagated to _completedNodes. + BOOL _itemCountsFromDataSourceAreValid; // Main thread only. + std::vector _itemCountsFromDataSource; // Main thread only. + + ASMainSerialQueue *_mainSerialQueue; + + dispatch_queue_t _editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. + dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting. + + BOOL _initialReloadDataHasBeenCalled; + + BOOL _delegateDidInsertNodes; + BOOL _delegateDidDeleteNodes; + BOOL _delegateDidInsertSections; + BOOL _delegateDidDeleteSections; +} + +@end + +@implementation ASDataController + +#pragma mark - Lifecycle + +- (instancetype)initWithDataSource:(id)dataSource eventLog:(ASEventLog *)eventLog +{ + if (!(self = [super init])) { + return nil; + } + + _dataSource = dataSource; + +#if ASEVENTLOG_ENABLE + _eventLog = eventLog; +#endif + + _nodeContexts = [NSMutableDictionary dictionary]; + _completedNodes = [NSMutableDictionary dictionary]; + _editingNodes = [NSMutableDictionary dictionary]; + + _nodeContexts[ASDataControllerRowNodeKind] = [NSMutableArray array]; + _completedNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; + _editingNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; + + _mainSerialQueue = [[ASMainSerialQueue alloc] init]; + + const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding]; + _editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(_editingTransactionQueue, &kASDataControllerEditingQueueKey, &kASDataControllerEditingQueueContext, NULL); + _editingTransactionGroup = dispatch_group_create(); + + return self; +} + +- (instancetype)init +{ + ASDisplayNodeFailAssert(@"Failed to call designated initializer."); + id fakeDataSource = nil; + ASEventLog *eventLog = nil; + return [self initWithDataSource:fakeDataSource eventLog:eventLog]; +} + +- (void)setDelegate:(id)delegate +{ + if (_delegate == delegate) { + return; + } + + _delegate = delegate; + + // Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later. + _delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)]; + _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodes:atIndexPaths:withAnimationOptions:)]; + _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; + _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; +} + ++ (NSUInteger)parallelProcessorCount +{ + static NSUInteger parallelProcessorCount; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + parallelProcessorCount = [[NSProcessInfo processInfo] activeProcessorCount]; + }); + + return parallelProcessorCount; +} + +#pragma mark - Cell Layout + +- (void)batchLayoutNodesFromContexts:(NSArray *)contexts batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler +{ + ASSERT_ON_EDITING_QUEUE; +#if AS_MEASURE_AVOIDED_DATACONTROLLER_WORK + [ASDataController _expectToInsertNodes:contexts.count]; +#endif + + if (contexts.count == 0) { + batchCompletionHandler(@[], @[]); + return; + } + + ASProfilingSignpostStart(2, _dataSource); + + NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; + NSUInteger count = contexts.count; + + // Processing in batches + for (NSUInteger i = 0; i < count; i += blockSize) { + NSRange batchedRange = NSMakeRange(i, MIN(count - i, blockSize)); + NSArray *batchedContexts = [contexts subarrayWithRange:batchedRange]; + NSArray *nodes = [self _layoutNodesFromContexts:batchedContexts]; + NSArray *indexPaths = [ASIndexedNodeContext indexPathsFromContexts:batchedContexts]; + batchCompletionHandler(nodes, indexPaths); + } + + ASProfilingSignpostEnd(2, _dataSource); +} + +/** + * Measure and layout the given node with the constrained size range. + */ +- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize +{ + ASDisplayNodeAssert(ASSizeRangeHasSignificantArea(constrainedSize), @"Attempt to layout cell node with invalid size range %@", NSStringFromASSizeRange(constrainedSize)); + + CGRect frame = CGRectZero; + frame.size = [node layoutThatFits:constrainedSize].size; + node.frame = frame; +} + +/** + * Measures and defines the layout for each node in optimized batches on an editing queue, inserting the results into the backing store. + */ +- (void)_batchLayoutAndInsertNodesFromContexts:(NSArray *)contexts withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASSERT_ON_EDITING_QUEUE; + + [self batchLayoutNodesFromContexts:contexts batchCompletion:^(NSArray *nodes, NSArray *indexPaths) { + // Insert finished nodes into data storage + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +- (NSArray *)_layoutNodesFromContexts:(NSArray *)contexts +{ + ASSERT_ON_EDITING_QUEUE; + + NSUInteger nodeCount = contexts.count; + if (!nodeCount || _dataSource == nil) { + return nil; + } + + __strong ASCellNode **allocatedNodeBuffer = (__strong ASCellNode **)calloc(nodeCount, sizeof(ASCellNode *)); + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + ASDispatchApply(nodeCount, queue, 0, ^(size_t i) { + RETURN_IF_NO_DATASOURCE(); + + // Allocate the node. + ASIndexedNodeContext *context = contexts[i]; + ASCellNode *node = context.node; + if (node == nil) { + ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, self.dataSource); + node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps. + } + + // Layout the node if the size range is valid. + ASSizeRange sizeRange = context.constrainedSize; + if (ASSizeRangeHasSignificantArea(sizeRange)) { + [self _layoutNode:node withConstrainedSize:sizeRange]; + } + +#if AS_MEASURE_AVOIDED_DATACONTROLLER_WORK + [ASDataController _didLayoutNode]; +#endif + allocatedNodeBuffer[i] = node; + }); + + BOOL canceled = _dataSource == nil; + + // Create nodes array + NSArray *nodes = canceled ? nil : [NSArray arrayWithObjects:allocatedNodeBuffer count:nodeCount]; + + // Nil out buffer indexes to allow arc to free the stored cells. + for (int i = 0; i < nodeCount; i++) { + allocatedNodeBuffer[i] = nil; + } + free(allocatedNodeBuffer); + + return nodes; +} + +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; +} + +#pragma mark - External Data Querying + Editing + +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(ASDataControllerCompletionBlock)completionBlock +{ + ASSERT_ON_EDITING_QUEUE; + if (!indexPaths.count || _dataSource == nil) { + return; + } + + NSMutableArray *editingNodes = _editingNodes[kind]; + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSMutableArray *completedNodes = ASTwoDimensionalArrayDeepMutableCopy(editingNodes); + + [_mainSerialQueue performBlockOnMainThread:^{ + _completedNodes[kind] = completedNodes; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + }]; +} + +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(ASDataControllerCompletionBlock)completionBlock +{ + ASSERT_ON_EDITING_QUEUE; + if (!indexPaths.count || _dataSource == nil) { + return; + } + + LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@, full index paths in _editingNodes = %@", indexPaths, kind, ASIndexPathsForTwoDimensionalArray(_editingNodes[kind])); + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[kind], indexPaths); + + [_mainSerialQueue performBlockOnMainThread:^{ + NSMutableArray *allNodes = _completedNodes[kind]; + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(allNodes, indexPaths); + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(allNodes, indexPaths); + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + }]; +} + +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock +{ + ASSERT_ON_EDITING_QUEUE; + if (!indexSet.count|| _dataSource == nil) { + return; + } + + if (_editingNodes[kind] == nil) { + _editingNodes[kind] = [NSMutableArray array]; + } + + [_editingNodes[kind] insertObjects:sections atIndexes:indexSet]; + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSArray *sectionsForCompleted = ASTwoDimensionalArrayDeepMutableCopy(sections); + + [_mainSerialQueue performBlockOnMainThread:^{ + [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; + if (completionBlock) { + completionBlock(sections, indexSet); + } + }]; +} + +- (void)deleteSections:(NSIndexSet *)indexSet completion:(void (^)())completionBlock +{ + ASSERT_ON_EDITING_QUEUE; + if (!indexSet.count || _dataSource == nil) { + return; + } + + [_editingNodes enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray *sections, BOOL * _Nonnull stop) { + [sections removeObjectsAtIndexes:indexSet]; + }]; + [_mainSerialQueue performBlockOnMainThread:^{ + [_completedNodes enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray *sections, BOOL * _Nonnull stop) { + [sections removeObjectsAtIndexes:indexSet]; + }]; + if (completionBlock) { + completionBlock(); + } + }]; +} + +#pragma mark - Internal Data Querying + Editing + +/** + * Inserts the specified nodes into the given index paths and notifies the delegate of newly inserted nodes. + * + * @discussion Nodes are first inserted into the editing store, then the completed store is replaced by a deep copy + * of the editing nodes. The delegate is invoked on the main thread. + */ +- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASSERT_ON_EDITING_QUEUE; + + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + ASDisplayNodeAssertMainThread(); + + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes the specified nodes at the given index paths and notifies the delegate of the nodes removed. + * + * @discussion Nodes are first removed from the editing store then removed from the completed store on the main thread. + * Once the backing stores are consistent, the delegate is invoked on the main thread. + */ +- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASSERT_ON_EDITING_QUEUE; + + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + ASDisplayNodeAssertMainThread(); + + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Inserts sections, represented as arrays, into the backing store at the given indices and notifies the delegate. + * + * @discussion The section arrays are inserted into the editing store, then a deep copy of the sections are inserted + * in the completed store on the main thread. The delegate is invoked on the main thread. + */ +- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASSERT_ON_EDITING_QUEUE; + + [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { + ASDisplayNodeAssertMainThread(); + + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes sections at the given indices from the backing store and notifies the delegate. + * + * @discussion Section array are first removed from the editing store, then the associated section in the completed + * store is removed on the main thread. The delegate is invoked on the main thread. + */ +- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASSERT_ON_EDITING_QUEUE; + + [self deleteSections:indexSet completion:^() { + ASDisplayNodeAssertMainThread(); + + if (_delegateDidDeleteSections) + [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + }]; +} + +#pragma mark - Initial Load & Full Reload (External API) + +- (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^)())completion +{ + [self _reloadDataWithAnimationOptions:animationOptions synchronously:NO completion:completion]; +} + +- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self _reloadDataWithAnimationOptions:animationOptions synchronously:YES completion:nil]; +} + +- (void)_reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions synchronously:(BOOL)synchronously completion:(void (^)())completion +{ + ASDisplayNodeAssertMainThread(); + + _initialReloadDataHasBeenCalled = YES; + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + [self invalidateDataSourceItemCounts]; + NSUInteger sectionCount = [self itemCountsFromDataSource].size(); + NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; + NSArray *newContexts = [self _populateNodeContextsFromDataSourceForSections:sectionIndexes]; + + // Update _nodeContexts + NSMutableArray *allContexts = _nodeContexts[ASDataControllerRowNodeKind]; + [allContexts removeAllObjects]; + NSArray *nodeIndexPaths = [ASIndexedNodeContext indexPathsFromContexts:newContexts]; + for (int i = 0; i < sectionCount; i++) { + [allContexts addObject:[[NSMutableArray alloc] init]]; + } + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(allContexts, nodeIndexPaths, newContexts); + + // Allow subclasses to perform setup before going into the edit transaction + [self prepareForReloadDataWithSectionCount:sectionCount]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + LOG(@"Edit Transaction - reloadData"); + + /** + * Leave the current data in the collection view until the first batch of nodes are laid out. + * Once the first batch is laid out, in one operation, replace all the sections and insert + * the first batch of items. + * + * We previously would replace all the sections immediately, and then start adding items as they + * were laid out. This resulted in more traffic to the UICollectionView and it also caused all the + * section headers to bunch up until the items come and fill out the sections. + */ + __block BOOL isFirstBatch = YES; + [self batchLayoutNodesFromContexts:newContexts batchCompletion:^(NSArray *nodes, NSArray *indexPaths) { + if (isFirstBatch) { + // -beginUpdates + [_mainSerialQueue performBlockOnMainThread:^{ + [_delegate dataControllerBeginUpdates:self]; + [_delegate dataControllerWillDeleteAllData:self]; + }]; + + // deleteSections: + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSUInteger oldSectionCount = [_editingNodes[ASDataControllerRowNodeKind] count]; + if (oldSectionCount) { + NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, oldSectionCount)]; + [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + } + + [self willReloadDataWithSectionCount:sectionCount]; + + // insertSections: + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; + for (int i = 0; i < sectionCount; i++) { + [sections addObject:[[NSMutableArray alloc] init]]; + } + [self _insertSections:sections atIndexSet:sectionIndexes withAnimationOptions:animationOptions]; + } + + // insertItemsAtIndexPaths: + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + + if (isFirstBatch) { + // -endUpdates + [_mainSerialQueue performBlockOnMainThread:^{ + [_delegate dataController:self endUpdatesAnimated:NO completion:nil]; + }]; + isFirstBatch = NO; + } + }]; + + if (completion) { + [_mainSerialQueue performBlockOnMainThread:completion]; + } + }); + if (synchronously) { + [self waitUntilAllUpdatesAreCommitted]; + } +} + +- (void)waitUntilAllUpdatesAreCommitted +{ + ASDisplayNodeAssertMainThread(); + + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + // Schedule block in main serial queue to wait until all operations are finished that are + // where scheduled while waiting for the _editingTransactionQueue to finish + [_mainSerialQueue performBlockOnMainThread:^{ }]; +} + +#pragma mark - Data Source Access (Calling _dataSource) + +/** + * Fetches row contexts for the provided sections from the data source. + */ +- (NSArray *)_populateNodeContextsFromDataSourceForSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + + __weak id environment = [self.environmentDelegate dataControllerEnvironment]; + + std::vector counts = [self itemCountsFromDataSource]; + NSMutableArray *contexts = [NSMutableArray array]; + [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger sectionIndex = range.location; sectionIndex < NSMaxRange(range); sectionIndex++) { + NSUInteger itemCount = counts[sectionIndex]; + for (NSUInteger i = 0; i < itemCount; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sectionIndex]; + ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; + + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; + [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock + indexPath:indexPath + supplementaryElementKind:nil + constrainedSize:constrainedSize + environment:environment]]; + } + } + }]; + return contexts; +} + +- (void)invalidateDataSourceItemCounts +{ + ASDisplayNodeAssertMainThread(); + _itemCountsFromDataSourceAreValid = NO; +} + +- (std::vector)itemCountsFromDataSource +{ + ASDisplayNodeAssertMainThread(); + if (NO == _itemCountsFromDataSourceAreValid) { + id source = self.dataSource; + NSInteger sectionCount = [source numberOfSectionsInDataController:self]; + std::vector newCounts; + newCounts.reserve(sectionCount); + for (NSInteger i = 0; i < sectionCount; i++) { + newCounts.push_back([source dataController:self rowsInSection:i]); + } + _itemCountsFromDataSource = newCounts; + _itemCountsFromDataSourceAreValid = YES; + } + return _itemCountsFromDataSource; +} + +#pragma mark - Batching (External API) + +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + // TODO: make this -waitUntilAllUpdatesAreCommitted? + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [_mainSerialQueue performBlockOnMainThread:^{ + // Deep copy _completedNodes to _externalCompletedNodes. + // Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate. + _externalCompletedNodes = ASTwoDimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); + + LOG(@"beginUpdates - begin updates call to delegate"); + [_delegate dataControllerBeginUpdates:self]; + }]; + }); +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion +{ + LOG(@"endUpdatesWithCompletion - beginning"); + ASDisplayNodeAssertMainThread(); + + // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. + // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction. + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [_mainSerialQueue performBlockOnMainThread:^{ + // Now that the transaction is done, _completedNodes can be accessed externally again. + _externalCompletedNodes = nil; + + LOG(@"endUpdatesWithCompletion - calling delegate end"); + [_delegate dataController:self endUpdatesAnimated:animated completion:completion]; + }]; + }); +} + +- (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + + void (^batchCompletion)(BOOL) = changeSet.completionHandler; + + /** + * If the initial reloadData has not been called, just bail because we don't have + * our old data source counts. + * See ASUICollectionViewTests.testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable + * For the issue that UICollectionView has that we're choosing to workaround. + */ + if (!self.initialReloadDataHasBeenCalled) { + if (batchCompletion != nil) { + batchCompletion(YES); + } + return; + } + + [self invalidateDataSourceItemCounts]; + + // Attempt to mark the update completed. This is when update validation will occur inside the changeset. + // If an invalid update exception is thrown, we catch it and inject our "validationErrorSource" object, + // which is the table/collection node's data source, into the exception reason to help debugging. + @try { + [changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; + } @catch (NSException *e) { + id responsibleDataSource = self.validationErrorSource; + if (e.name == ASCollectionInvalidUpdateException && responsibleDataSource != nil) { + [NSException raise:ASCollectionInvalidUpdateException format:@"%@: %@", [responsibleDataSource class], e.reason]; + } else { + @throw e; + } + } + + ASDataControllerLogEvent(self, @"triggeredUpdate: %@", changeSet); + + [self beginUpdates]; + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { + [self deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + [self deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [self insertSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { + [self insertRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + +#if ASEVENTLOG_ENABLE + NSString *changeSetDescription = ASObjectDescriptionMakeTiny(changeSet); + batchCompletion = ^(BOOL finished) { + if (batchCompletion != nil) { + batchCompletion(finished); + } + ASDataControllerLogEvent(self, @"finishedUpdate: %@", changeSetDescription); + }; +#endif + + [self endUpdatesAnimated:animated completion:batchCompletion]; +} + +#pragma mark - Section Editing (External API) + +- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - insertSections: %@", sections); + if (!_initialReloadDataHasBeenCalled) { + return; + } + + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + NSArray *contexts = [self _populateNodeContextsFromDataSourceForSections:sections]; + + // Update _nodeContexts + { + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { + [sectionArray addObject:[NSMutableArray array]]; + } + NSMutableArray *allRowContexts = _nodeContexts[ASDataControllerRowNodeKind]; + [allRowContexts insertObjects:sectionArray atIndexes:sections]; + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(allRowContexts, [ASIndexedNodeContext indexPathsFromContexts:contexts], contexts); + } + + [self prepareForInsertSections:sections]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willInsertSections:sections]; + + LOG(@"Edit Transaction - insertSections: %@", sections); + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { + [sectionArray addObject:[NSMutableArray array]]; + } + + [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; + + [self _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; + }); +} + +- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - deleteSections: %@", sections); + if (!_initialReloadDataHasBeenCalled) { + return; + } + + [_nodeContexts[ASDataControllerRowNodeKind] removeObjectsAtIndexes:sections]; + + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + [self prepareForDeleteSections:sections]; + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willDeleteSections:sections]; + + // remove elements + LOG(@"Edit Transaction - deleteSections: %@", sections); + [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }); +} + +#pragma mark - Backing store manipulation optional hooks (Subclass API) + +- (void)prepareForReloadDataWithSectionCount:(NSInteger)newSectionCount +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willReloadDataWithSectionCount:(NSInteger)newSectionCount +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForInsertSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForDeleteSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willInsertSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willDeleteSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForInsertRowsAtIndexPaths:(NSArray *)indexPaths +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willInsertRowsAtIndexPaths:(NSArray *)indexPaths +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForDeleteRowsAtIndexPaths:(NSArray *)indexPaths +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willDeleteRowsAtIndexPaths:(NSArray *)indexPaths +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +#pragma mark - Row Editing (External API) + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } + + LOG(@"Edit Command - insertRows: %@", indexPaths); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + // Sort indexPath to avoid messing up the index when inserting in several batches + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + + __weak id environment = [self.environmentDelegate dataControllerEnvironment]; + + for (NSIndexPath *indexPath in sortedIndexPaths) { + ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; + [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock + indexPath:indexPath + supplementaryElementKind:nil + constrainedSize:constrainedSize + environment:environment]]; + } + + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_nodeContexts[ASDataControllerRowNodeKind], sortedIndexPaths, contexts); + [self prepareForInsertRowsAtIndexPaths:indexPaths]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willInsertRowsAtIndexPaths:indexPaths]; + + LOG(@"Edit Transaction - insertRows: %@", indexPaths); + [self _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; + }); +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + + if (!_initialReloadDataHasBeenCalled) { + return; + } + + LOG(@"Edit Command - deleteRows: %@", indexPaths); + + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + // Sort indexPath in order to avoid messing up the index when deleting in several batches. + // FIXME: Shouldn't deletes be sorted in descending order? + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_nodeContexts[ASDataControllerRowNodeKind], sortedIndexPaths); + [self prepareForDeleteRowsAtIndexPaths:sortedIndexPaths]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willDeleteRowsAtIndexPaths:sortedIndexPaths]; + + LOG(@"Edit Transaction - deleteRows: %@", indexPaths); + [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; + }); +} + +- (void)relayoutAllNodes +{ + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } + + LOG(@"Edit Command - relayoutRows"); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + // Can't relayout right away because _completedNodes may not be up-to-date, + // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes + // (see _layoutNodes:atIndexPaths:withAnimationOptions:). + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [_mainSerialQueue performBlockOnMainThread:^{ + for (NSString *kind in _completedNodes) { + [self _relayoutNodesOfKind:kind]; + } + }]; + }); +} + +- (void)_relayoutNodesOfKind:(NSString *)kind +{ + ASDisplayNodeAssertMainThread(); + NSArray *nodes = [self completedNodesOfKind:kind]; + if (!nodes.count) { + return; + } + + NSUInteger sectionIndex = 0; + for (NSMutableArray *section in nodes) { + NSUInteger rowIndex = 0; + for (ASCellNode *node in section) { + RETURN_IF_NO_DATASOURCE(); + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + if (ASSizeRangeHasSignificantArea(constrainedSize)) { + [self _layoutNode:node withConstrainedSize:constrainedSize]; + } + rowIndex += 1; + } + sectionIndex += 1; + } +} + +#pragma mark - Data Querying (Subclass API) + +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] ? : [NSMutableArray array]; +} + +- (NSMutableArray *)completedNodesOfKind:(NSString *)kind +{ + return _completedNodes[kind]; +} + +#pragma mark - Data Querying (External API) + +- (NSUInteger)numberOfSections +{ + ASDisplayNodeAssertMainThread(); + return [_nodeContexts[ASDataControllerRowNodeKind] count]; +} + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)section +{ + ASDisplayNodeAssertMainThread(); + NSArray *contextSections = _nodeContexts[ASDataControllerRowNodeKind]; + return (section < contextSections.count) ? [contextSections[section] count] : 0; +} + +- (NSUInteger)completedNumberOfSections +{ + ASDisplayNodeAssertMainThread(); + return [[self completedNodes] count]; +} + +- (NSUInteger)completedNumberOfRowsInSection:(NSUInteger)section +{ + ASDisplayNodeAssertMainThread(); + NSArray *completedNodes = [self completedNodes]; + return (section < completedNodes.count) ? [completedNodes[section] count] : 0; +} + +- (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + if (indexPath == nil) { + return nil; + } + + NSArray *contexts = _nodeContexts[ASDataControllerRowNodeKind]; + NSInteger section = indexPath.section; + NSInteger row = indexPath.row; + ASIndexedNodeContext *context = nil; + + if (section >= 0 && row >= 0 && section < contexts.count) { + NSArray *completedNodesSection = contexts[section]; + if (row < completedNodesSection.count) { + context = completedNodesSection[row]; + } + } + return context.node; +} + +- (ASCellNode *)nodeAtCompletedIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + if (indexPath == nil) { + return nil; + } + + NSArray *completedNodes = [self completedNodes]; + NSInteger section = indexPath.section; + NSInteger row = indexPath.row; + ASCellNode *node = nil; + + if (section >= 0 && row >= 0 && section < completedNodes.count) { + NSArray *completedNodesSection = completedNodes[section]; + if (row < completedNodesSection.count) { + node = completedNodesSection[row]; + } + } + + return node; +} + +- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode; +{ + ASDisplayNodeAssertMainThread(); + if (cellNode == nil) { + return nil; + } + + NSString *kind = cellNode.supplementaryElementKind ?: ASDataControllerRowNodeKind; + NSArray *contexts = _nodeContexts[kind]; + + // Check if the cached index path is still correct. + NSIndexPath *indexPath = cellNode.cachedIndexPath; + if (indexPath != nil) { + ASIndexedNodeContext *context = ASGetElementInTwoDimensionalArray(contexts, indexPath); + if (context.nodeIfAllocated == cellNode) { + return indexPath; + } else { + indexPath = nil; + } + } + + // Loop through each section to look for the node context + NSInteger section = 0; + for (NSArray *nodeContexts in contexts) { + NSUInteger item = [nodeContexts indexOfObjectPassingTest:^BOOL(ASIndexedNodeContext * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + return obj.nodeIfAllocated == cellNode; + }]; + if (item != NSNotFound) { + indexPath = [NSIndexPath indexPathForItem:item inSection:section]; + break; + } + section += 1; + } + cellNode.cachedIndexPath = indexPath; + return indexPath; +} + +- (NSIndexPath *)completedIndexPathForNode:(ASCellNode *)cellNode +{ + ASDisplayNodeAssertMainThread(); + if (cellNode == nil) { + return nil; + } + + NSInteger section = 0; + // Loop through each section to look for the cellNode + NSString *kind = cellNode.supplementaryElementKind ?: ASDataControllerRowNodeKind; + for (NSArray *sectionNodes in [self completedNodesOfKind:kind]) { + NSUInteger item = [sectionNodes indexOfObjectIdenticalTo:cellNode]; + if (item != NSNotFound) { + return [NSIndexPath indexPathForItem:item inSection:section]; + } + section += 1; + } + + return nil; +} + +/// Returns nodes that can be queried externally. _externalCompletedNodes is used if available, _completedNodes otherwise. +- (NSArray *)completedNodes +{ + ASDisplayNodeAssertMainThread(); + return _externalCompletedNodes ? : _completedNodes[ASDataControllerRowNodeKind]; +} + +- (void)moveCompletedNodeAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +{ + ASDisplayNodeAssertMainThread(); + ASMoveElementInTwoDimensionalArray(_externalCompletedNodes, indexPath, newIndexPath); + ASMoveElementInTwoDimensionalArray(_completedNodes[ASDataControllerRowNodeKind], indexPath, newIndexPath); +} + +@end + +#if AS_MEASURE_AVOIDED_DATACONTROLLER_WORK + +static volatile int64_t _totalExpectedItems = 0; +static volatile int64_t _totalMeasuredNodes = 0; + +@implementation ASDataController (WorkMeasuring) + ++ (void)_didLayoutNode +{ + int64_t measured = OSAtomicIncrement64(&_totalMeasuredNodes); + int64_t expected = _totalExpectedItems; + if (measured % 20 == 0 || measured == expected) { + NSLog(@"Data controller avoided work (underestimated): %lld / %lld", measured, expected); + } +} + ++ (void)_expectToInsertNodes:(NSUInteger)count +{ + OSAtomicAdd64((int64_t)count, &_totalExpectedItems); +} + +@end +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASFlowLayoutController.h b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASFlowLayoutController.h new file mode 100644 index 0000000000..7eeb4d3533 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASFlowLayoutController.h @@ -0,0 +1,44 @@ +// +// ASFlowLayoutController.h +// AsyncDisplayKit +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +#ifndef MINIMAL_ASDK +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCellNode; + +typedef NS_ENUM(NSUInteger, ASFlowLayoutDirection) { + ASFlowLayoutDirectionVertical, + ASFlowLayoutDirectionHorizontal, +}; + +@protocol ASFlowLayoutControllerDataSource + +- (NSArray *> *)completedNodes; // This provides access to ASDataController's _completedNodes multidimensional array. + +@end + +/** + * An optimized flow layout controller that supports only vertical or horizontal scrolling, not simultaneously two-dimensional scrolling. + * It is used for all ASTableViews, and may be used with ASCollectionView. + */ +@interface ASFlowLayoutController : ASAbstractLayoutController + +@property (nonatomic, readonly, assign) ASFlowLayoutDirection layoutDirection; +@property (nonatomic, readwrite, weak) id dataSource; + +- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASFlowLayoutController.mm new file mode 100644 index 0000000000..4a23ec25a6 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -0,0 +1,203 @@ +// +// ASFlowLayoutController.mm +// AsyncDisplayKit +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +#ifndef MINIMAL_ASDK +#import +#import +#import +#import +#import + +#include +#include + +@interface ASFlowLayoutController() +{ + ASIndexPathRange _visibleRange; + std::vector _rangesByType; // All ASLayoutRangeTypes besides visible. +} + +@end + +@implementation ASFlowLayoutController + +- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection +{ + if (!(self = [super init])) { + return nil; + } + _layoutDirection = layoutDirection; + _rangesByType = std::vector(ASLayoutRangeTypeCount); + return self; +} + +#pragma mark - Visible Indices + +- (void)setVisibleNodeIndexPaths:(NSArray *)indexPaths +{ + _visibleRange = [self indexPathRangeForIndexPaths:indexPaths]; +} + +/** + * IndexPath array for the element in the working range. + */ + +- (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + CGSize viewportSize = [self viewportSize]; + + CGFloat viewportDirectionalSize = 0.0; + ASDirectionalScreenfulBuffer directionalBuffer = { 0, 0 }; + ASRangeTuningParameters tuningParameters = [self tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + + if (_layoutDirection == ASFlowLayoutDirectionHorizontal) { + viewportDirectionalSize = viewportSize.width; + directionalBuffer = ASDirectionalScreenfulBufferHorizontal(scrollDirection, tuningParameters); + } else { + viewportDirectionalSize = viewportSize.height; + directionalBuffer = ASDirectionalScreenfulBufferVertical(scrollDirection, tuningParameters); + } + + ASIndexPath startPath = [self findIndexPathAtDistance:(-directionalBuffer.negativeDirection * viewportDirectionalSize) + fromIndexPath:_visibleRange.start]; + + ASIndexPath endPath = [self findIndexPathAtDistance:(directionalBuffer.positiveDirection * viewportDirectionalSize) + fromIndexPath:_visibleRange.end]; + + ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never begin at a further position than endPath"); + + NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; + + NSArray *completedNodes = [_dataSource completedNodes]; + + ASIndexPath currPath = startPath; + + while (!ASIndexPathEqualToIndexPath(currPath, endPath)) { + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:currPath]]; + currPath.row++; + + // Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. + while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < endPath.section) { + currPath.row = 0; + currPath.section++; + } + } + ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath"); + + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]]; + + return indexPathSet; +} + +#pragma mark - Utility + +- (ASIndexPathRange)indexPathRangeForIndexPaths:(NSArray *)indexPaths +{ + // Set up an initial value so the MIN and MAX can work in the enumeration. + __block ASIndexPath currentIndexPath = [[indexPaths firstObject] ASIndexPathValue]; + __block ASIndexPathRange range; + range.start = currentIndexPath; + range.end = currentIndexPath; + + for (NSIndexPath *indexPath in indexPaths) { + currentIndexPath = [indexPath ASIndexPathValue]; + range.start = ASIndexPathMinimum(range.start, currentIndexPath); + range.end = ASIndexPathMaximum(range.end, currentIndexPath); + } + return range; +} + +- (ASIndexPath)findIndexPathAtDistance:(CGFloat)distance fromIndexPath:(ASIndexPath)start +{ + // "end" is the index path we'll advance until we have gone far enough from "start" to reach "distance" + ASIndexPath end = start; + // "previous" will store one iteration before "end", in case we go too far and need to reset "end" to be "previous" + ASIndexPath previous = start; + + NSArray *completedNodes = [_dataSource completedNodes]; + NSUInteger numberOfSections = [completedNodes count]; + NSUInteger numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + + // If "distance" is negative, advance "end" backwards across rows and sections. + // Otherwise, advance forward. In either case, bring "distance" closer to zero by the dimension of each row passed. + if (distance < 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) { + while (distance < 0.0 && end.section >= 0 && end.row >= 0) { + previous = end; + ASDisplayNode *node = completedNodes[end.section][end.row]; + CGSize size = node.calculatedSize; + distance += (_layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height); + end.row--; + // If we've gone to a negative row, set to the last row of the previous section. While loop is required to handle empty sections. + while (end.row < 0 && end.section > 0) { + end.section--; + numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + end.row = numberOfRowsInSection - 1; + } + } + + if (end.row < 0) { + end = previous; + } + } else { + while (distance > 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) { + previous = end; + ASDisplayNode *node = completedNodes[end.section][end.row]; + CGSize size = node.calculatedSize; + distance -= _layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; + + end.row++; + // If we've gone beyond the section, reset to the beginning of the next section. While loop is required to handle empty sections. + while (end.row >= numberOfRowsInSection && end.section < numberOfSections - 1) { + end.row = 0; + end.section++; + numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + } + } + + if (end.row >= numberOfRowsInSection) { + end = previous; + } + } + + return end; +} + +- (NSInteger)flowLayoutDistanceForRange:(ASIndexPathRange)range +{ + // This method should only be called with the range in proper order (start comes before end). + ASDisplayNodeAssert(ASIndexPathEqualToIndexPath(ASIndexPathMinimum(range.start, range.end), range.start), @"flowLayoutDistanceForRange: called with invalid range"); + + if (ASIndexPathEqualToIndexPath(range.start, range.end)) { + return 0; + } + + NSInteger totalRowCount = 0; + NSUInteger numberOfRowsInSection = 0; + NSArray *completedNodes = [_dataSource completedNodes]; + + for (NSInteger section = range.start.section; section <= range.end.section; section++) { + numberOfRowsInSection = [(NSArray *)completedNodes[section] count]; + totalRowCount += numberOfRowsInSection; + + if (section == range.start.section) { + // For the start section, make sure we don't count the rows before the start row. + totalRowCount -= range.start.row; + } else if (section == range.end.section) { + // For the start section, make sure we don't count the rows after the end row. + totalRowCount -= (numberOfRowsInSection - (range.end.row + 1)); + } + } + + ASDisplayNodeAssert(totalRowCount >= 0, @"totalRowCount in flowLayoutDistanceForRange: should not be negative"); + return totalRowCount; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit/Private/ASDataController+Subclasses.h b/submodules/AsyncDisplayKit/AsyncDisplayKit/Private/ASDataController+Subclasses.h new file mode 100644 index 0000000000..8061e25a8d --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit/Private/ASDataController+Subclasses.h @@ -0,0 +1,181 @@ +// +// ASDataController+Subclasses.h +// AsyncDisplayKit +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#ifndef MINIMAL_ASDK + +#pragma once +#import + +@class ASIndexedNodeContext; + +typedef void (^ASDataControllerCompletionBlock)(NSArray *nodes, NSArray *indexPaths); + +@interface ASDataController (Subclasses) + +#pragma mark - Internal editing & completed store querying + +/** + * Read-only access to the underlying editing nodes of the given kind + */ +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind; + +/** + * Read only access to the underlying completed nodes of the given kind + */ +- (NSMutableArray *)completedNodesOfKind:(NSString *)kind; + +#pragma mark - Node sizing + +/** + * Measure and layout the given nodes in optimized batches, constraining each to a given size in `constrainedSizeForNodeOfKind:atIndexPath:`. + * + * This method runs synchronously. + * @param batchCompletion A handler to be run after each batch is completed. It is executed synchronously on the calling thread. + */ +- (void)batchLayoutNodesFromContexts:(NSArray *)contexts batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler; + +/** + * Provides the size range for a specific node during the layout process. + */ +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +#pragma mark - Node & Section Insertion/Deletion API + +/** + * Inserts the given nodes of the specified kind into the backing store, calling completion on the main thread when the write finishes. + */ +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(ASDataControllerCompletionBlock)completionBlock; + +/** + * Deletes the given nodes of the specified kind in the backing store, calling completion on the main thread when the deletion finishes. + */ +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(ASDataControllerCompletionBlock)completionBlock; + +/** + * Inserts the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + */ +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock; + +/** + * Deletes the given sections in the backing store, calling completion on the main thread when finished. + */ +- (void)deleteSections:(NSIndexSet *)indexSet completion:(void (^)())completionBlock; + +#pragma mark - Data Manipulation Hooks + +/** + * Notifies the subclass to perform any work needed before the data controller is reloaded entirely + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + */ + - (void)prepareForReloadDataWithSectionCount:(NSInteger)newSectionCount; + +/** + * Notifies the subclass that the data controller is about to reload its data entirely + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform new node creation like supplementary views + * or header/footer nodes. + */ +- (void)willReloadDataWithSectionCount:(NSInteger)newSectionCount; + +/** + * Notifies the subclass to perform setup before sections are inserted in the data controller + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param sections Indices of sections to be inserted + */ +- (void)prepareForInsertSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will insert new sections at the given position + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be inserted + */ +- (void)willInsertSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass to perform setup before sections are deleted in the data controller + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param sections Indices of sections to be inserted + */ +- (void)prepareForDeleteSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will delete sections at the given positions + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be deleted + */ +- (void)willDeleteSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass to perform setup before rows are inserted in the data controller. + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param indexPaths Index paths for the rows to be inserted. + */ +- (void)prepareForInsertRowsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Notifies the subclass that the data controller will insert new rows at the given index paths. + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param indexPaths Index paths for the rows to be inserted. + */ +- (void)willInsertRowsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Notifies the subclass to perform setup before rows are deleted in the data controller. + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param indexPaths Index paths for the rows to be deleted. + */ +- (void)prepareForDeleteRowsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Notifies the subclass that the data controller will delete rows at the given index paths. + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param indexPaths Index paths for the rows to be deleted. + */ +- (void)willDeleteRowsAtIndexPaths:(NSArray *)indexPaths; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..77cacfa0b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,3594 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 47; + objects = { + +/* Begin PBXBuildFile section */ + 044284FD1BAA365100D16268 /* UICollectionViewLayout+ASConvenience.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E0E1B371875007741D0 /* UICollectionViewLayout+ASConvenience.mm */; }; + 044284FF1BAA3BD600D16268 /* UICollectionViewLayout+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */ = {isa = PBXBuildFile; fileRef = 044285051BAA63FE00D16268 /* ASBatchFetching.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 0442850A1BAA63FE00D16268 /* ASBatchFetching.mm in Sources */ = {isa = PBXBuildFile; fileRef = 044285061BAA63FE00D16268 /* ASBatchFetching.mm */; }; + 0442850E1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 0442850B1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 044285101BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0442850C1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.mm */; }; + 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.mm */; }; + 052EE06B1A15A0D8002C6279 /* TestResources in Resources */ = {isa = PBXBuildFile; fileRef = 052EE06A1A15A0D8002C6279 /* TestResources */; }; + 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.mm */; }; + 057D02C41AC0A66700C7AC3C /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 057D02C31AC0A66700C7AC3C /* main.mm */; }; + 057D02C71AC0A66700C7AC3C /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 057D02C61AC0A66700C7AC3C /* AppDelegate.mm */; }; + 058D09BE195D04C000B7D73C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09BD195D04C000B7D73C /* XCTest.framework */; }; + 058D09BF195D04C000B7D73C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AF195D04C000B7D73C /* Foundation.framework */; }; + 058D09C1195D04C000B7D73C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09C0195D04C000B7D73C /* UIKit.framework */; }; + 058D09CA195D04C000B7D73C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 058D09C8195D04C000B7D73C /* InfoPlist.strings */; }; + 058D0A38195D057000B7D73C /* ASDisplayLayerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.mm */; }; + 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A2E195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm */; }; + 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A2F195D057000B7D73C /* ASDisplayNodeTests.mm */; }; + 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.mm */; }; + 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm */; }; + 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.mm */; }; + 058D0A40195D057000B7D73C /* ASTextNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A36195D057000B7D73C /* ASTextNodeTests.mm */; }; + 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */; }; + 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */; }; + 0FAFDF7520EC1C90003A51C0 /* ASLayout+IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0FAFDF7620EC1C90003A51C0 /* ASLayout+IGListKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */; }; + 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */; }; + 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1A6C000E1FAB4E2100D05926 /* ASCornerLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */; }; + 1A6C00111FAB4EDD00D05926 /* ASCornerLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */; }; + 242995D31B29743C00090100 /* ASBasicImageDownloaderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.mm */; }; + 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.mm */; }; + 254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */; }; + 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */; }; + 254C6B731BF94DF4003EC431 /* ASTextKitCoreTextAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B741BF94DF4003EC431 /* ASTextNodeWordKerner.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754B91BEE458E00737CA5 /* ASTextNodeWordKerner.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B751BF94DF4003EC431 /* ASTextKitComponents.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754BA1BEE458E00737CA5 /* ASTextKitComponents.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 254C6B761BF94DF4003EC431 /* ASTextNodeTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754BC1BEE458E00737CA5 /* ASTextNodeTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 254C6B771BF94DF4003EC431 /* ASTextKitAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754951BEE44CD00737CA5 /* ASTextKitAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B781BF94DF4003EC431 /* ASTextKitContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754961BEE44CD00737CA5 /* ASTextKitContext.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754981BEE44CD00737CA5 /* ASTextKitEntityAttribute.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B7A1BF94DF4003EC431 /* ASTextKitRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754931BEE44CD00737CA5 /* ASTextKitRenderer.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B7B1BF94DF4003EC431 /* ASTextKitRenderer+Positioning.h in Headers */ = {isa = PBXBuildFile; fileRef = 2577549B1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B7C1BF94DF4003EC431 /* ASTextKitRenderer+TextChecking.h in Headers */ = {isa = PBXBuildFile; fileRef = 2577549D1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B7D1BF94DF4003EC431 /* ASTextKitShadower.h in Headers */ = {isa = PBXBuildFile; fileRef = 2577549F1BEE44CD00737CA5 /* ASTextKitShadower.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B7E1BF94DF4003EC431 /* ASTextKitTailTruncater.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754A11BEE44CD00737CA5 /* ASTextKitTailTruncater.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B7F1BF94DF4003EC431 /* ASTextKitTruncating.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754A31BEE44CD00737CA5 /* ASTextKitTruncating.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 254C6B821BF94F8A003EC431 /* ASTextKitComponents.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754B71BEE458D00737CA5 /* ASTextKitComponents.mm */; }; + 254C6B831BF94F8A003EC431 /* ASTextKitCoreTextAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.mm */; }; + 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754BD1BEE458E00737CA5 /* ASTextNodeWordKerner.mm */; }; + 254C6B851BF94F8A003EC431 /* ASTextKitAttributes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754941BEE44CD00737CA5 /* ASTextKitAttributes.mm */; }; + 254C6B861BF94F8A003EC431 /* ASTextKitContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754971BEE44CD00737CA5 /* ASTextKitContext.mm */; }; + 254C6B871BF94F8A003EC431 /* ASTextKitEntityAttribute.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754991BEE44CD00737CA5 /* ASTextKitEntityAttribute.mm */; }; + 254C6B881BF94F8A003EC431 /* ASTextKitRenderer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2577549A1BEE44CD00737CA5 /* ASTextKitRenderer.mm */; }; + 254C6B891BF94F8A003EC431 /* ASTextKitRenderer+Positioning.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2577549C1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.mm */; }; + 254C6B8A1BF94F8A003EC431 /* ASTextKitRenderer+TextChecking.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2577549E1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.mm */; }; + 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754A01BEE44CD00737CA5 /* ASTextKitShadower.mm */; }; + 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754A21BEE44CD00737CA5 /* ASTextKitTailTruncater.mm */; }; + 25E327571C16819500A2170C /* ASPagerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E327541C16819500A2170C /* ASPagerNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25E327591C16819500A2170C /* ASPagerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 25E327551C16819500A2170C /* ASPagerNode.mm */; }; + 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2911485C1A77147A005D0878 /* ASControlNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2911485B1A77147A005D0878 /* ASControlNodeTests.mm */; }; + 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.mm */; }; + 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm */; }; + 2C107F5B1BA9F54500F13DE5 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34566CB31BC1213700715E6B /* ASPhotosFrameworkImageRequest.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.mm */; }; + 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED071B17843500DA7C62 /* ASDimension.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED081B17843500DA7C62 /* ASDimension.mm */; }; + 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 34EFC75E1B701BF000AD841F /* ASInternalHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.mm */; }; + 34EFC75F1B701C8600AD841F /* ASInsetLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */; }; + 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */; }; + 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */; }; + 34EFC7671B701CD900AD841F /* ASLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED0B1B17843500DA7C62 /* ASLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */; }; + 34EFC7691B701CE100AD841F /* ASLayoutElement.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED111B17843500DA7C62 /* ASLayoutElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76A1B701CE600AD841F /* ASLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED0D1B17843500DA7C62 /* ASLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED0E1B17843500DA7C62 /* ASLayoutSpec.mm */; }; + 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED121B17843500DA7C62 /* ASOverlayLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76D1B701CF100AD841F /* ASOverlayLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED131B17843500DA7C62 /* ASOverlayLayoutSpec.mm */; }; + 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED141B17843500DA7C62 /* ASRatioLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED151B17843500DA7C62 /* ASRatioLayoutSpec.mm */; }; + 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED161B17843500DA7C62 /* ASStackLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED171B17843500DA7C62 /* ASStackLayoutSpec.mm */; }; + 34EFC7731B701D0700AD841F /* ASAbsoluteLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED181B17843500DA7C62 /* ASAbsoluteLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7741B701D0A00AD841F /* ASAbsoluteLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED191B17843500DA7C62 /* ASAbsoluteLayoutSpec.mm */; }; + 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.mm */; }; + 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */; }; + 4E9127691F64157600499623 /* ASRunLoopQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */; }; + 509E68601B3AED8E009B9150 /* ASScrollDirection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E111B371BD7007741D0 /* ASScrollDirection.mm */; }; + 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */; }; + 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E181B37339C007741D0 /* ASAbstractLayoutController.mm */; }; + 509E68631B3AEDB4009B9150 /* ASCollectionViewLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */; }; + 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */; }; + 509E68651B3AEDC5009B9150 /* CoreGraphics+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */; }; + 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.mm */; }; + 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; + 68355B3E1CB57A60001D4E68 /* ASPINRemoteImageDownloader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.mm */; }; + 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.mm */; }; + 68355B411CB57A6C001D4E68 /* ASImageContainerProtocolCategories.h in Headers */ = {isa = PBXBuildFile; fileRef = 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 683F563720E409D700CEB7A3 /* ASDisplayNode+InterfaceState.h in Headers */ = {isa = PBXBuildFile; fileRef = 683F563620E409D600CEB7A3 /* ASDisplayNode+InterfaceState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68AF37DB1CBEF4D80077BF76 /* ASImageNode+AnimatedImagePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 68B0277B1C1A79D60041016B /* ASDisplayNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68B8A4E21CBDB958007E4543 /* ASWeakProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 68B8A4DF1CBDB958007E4543 /* ASWeakProxy.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 68B8A4E41CBDB958007E4543 /* ASWeakProxy.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68B8A4E01CBDB958007E4543 /* ASWeakProxy.mm */; }; + 68C215581DE10D330019C4BC /* ASCollectionViewLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 68C215561DE10D330019C4BC /* ASCollectionViewLayoutInspector.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68C2155A1DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68C215571DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm */; }; + 68EE0DBE1C1B4ED300BA1B99 /* ASMainSerialQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 68EE0DBB1C1B4ED300BA1B99 /* ASMainSerialQueue.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 68EE0DC01C1B4ED300BA1B99 /* ASMainSerialQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68EE0DBC1C1B4ED300BA1B99 /* ASMainSerialQueue.mm */; }; + 68FC85E31CE29B7E00EDD713 /* ASTabBarController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85E01CE29B7E00EDD713 /* ASTabBarController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68FC85E51CE29B7E00EDD713 /* ASTabBarController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85E11CE29B7E00EDD713 /* ASTabBarController.mm */; }; + 68FC85E61CE29B9400EDD713 /* ASNavigationController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85DD1CE29AB700EDD713 /* ASNavigationController.mm */; }; + 68FC85EA1CE29C7D00EDD713 /* ASVisibilityProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85E71CE29C7D00EDD713 /* ASVisibilityProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68FC85EC1CE29C7D00EDD713 /* ASVisibilityProtocols.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.mm */; }; + 6900C5F41E8072DA00BCD75C /* ASImageNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */; }; + 6907C2581DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 6907C2561DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.mm */; }; + 690BC8C120F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 690BC8BF20F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 690BC8C220F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690BC8C020F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.mm */; }; + 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */; }; + 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 690C35631E055C7B00069B91 /* ASDimensionInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 690ED58E1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 690ED5981E36D118000627C0 /* ASControlNode+tvOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690ED5931E36D118000627C0 /* ASControlNode+tvOS.mm */; }; + 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690ED5951E36D118000627C0 /* ASImageNode+tvOS.mm */; }; + 692510141E74FB44003F2DD0 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 692510131E74FB44003F2DD0 /* Default-568h@2x.png */; }; + 692BE8D71E36B65B00C86D87 /* ASLayoutSpecPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 692BE8D61E36B65B00C86D87 /* ASLayoutSpecPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6947B0BE1E36B4E30007C478 /* ASStackUnpositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 6947B0BC1E36B4E30007C478 /* ASStackUnpositionedLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6947B0C01E36B4E30007C478 /* ASStackUnpositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6947B0BD1E36B4E30007C478 /* ASStackUnpositionedLayout.mm */; }; + 6947B0C31E36B5040007C478 /* ASStackPositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 6947B0C11E36B5040007C478 /* ASStackPositionedLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6947B0C51E36B5040007C478 /* ASStackPositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6947B0C21E36B5040007C478 /* ASStackPositionedLayout.mm */; }; + 695943401D70815300B0EE1F /* ASDisplayNodeLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 695BE2551DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */; }; + 696F01EC1DD2AF450049FBD5 /* ASEventLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */ = {isa = PBXBuildFile; fileRef = 696F01EB1DD2AF450049FBD5 /* ASEventLog.mm */; }; + 696FCB311D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */; }; + 6977965F1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = 6977965D1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 697796611D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6977965E1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm */; }; + 697B315A1CFE4B410049936F /* ASEditableTextNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 697B31591CFE4B410049936F /* ASEditableTextNodeTests.mm */; }; + 698371DB1E4379CD00437585 /* ASNodeController+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = 698371D91E4379CD00437585 /* ASNodeController+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 698371DC1E4379CD00437585 /* ASNodeController+Beta.mm in Sources */ = {isa = PBXBuildFile; fileRef = 698371DA1E4379CD00437585 /* ASNodeController+Beta.mm */; }; + 698C8B621CAB49FC0052DC3F /* ASLayoutElementExtensibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 698DFF431E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 698DFF461E36B7E9002891F1 /* ASLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69B225661D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm */; }; + 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */; }; + 69CB62AC1CB8165900024920 /* _ASDisplayViewAccessiblity.h in Headers */ = {isa = PBXBuildFile; fileRef = 69CB62A91CB8165900024920 /* _ASDisplayViewAccessiblity.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 69CB62AE1CB8165900024920 /* _ASDisplayViewAccessiblity.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69CB62AA1CB8165900024920 /* _ASDisplayViewAccessiblity.mm */; }; + 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69F10C871C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = 69F10C851C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm */; }; + 7630FFA81C9E267E007A7C0E /* ASVideoNode.h in Headers */ = {isa = PBXBuildFile; fileRef = AEEC47DF1C20C2DD00EC1693 /* ASVideoNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 764D83D51C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h in Headers */ = {isa = PBXBuildFile; fileRef = 764D83D21C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.mm in Sources */ = {isa = PBXBuildFile; fileRef = 764D83D31C8EA515009B4FB8 /* AsyncDisplayKit+Debug.mm */; }; + 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A06A7381C35F08800FE8DAA /* ASRelativeLayoutSpec.mm */; }; + 7AB338671C55B3460055FDE8 /* ASRelativeLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */; }; + 8021EC1D1D2B00B100799119 /* UIImage+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */; }; + 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.mm */; }; + 83A7D95B1D44547700BF333E /* ASWeakMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = 83A7D9591D44542100BF333E /* ASWeakMap.mm */; }; + 83A7D95C1D44548100BF333E /* ASWeakMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A7D9581D44542100BF333E /* ASWeakMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 83A7D95E1D446A6E00BF333E /* ASWeakMapTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.mm */; }; + 8BBBAB8C1CEBAF1700107FC6 /* ASDefaultPlaybackButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.mm */; }; + 8BDA5FC71CDBDF91007D13B2 /* ASVideoPlayerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */; }; + 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */; }; + 9019FBC01ED8061D00C45F72 /* ASYogaUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */; }; + 909C4C751F09C98B00D6B76F /* ASTextNode2.h in Headers */ = {isa = PBXBuildFile; fileRef = 909C4C731F09C98B00D6B76F /* ASTextNode2.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 909C4C761F09C98B00D6B76F /* ASTextNode2.mm in Sources */ = {isa = PBXBuildFile; fileRef = 909C4C741F09C98B00D6B76F /* ASTextNode2.mm */; }; + 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */; }; + 92DD2FE61BF4D05E0074C9DD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */; }; + 92DD2FE81BF4D0A80074C9DD /* ASMapNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9644CFE02193777C00213478 /* ASThrashUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 9644CFDF2193777C00213478 /* ASThrashUtility.m */; }; + 9692B4FF219E12370060C2C3 /* ASCollectionViewThrashTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */; }; + 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */; }; + 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C6BB3B31B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C6BB3B01B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C70F2051CDA4F06007D6C76 /* ASTraitCollection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C70F2021CDA4EFA007D6C76 /* ASTraitCollection.mm */; }; + 9C70F2061CDA4F0C007D6C76 /* ASTraitCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C70F2011CDA4EFA007D6C76 /* ASTraitCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C70F2091CDABA36007D6C76 /* ASViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */; }; + 9C70F20A1CDBE949007D6C76 /* ASTableNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */; }; + 9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */ = {isa = PBXBuildFile; fileRef = AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */ = {isa = PBXBuildFile; fileRef = DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C70F20F1CDBE9FF007D6C76 /* ASLayoutManager.h in Headers */ = {isa = PBXBuildFile; fileRef = B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */; }; + 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */ = {isa = PBXBuildFile; fileRef = A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9CC606651D24DF9E006581A0 /* NSIndexSet+ASHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.mm */; }; + 9CDC18CD1B910E12004965E2 /* ASLayoutElementPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutElementPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9D302F9B2231B07E005739C3 /* ASButtonNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D302F9A2231B07E005739C3 /* ASButtonNode+Private.h */; }; + 9D302F9E2231B373005739C3 /* ASButtonNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */; }; + 9D302F9F2231B373005739C3 /* ASButtonNode+Yoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */; }; + 9D9AA56921E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AA56721E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm */; }; + 9D9AA56B21E254B800172C09 /* ASDisplayNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */; }; + 9D9AA56D21E2568500172C09 /* ASDisplayNode+LayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56C21E2568500172C09 /* ASDisplayNode+LayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */; }; + 9F98C0261DBE29E000476D92 /* ASControlTargetAction.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F98C0241DBDF2A300476D92 /* ASControlTargetAction.mm */; }; + 9F98C0271DBE29FC00476D92 /* ASControlTargetAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 9F98C0231DBDF2A300476D92 /* ASControlTargetAction.h */; settings = {ATTRIBUTES = (Private, ); }; }; + A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC026B581BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.mm */; }; + AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; settings = {ATTRIBUTES = (Private, ); }; }; + AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */; }; + AC47D9421B3B891B00AAEE9D /* ASCellNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC6456071B0A335000CF11B8 /* ASCellNode.mm */; }; + AC6145411D8AFAE8003D62A2 /* ASSection.h in Headers */ = {isa = PBXBuildFile; fileRef = AC6145401D8AFAE8003D62A2 /* ASSection.h */; settings = {ATTRIBUTES = (Private, ); }; }; + AC6145441D8AFD4F003D62A2 /* ASSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC6145421D8AFD4F003D62A2 /* ASSection.mm */; }; + AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + ACE87A2C1D73696800D7FF06 /* ASSectionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = ACE87A2B1D73696800D7FF06 /* ASSectionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + ACF6ED5C1B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED531B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm */; }; + ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED541B178DC700DA7C62 /* ASDimensionTests.mm */; }; + ACF6ED5E1B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */; }; + ACF6ED601B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.mm */; }; + ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */; }; + ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */; }; + ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */; }; + AE440175210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = AE440174210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm */; }; + AE6987C11DD04E1000B9E458 /* ASPagerNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.mm */; }; + AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E31C21D3D200EC1693 /* ASVideoNodeTests.mm */; }; + B13CA0F81C519EBA00E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B13CA1011C52004900E031AB /* ASCollectionNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B30BF6541C59D889004FCD53 /* ASLayoutManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = B30BF6511C5964B0004FCD53 /* ASLayoutManager.mm */; }; + B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A501A1139C100143C57 /* ASCollectionView.mm */; }; + B35061F71B010EFD0018CF92 /* ASCollectionViewProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09D5195D050800B7D73C /* ASControlNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09D6195D050800B7D73C /* ASControlNode.mm */; }; + B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09D7195D050800B7D73C /* ASControlNode+Subclasses.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061FB1B010EFD0018CF92 /* ASDisplayNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09D8195D050800B7D73C /* ASDisplayNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061FC1B010EFD0018CF92 /* ASDisplayNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09D9195D050800B7D73C /* ASDisplayNode.mm */; }; + B35061FD1B010EFD0018CF92 /* ASDisplayNode+Subclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061FE1B010EFD0018CF92 /* ASDisplayNodeExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35061FF1B010EFD0018CF92 /* ASDisplayNodeExtras.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09DC195D050800B7D73C /* ASDisplayNodeExtras.mm */; }; + B35062001B010EFD0018CF92 /* ASEditableTextNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 0587F9BB1A7309ED00AFF0BA /* ASEditableTextNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062011B010EFD0018CF92 /* ASEditableTextNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0587F9BC1A7309ED00AFF0BA /* ASEditableTextNode.mm */; }; + B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09DD195D050800B7D73C /* ASImageNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062031B010EFD0018CF92 /* ASImageNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09DE195D050800B7D73C /* ASImageNode.mm */; }; + B35062041B010EFD0018CF92 /* ASMultiplexImageNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062051B010EFD0018CF92 /* ASMultiplexImageNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */; }; + B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062071B010EFD0018CF92 /* ASNetworkImageNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */; }; + B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.mm */; }; + B350620A1B010EFD0018CF92 /* ASTableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 055F1A3319ABD3E3004DAFF1 /* ASTableView.mm */; }; + B350620C1B010EFD0018CF92 /* ASTableViewProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350620D1B010EFD0018CF92 /* ASTextNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09DF195D050800B7D73C /* ASTextNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09E0195D050800B7D73C /* ASTextNode.mm */; }; + B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062101B010EFD0018CF92 /* _ASDisplayLayer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */; }; + B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09E4195D050800B7D73C /* _ASDisplayView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062121B010EFD0018CF92 /* _ASDisplayView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09E5195D050800B7D73C /* _ASDisplayView.mm */; }; + B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */; }; + B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.mm */; }; + B35062171B010EFD0018CF92 /* ASDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521A1A3F83C40061C0BA /* ASDataController.mm */; }; + B350621B1B010EFD0018CF92 /* ASTableLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B350621C1B010EFD0018CF92 /* ASTableLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521C1A3F83C40061C0BA /* ASTableLayoutController.mm */; }; + B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350621E1B010EFD0018CF92 /* ASHighlightOverlayLayer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09E7195D050800B7D73C /* ASHighlightOverlayLayer.mm */; }; + B350621F1B010EFD0018CF92 /* ASImageProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062201B010EFD0018CF92 /* ASLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521D1A3F83C40061C0BA /* ASLayoutController.h */; }; + B35062211B010EFD0018CF92 /* ASLayoutRangeType.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062241B010EFD0018CF92 /* ASMutableAttributedStringBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09E8195D050800B7D73C /* ASMutableAttributedStringBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062251B010EFD0018CF92 /* ASMutableAttributedStringBuilder.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09E9195D050800B7D73C /* ASMutableAttributedStringBuilder.mm */; }; + B35062261B010EFD0018CF92 /* ASRangeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3619ABD413004DAFF1 /* ASRangeController.h */; }; + B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 055F1A3719ABD413004DAFF1 /* ASRangeController.mm */; }; + B350622D1B010EFD0018CF92 /* ASScrollDirection.h in Headers */ = {isa = PBXBuildFile; fileRef = 296A0A311A951715005ACEAA /* ASScrollDirection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062391B010EFD0018CF92 /* ASThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A12195D050800B7D73C /* ASThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350623A1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09F5195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09F6195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.mm */; }; + B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09F8195D050800B7D73C /* _ASAsyncTransaction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09F9195D050800B7D73C /* _ASAsyncTransaction.mm */; }; + B350623E1B010EFD0018CF92 /* _ASAsyncTransactionContainer+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09FA195D050800B7D73C /* _ASAsyncTransactionContainer+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B350623F1B010EFD0018CF92 /* _ASAsyncTransactionContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09FB195D050800B7D73C /* _ASAsyncTransactionContainer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09FC195D050800B7D73C /* _ASAsyncTransactionContainer.mm */; }; + B35062411B010EFD0018CF92 /* _ASAsyncTransactionGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09FD195D050800B7D73C /* _ASAsyncTransactionGroup.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D09FE195D050800B7D73C /* _ASAsyncTransactionGroup.mm */; }; + B35062431B010EFD0018CF92 /* UIView+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D09FF195D050800B7D73C /* UIView+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B35062491B010EFD0018CF92 /* _ASCoreAnimationExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B350624A1B010EFD0018CF92 /* _ASCoreAnimationExtras.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */; }; + B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A05195D050800B7D73C /* _ASPendingState.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B350624C1B010EFD0018CF92 /* _ASPendingState.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A06195D050800B7D73C /* _ASPendingState.mm */; }; + B350624D1B010EFD0018CF92 /* _ASScopeTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A07195D050800B7D73C /* _ASScopeTimer.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A08195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm */; }; + B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */; }; + B35062511B010EFD0018CF92 /* ASDisplayNode+UIViewBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */; }; + B35062521B010EFD0018CF92 /* ASDisplayNodeInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B35062541B010EFD0018CF92 /* ASImageNode+CGExtras.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.mm */; }; + B35062571B010F070018CF92 /* ASAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A43195D058D00B7D73C /* ASAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062581B010F070018CF92 /* ASAvailability.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3A1A15563400B4EBED /* ASAvailability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A44195D058D00B7D73C /* ASBaseDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350625C1B010F070018CF92 /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */; }; + BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.mm */; }; + C018DF21216BF26700181FDA /* ASAbstractLayoutController+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */; }; + C057D9BD20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm */; }; + C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC034A0A1E60BEB400626263 /* ASDisplayNode+Convenience.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.mm */; }; + CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC034A141E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.mm */; }; + CC051F1F1D7A286A006434CB /* ASCALayerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC051F1E1D7A286A006434CB /* ASCALayerTests.mm */; }; + CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.mm */; }; + CC0F885B1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.mm */; }; + CC0F885C1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = CC0F885A1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC0F885F1E4280B800576FED /* _ASCollectionViewCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC0F885D1E4280B800576FED /* _ASCollectionViewCell.mm */; }; + CC0F88601E4280B800576FED /* _ASCollectionViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC0F88621E4281E200576FED /* ASSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC0F88631E4281E700576FED /* ASSupplementaryNodeSource.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC0F886C1E4286FA00576FED /* ReferenceImages_64 in Resources */ = {isa = PBXBuildFile; fileRef = CC0F88691E4286FA00576FED /* ReferenceImages_64 */; }; + CC0F886D1E4286FA00576FED /* ReferenceImages_iOS_10 in Resources */ = {isa = PBXBuildFile; fileRef = CC0F886A1E4286FA00576FED /* ReferenceImages_iOS_10 */; }; + CC11F97A1DB181180024D77B /* ASNetworkImageNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.mm */; }; + CC18248C200D49C800875940 /* ASTextNodeCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = CC18248B200D49C800875940 /* ASTextNodeCommon.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC224E962066CA6D00BBA57F /* configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = CC224E952066CA6D00BBA57F /* configuration.json */; }; + CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */; }; + CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */; }; + CC35CEC320DD7F600006448D /* ASCollections.h in Headers */ = {isa = PBXBuildFile; fileRef = CC35CEC120DD7F600006448D /* ASCollections.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC35CEC420DD7F600006448D /* ASCollections.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC35CEC220DD7F600006448D /* ASCollections.mm */; }; + CC35CEC620DD87280006448D /* ASCollectionsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC35CEC520DD87280006448D /* ASCollectionsTests.mm */; }; + CC36C18F218B841600232F23 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C18E218B841600232F23 /* UIKit.framework */; }; + CC36C191218B841A00232F23 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C190218B841A00232F23 /* CoreText.framework */; }; + CC36C193218B842E00232F23 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C192218B842E00232F23 /* CoreGraphics.framework */; }; + CC36C194218B844800232F23 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AF195D04C000B7D73C /* Foundation.framework */; }; + CC36C196218B845B00232F23 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C195218B845B00232F23 /* AVFoundation.framework */; }; + CC36C198218B846300232F23 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C197218B846300232F23 /* QuartzCore.framework */; }; + CC36C19A218B846F00232F23 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C199218B846F00232F23 /* CoreLocation.framework */; }; + CC36C19C218B847400232F23 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C19B218B847400232F23 /* CoreMedia.framework */; }; + CC36C19D218B849C00232F23 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C18E218B841600232F23 /* UIKit.framework */; }; + CC36C19E218B894400232F23 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C195218B845B00232F23 /* AVFoundation.framework */; }; + CC36C19F218B894800232F23 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC36C19B218B847400232F23 /* CoreMedia.framework */; }; + CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20811C3F76D600798563 /* ASPendingStateController.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20821C3F76D600798563 /* ASPendingStateController.mm */; }; + CC3B208A1C3F7A5400798563 /* ASWeakSet.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20871C3F7A5400798563 /* ASWeakSet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC3B208C1C3F7A5400798563 /* ASWeakSet.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20881C3F7A5400798563 /* ASWeakSet.mm */; }; + CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.mm */; }; + CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */; }; + CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.mm */; }; + CC4C2A771D88E3BF0039ACAB /* ASTraceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC4C2A791D88E3BF0039ACAB /* ASTraceEvent.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.mm */; }; + CC54A81C1D70079800296A24 /* ASDispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = CC54A81B1D70077A00296A24 /* ASDispatch.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC54A81E1D7008B300296A24 /* ASDispatchTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC54A81D1D7008B300296A24 /* ASDispatchTests.mm */; }; + CC55A70D1E529FA200594372 /* UIResponder+AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = CC55A70B1E529FA200594372 /* UIResponder+AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC55A70E1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm */; }; + CC55A7111E52A0F200594372 /* ASResponderChainEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */; }; + CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.mm */; }; + CC56013B1F06E9A700DC4FBE /* ASIntegerMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */; }; + CC56013C1F06E9A700DC4FBE /* ASIntegerMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */; }; + CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC57EAF81E3939450034C595 /* ASTableView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC583AD61EF9BDBE00134156 /* ASTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC583AC21EF9BAB400134156 /* ASTestCase.mm */; }; + CC583AD71EF9BDC100134156 /* NSInvocation+ASTestHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC583AC51EF9BAB400134156 /* NSInvocation+ASTestHelpers.mm */; }; + CC583AD81EF9BDC300134156 /* OCMockObject+ASAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC583AC71EF9BAB400134156 /* OCMockObject+ASAdditions.mm */; }; + CC583AD91EF9BDC600134156 /* ASDisplayNode+OCMock.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.mm */; }; + CC58AA4B1E398E1D002C8CB4 /* ASBlockTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */ = {isa = PBXBuildFile; fileRef = CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC6AA2DB1E9F03B900978E87 /* ASDisplayNode+Ancestry.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.mm */; }; + CC7AF196200D9BD500A21BDE /* ASExperimentalFeatures.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7AF195200D9BD500A21BDE /* ASExperimentalFeatures.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC7AF198200DAB2200A21BDE /* ASExperimentalFeatures.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC7AF197200D9E8400A21BDE /* ASExperimentalFeatures.mm */; }; + CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.mm */; }; + CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC84C7F220474C5300A3851B /* ASCGImageBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = CC84C7F020474C5300A3851B /* ASCGImageBuffer.h */; }; + CC84C7F320474C5300A3851B /* ASCGImageBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC84C7F120474C5300A3851B /* ASCGImageBuffer.mm */; }; + CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC8B05D61D73836400F54286 /* ASPerformanceTestContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC8B05D51D73836400F54286 /* ASPerformanceTestContext.mm */; }; + CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.mm */; }; + CC90E1F41E383C0400FED591 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */; }; + CCA221D31D6FA7EF00AF6A0F /* ASViewControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.mm */; }; + CCA282B41E9EA7310037E8B7 /* ASTipsController.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282B21E9EA7310037E8B7 /* ASTipsController.h */; }; + CCA282B51E9EA7310037E8B7 /* ASTipsController.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282B31E9EA7310037E8B7 /* ASTipsController.mm */; }; + CCA282B81E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282B61E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCA282B91E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282B71E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.mm */; }; + CCA282BC1E9EABDD0037E8B7 /* ASTipProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282BA1E9EABDD0037E8B7 /* ASTipProvider.h */; }; + CCA282BD1E9EABDD0037E8B7 /* ASTipProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282BB1E9EABDD0037E8B7 /* ASTipProvider.mm */; }; + CCA282C01E9EAE010037E8B7 /* ASTip.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282BE1E9EAE010037E8B7 /* ASTip.h */; }; + CCA282C11E9EAE010037E8B7 /* ASTip.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282BF1E9EAE010037E8B7 /* ASTip.mm */; }; + CCA282C41E9EAE630037E8B7 /* ASLayerBackingTipProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282C21E9EAE630037E8B7 /* ASLayerBackingTipProvider.h */; }; + CCA282C51E9EAE630037E8B7 /* ASLayerBackingTipProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282C31E9EAE630037E8B7 /* ASLayerBackingTipProvider.mm */; }; + CCA282C81E9EB64B0037E8B7 /* ASDisplayNodeTipState.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282C61E9EB64B0037E8B7 /* ASDisplayNodeTipState.h */; }; + CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282C71E9EB64B0037E8B7 /* ASDisplayNodeTipState.mm */; }; + CCA282CC1E9EB73E0037E8B7 /* ASTipNode.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282CA1E9EB73E0037E8B7 /* ASTipNode.h */; }; + CCA282CD1E9EB73E0037E8B7 /* ASTipNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CB1E9EB73E0037E8B7 /* ASTipNode.mm */; }; + CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */; }; + CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.mm */; }; + CCA5F62E1EECC2A80060C137 /* ASAssert.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62D1EECC2A80060C137 /* ASAssert.mm */; }; + CCAA0B7F206ADBF30057B336 /* ASRecursiveUnfairLock.h in Headers */ = {isa = PBXBuildFile; fileRef = CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.mm */; }; + CCAA0B82206ADECB0057B336 /* ASRecursiveUnfairLockTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCAA0B81206ADECB0057B336 /* ASRecursiveUnfairLockTests.mm */; }; + CCB1F95A1EFB60A5009C7475 /* ASLog.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCB1F9591EFB60A5009C7475 /* ASLog.mm */; }; + CCB1F95C1EFB6350009C7475 /* ASSignpost.h in Headers */ = {isa = PBXBuildFile; fileRef = CCB1F95B1EFB6316009C7475 /* ASSignpost.h */; }; + CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.mm */; }; + CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCBDDD0520C62A2D00CBA922 /* ASMainThreadDeallocation.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBDDD0320C62A2D00CBA922 /* ASMainThreadDeallocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCBDDD0620C62A2D00CBA922 /* ASMainThreadDeallocation.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCBDDD0420C62A2D00CBA922 /* ASMainThreadDeallocation.mm */; }; + CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */; }; + CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.mm */; }; + CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */; }; + CCCCCCD81EC3EF060087FE10 /* ASTextInput.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC61EC3EF060087FE10 /* ASTextInput.mm */; }; + CCCCCCD91EC3EF060087FE10 /* ASTextLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC71EC3EF060087FE10 /* ASTextLayout.h */; }; + CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC81EC3EF060087FE10 /* ASTextLayout.mm */; }; + CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC91EC3EF060087FE10 /* ASTextLine.h */; }; + CCCCCCDC1EC3EF060087FE10 /* ASTextLine.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCCA1EC3EF060087FE10 /* ASTextLine.mm */; }; + CCCCCCDD1EC3EF060087FE10 /* ASTextAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCCC1EC3EF060087FE10 /* ASTextAttribute.h */; }; + CCCCCCDE1EC3EF060087FE10 /* ASTextAttribute.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCCD1EC3EF060087FE10 /* ASTextAttribute.mm */; }; + CCCCCCDF1EC3EF060087FE10 /* ASTextRunDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCCE1EC3EF060087FE10 /* ASTextRunDelegate.h */; }; + CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCCF1EC3EF060087FE10 /* ASTextRunDelegate.mm */; }; + CCCCCCE11EC3EF060087FE10 /* ASTextUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCD11EC3EF060087FE10 /* ASTextUtilities.h */; }; + CCCCCCE21EC3EF060087FE10 /* ASTextUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCD21EC3EF060087FE10 /* ASTextUtilities.mm */; }; + CCCCCCE31EC3EF060087FE10 /* NSParagraphStyle+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCD31EC3EF060087FE10 /* NSParagraphStyle+ASText.h */; }; + CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.mm */; }; + CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */; }; + CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.mm */; }; + CCDC9B4D200991D10063C1F8 /* ASGraphicsContext.h in Headers */ = {isa = PBXBuildFile; fileRef = CCDC9B4B200991D10063C1F8 /* ASGraphicsContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCDC9B4E200991D10063C1F8 /* ASGraphicsContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCDC9B4C200991D10063C1F8 /* ASGraphicsContext.mm */; }; + CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.mm */; }; + CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.mm */; }; + CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */; }; + CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */; }; + CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */; }; + CCED5E3E2020D36800395C40 /* ASNetworkImageLoadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCED5E3F2020D36800395C40 /* ASNetworkImageLoadInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.mm */; }; + CCED5E412020D49D00395C40 /* ASNetworkImageLoadInfo+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = CCED5E402020D41600395C40 /* ASNetworkImageLoadInfo+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CCEDDDCA200C2AC300FFCD0A /* ASConfigurationInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = CCEDDDC8200C2AC300FFCD0A /* ASConfigurationInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEDDDCB200C2AC300FFCD0A /* ASConfigurationInternal.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDC9200C2AC300FFCD0A /* ASConfigurationInternal.mm */; }; + CCEDDDCD200C2CB900FFCD0A /* ASConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = CCEDDDCC200C2CB900FFCD0A /* ASConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEDDDCF200C42A200FFCD0A /* ASConfigurationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = CCEDDDCE200C42A200FFCD0A /* ASConfigurationDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEDDDD1200C488000FFCD0A /* ASConfiguration.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD0200C488000FFCD0A /* ASConfiguration.mm */; }; + CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.mm */; }; + CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CCF1FF5E20C4785000AAD8FC /* ASLocking.h in Headers */ = {isa = PBXBuildFile; fileRef = CCF1FF5D20C4785000AAD8FC /* ASLocking.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; + DB78412E1C6BCE1600A9E2B4 /* _ASTransitionContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.mm */; }; + DBABFAFC1C6A8D2F0039EA4A /* _ASTransitionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */; settings = {ATTRIBUTES = (Private, ); }; }; + DBC452DE1C5C6A6A00B16017 /* ArrayDiffingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.mm */; }; + DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.mm */; }; + DBDB83951C6E879900D0098C /* ASPagerFlowLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DBDB83971C6E879900D0098C /* ASPagerFlowLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBDB83931C6E879900D0098C /* ASPagerFlowLayout.mm */; }; + DE4843DC1C93EAC100A1F33B /* ASLayoutTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */; settings = {ATTRIBUTES = (Private, ); }; }; + DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DE84918E1C8FFF9F003D89E9 /* ASRunLoopQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */; }; + DE8BEAC21C2DF3FC00D57C12 /* ASDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */; settings = {ATTRIBUTES = (Private, ); }; }; + DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.mm */; }; + DEB8ED7C1DD003D300DBDE55 /* ASLayoutTransition.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */; }; + DEC146B71C37A16A004A0EE7 /* ASCollectionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; + DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; + E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.mm */; }; + E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; + E54E81FD1EB357BD00FFE8E1 /* ASPageTable.mm in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */; }; + E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; + E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm */; }; + E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */; }; + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */; }; + E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.mm */; }; + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.mm */; }; + E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.mm */; }; + E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */; }; + E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */; }; + E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5B078001E69F4EB00C24B5B /* ASElementMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.mm */; }; + E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5B225291F1790EE001E1431 /* ASHashing.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5B225261F1790B5001E1431 /* ASHashing.mm */; }; + E5B2252E1F17E521001E1431 /* ASDispatch.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252D1F17E521001E1431 /* ASDispatch.mm */; }; + E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; }; + E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; + E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */; }; + E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */; }; + F325E48C21745F9E00AC93A4 /* ASButtonNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F325E48B21745F9E00AC93A4 /* ASButtonNodeTests.mm */; }; + F325E490217460B100AC93A4 /* ASTextNode2Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F325E48F217460B000AC93A4 /* ASTextNode2Tests.mm */; }; + F3F698D2211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F3F698D1211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm */; }; + F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.mm */; }; + FA4FAF15200A850200E735BD /* ASControlNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = FA4FAF14200A850200E735BD /* ASControlNode+Private.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 057D02E51AC0A67000C7AC3C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 058D09A4195D04C000B7D73C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 057D02BE1AC0A66700C7AC3C; + remoteInfo = AsyncDisplayKitTestHost; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 044285011BAA3CC700D16268 /* AsyncDisplayKit.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = AsyncDisplayKit.modulemap; sourceTree = ""; }; + 044285051BAA63FE00D16268 /* ASBatchFetching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetching.h; sourceTree = ""; }; + 044285061BAA63FE00D16268 /* ASBatchFetching.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBatchFetching.mm; sourceTree = ""; }; + 0442850B1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTwoDimensionalArrayUtils.h; sourceTree = ""; }; + 0442850C1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTwoDimensionalArrayUtils.mm; sourceTree = ""; }; + 0516FA3A1A15563400B4EBED /* ASAvailability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAvailability.h; sourceTree = ""; }; + 0516FA3B1A15563400B4EBED /* ASLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLog.h; sourceTree = ""; }; + 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMultiplexImageNode.h; sourceTree = ""; }; + 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultiplexImageNode.mm; sourceTree = ""; }; + 051943121A1575630030A7D0 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 051943141A1575670030A7D0 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultiplexImageNodeTests.mm; sourceTree = ""; }; + 052EE06A1A15A0D8002C6279 /* TestResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TestResources; sourceTree = ""; }; + 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBasicImageDownloader.h; sourceTree = ""; }; + 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBasicImageDownloader.mm; sourceTree = ""; }; + 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNetworkImageNode.h; sourceTree = ""; }; + 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNetworkImageNode.mm; sourceTree = ""; }; + 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASTableView.h; sourceTree = ""; }; + 055F1A3319ABD3E3004DAFF1 /* ASTableView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTableView.mm; sourceTree = ""; }; + 055F1A3619ABD413004DAFF1 /* ASRangeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeController.h; sourceTree = ""; }; + 055F1A3719ABD413004DAFF1 /* ASRangeController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRangeController.mm; sourceTree = ""; }; + 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCellNode.h; sourceTree = ""; }; + 056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSnapshotTestCase.h; sourceTree = ""; }; + 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASImageNodeSnapshotTests.mm; sourceTree = ""; }; + 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTableViewProtocols.h; sourceTree = ""; }; + 057D02BF1AC0A66700C7AC3C /* AsyncDisplayKitTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AsyncDisplayKitTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 057D02C21AC0A66700C7AC3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 057D02C31AC0A66700C7AC3C /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + 057D02C51AC0A66700C7AC3C /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 057D02C61AC0A66700C7AC3C /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = AppDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 0587F9BB1A7309ED00AFF0BA /* ASEditableTextNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEditableTextNode.h; sourceTree = ""; }; + 0587F9BC1A7309ED00AFF0BA /* ASEditableTextNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASEditableTextNode.mm; sourceTree = ""; }; + 058D09AF195D04C000B7D73C /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 058D09BC195D04C000B7D73C /* AsyncDisplayKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AsyncDisplayKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 058D09BD195D04C000B7D73C /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 058D09C0195D04C000B7D73C /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + 058D09C7195D04C000B7D73C /* AsyncDisplayKitTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "AsyncDisplayKitTests-Info.plist"; sourceTree = ""; }; + 058D09C9195D04C000B7D73C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 058D09D5195D050800B7D73C /* ASControlNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASControlNode.h; sourceTree = ""; }; + 058D09D6195D050800B7D73C /* ASControlNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASControlNode.mm; sourceTree = ""; }; + 058D09D7195D050800B7D73C /* ASControlNode+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+Subclasses.h"; sourceTree = ""; }; + 058D09D8195D050800B7D73C /* ASDisplayNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDisplayNode.h; sourceTree = ""; }; + 058D09D9195D050800B7D73C /* ASDisplayNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDisplayNode.mm; sourceTree = ""; }; + 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = "ASDisplayNode+Subclasses.h"; sourceTree = ""; }; + 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeExtras.h; sourceTree = ""; }; + 058D09DC195D050800B7D73C /* ASDisplayNodeExtras.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeExtras.mm; sourceTree = ""; }; + 058D09DD195D050800B7D73C /* ASImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageNode.h; sourceTree = ""; }; + 058D09DE195D050800B7D73C /* ASImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASImageNode.mm; sourceTree = ""; }; + 058D09DF195D050800B7D73C /* ASTextNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextNode.h; sourceTree = ""; }; + 058D09E0195D050800B7D73C /* ASTextNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTextNode.mm; sourceTree = ""; }; + 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASDisplayLayer.h; sourceTree = ""; }; + 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = _ASDisplayLayer.mm; sourceTree = ""; }; + 058D09E4195D050800B7D73C /* _ASDisplayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASDisplayView.h; sourceTree = ""; }; + 058D09E5195D050800B7D73C /* _ASDisplayView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASDisplayView.mm; sourceTree = ""; }; + 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASHighlightOverlayLayer.h; sourceTree = ""; }; + 058D09E7195D050800B7D73C /* ASHighlightOverlayLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASHighlightOverlayLayer.mm; sourceTree = ""; }; + 058D09E8195D050800B7D73C /* ASMutableAttributedStringBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableAttributedStringBuilder.h; sourceTree = ""; }; + 058D09E9195D050800B7D73C /* ASMutableAttributedStringBuilder.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMutableAttributedStringBuilder.mm; sourceTree = ""; }; + 058D09F5195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableAttributedString+TextKitAdditions.h"; sourceTree = ""; }; + 058D09F6195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSMutableAttributedString+TextKitAdditions.mm"; sourceTree = ""; }; + 058D09F8195D050800B7D73C /* _ASAsyncTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASAsyncTransaction.h; sourceTree = ""; }; + 058D09F9195D050800B7D73C /* _ASAsyncTransaction.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = _ASAsyncTransaction.mm; sourceTree = ""; }; + 058D09FA195D050800B7D73C /* _ASAsyncTransactionContainer+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "_ASAsyncTransactionContainer+Private.h"; sourceTree = ""; }; + 058D09FB195D050800B7D73C /* _ASAsyncTransactionContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASAsyncTransactionContainer.h; sourceTree = ""; }; + 058D09FC195D050800B7D73C /* _ASAsyncTransactionContainer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = _ASAsyncTransactionContainer.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 058D09FD195D050800B7D73C /* _ASAsyncTransactionGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASAsyncTransactionGroup.h; sourceTree = ""; }; + 058D09FE195D050800B7D73C /* _ASAsyncTransactionGroup.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = _ASAsyncTransactionGroup.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 058D09FF195D050800B7D73C /* UIView+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+ASConvenience.h"; sourceTree = ""; }; + 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCoreAnimationExtras.h; sourceTree = ""; }; + 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = _ASCoreAnimationExtras.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 058D0A05195D050800B7D73C /* _ASPendingState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASPendingState.h; sourceTree = ""; }; + 058D0A06195D050800B7D73C /* _ASPendingState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASPendingState.mm; sourceTree = ""; }; + 058D0A07195D050800B7D73C /* _ASScopeTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASScopeTimer.h; sourceTree = ""; }; + 058D0A08195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+AsyncDisplay.mm"; sourceTree = ""; }; + 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+DebugTiming.h"; sourceTree = ""; }; + 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+DebugTiming.mm"; sourceTree = ""; }; + 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+UIViewBridge.mm"; sourceTree = ""; }; + 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeInternal.h; sourceTree = ""; }; + 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+CGExtras.h"; sourceTree = ""; }; + 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+CGExtras.mm"; sourceTree = ""; }; + 058D0A12195D050800B7D73C /* ASThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASThread.h; sourceTree = ""; }; + 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDisplayLayerTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 058D0A2E195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDisplayNodeAppearanceTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 058D0A2F195D057000B7D73C /* ASDisplayNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeTests.mm; sourceTree = ""; }; + 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeTestsHelper.h; sourceTree = ""; }; + 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeTestsHelper.mm; sourceTree = ""; }; + 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMutableAttributedStringBuilderTests.mm; sourceTree = ""; }; + 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitCoreTextAdditionsTests.mm; sourceTree = ""; }; + 058D0A36195D057000B7D73C /* ASTextNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNodeTests.mm; sourceTree = ""; }; + 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNodeWordKernerTests.mm; sourceTree = ""; }; + 058D0A43195D058D00B7D73C /* ASAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAssert.h; sourceTree = ""; }; + 058D0A44195D058D00B7D73C /* ASBaseDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBaseDefines.h; sourceTree = ""; }; + 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASSnapshotTestCase.mm; sourceTree = ""; }; + 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; + 0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASLayout+IGListKit.h"; sourceTree = ""; }; + 0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayout+IGListKit.mm"; sourceTree = ""; }; + 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = ""; }; + 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionNode.mm; sourceTree = ""; }; + 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; + 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCornerLayoutSpec.h; sourceTree = ""; }; + 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCornerLayoutSpec.mm; sourceTree = ""; }; + 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCornerLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewLayout+ASConvenience.h"; sourceTree = ""; }; + 205F0E0E1B371875007741D0 /* UICollectionViewLayout+ASConvenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UICollectionViewLayout+ASConvenience.mm"; sourceTree = ""; }; + 205F0E111B371BD7007741D0 /* ASScrollDirection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollDirection.mm; sourceTree = ""; }; + 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAbstractLayoutController.h; sourceTree = ""; }; + 205F0E181B37339C007741D0 /* ASAbstractLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASAbstractLayoutController.mm; sourceTree = ""; }; + 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewLayoutController.h; sourceTree = ""; }; + 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewLayoutController.mm; sourceTree = ""; }; + 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CoreGraphics+ASConvenience.h"; sourceTree = ""; }; + 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBasicImageDownloaderTests.mm; sourceTree = ""; }; + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionViewFlowLayoutInspectorTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitTruncationTests.mm; sourceTree = ""; }; + 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitTests.mm; sourceTree = ""; }; + 257754931BEE44CD00737CA5 /* ASTextKitRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitRenderer.h; path = TextKit/ASTextKitRenderer.h; sourceTree = ""; }; + 257754941BEE44CD00737CA5 /* ASTextKitAttributes.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitAttributes.mm; path = TextKit/ASTextKitAttributes.mm; sourceTree = ""; }; + 257754951BEE44CD00737CA5 /* ASTextKitAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitAttributes.h; path = TextKit/ASTextKitAttributes.h; sourceTree = ""; }; + 257754961BEE44CD00737CA5 /* ASTextKitContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitContext.h; path = TextKit/ASTextKitContext.h; sourceTree = ""; }; + 257754971BEE44CD00737CA5 /* ASTextKitContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitContext.mm; path = TextKit/ASTextKitContext.mm; sourceTree = ""; }; + 257754981BEE44CD00737CA5 /* ASTextKitEntityAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitEntityAttribute.h; path = TextKit/ASTextKitEntityAttribute.h; sourceTree = ""; }; + 257754991BEE44CD00737CA5 /* ASTextKitEntityAttribute.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitEntityAttribute.mm; path = TextKit/ASTextKitEntityAttribute.mm; sourceTree = ""; }; + 2577549A1BEE44CD00737CA5 /* ASTextKitRenderer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitRenderer.mm; path = TextKit/ASTextKitRenderer.mm; sourceTree = ""; }; + 2577549B1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ASTextKitRenderer+Positioning.h"; path = "TextKit/ASTextKitRenderer+Positioning.h"; sourceTree = ""; }; + 2577549C1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "ASTextKitRenderer+Positioning.mm"; path = "TextKit/ASTextKitRenderer+Positioning.mm"; sourceTree = ""; }; + 2577549D1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ASTextKitRenderer+TextChecking.h"; path = "TextKit/ASTextKitRenderer+TextChecking.h"; sourceTree = ""; }; + 2577549E1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "ASTextKitRenderer+TextChecking.mm"; path = "TextKit/ASTextKitRenderer+TextChecking.mm"; sourceTree = ""; }; + 2577549F1BEE44CD00737CA5 /* ASTextKitShadower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitShadower.h; path = TextKit/ASTextKitShadower.h; sourceTree = ""; }; + 257754A01BEE44CD00737CA5 /* ASTextKitShadower.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitShadower.mm; path = TextKit/ASTextKitShadower.mm; sourceTree = ""; }; + 257754A11BEE44CD00737CA5 /* ASTextKitTailTruncater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitTailTruncater.h; path = TextKit/ASTextKitTailTruncater.h; sourceTree = ""; }; + 257754A21BEE44CD00737CA5 /* ASTextKitTailTruncater.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitTailTruncater.mm; path = TextKit/ASTextKitTailTruncater.mm; sourceTree = ""; }; + 257754A31BEE44CD00737CA5 /* ASTextKitTruncating.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitTruncating.h; path = TextKit/ASTextKitTruncating.h; sourceTree = ""; }; + 257754B71BEE458D00737CA5 /* ASTextKitComponents.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitComponents.mm; path = TextKit/ASTextKitComponents.mm; sourceTree = ""; }; + 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitCoreTextAdditions.mm; path = TextKit/ASTextKitCoreTextAdditions.mm; sourceTree = ""; }; + 257754B91BEE458E00737CA5 /* ASTextNodeWordKerner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextNodeWordKerner.h; path = TextKit/ASTextNodeWordKerner.h; sourceTree = ""; }; + 257754BA1BEE458E00737CA5 /* ASTextKitComponents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitComponents.h; path = TextKit/ASTextKitComponents.h; sourceTree = ""; }; + 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitCoreTextAdditions.h; path = TextKit/ASTextKitCoreTextAdditions.h; sourceTree = ""; }; + 257754BC1BEE458E00737CA5 /* ASTextNodeTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextNodeTypes.h; path = TextKit/ASTextNodeTypes.h; sourceTree = ""; }; + 257754BD1BEE458E00737CA5 /* ASTextNodeWordKerner.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextNodeWordKerner.mm; path = TextKit/ASTextNodeWordKerner.mm; sourceTree = ""; }; + 25E327541C16819500A2170C /* ASPagerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASPagerNode.h; sourceTree = ""; }; + 25E327551C16819500A2170C /* ASPagerNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASPagerNode.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 2911485B1A77147A005D0878 /* ASControlNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASControlNodeTests.mm; sourceTree = ""; }; + 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutRangeType.h; sourceTree = ""; }; + 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASBasicImageDownloaderInternal.h; sourceTree = ""; }; + 296A0A311A951715005ACEAA /* ASScrollDirection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASScrollDirection.h; path = Source/Details/ASScrollDirection.h; sourceTree = SOURCE_ROOT; }; + 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASBatchFetchingTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 299DA1A71A828D2900162D41 /* ASBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchContext.h; sourceTree = ""; }; + 299DA1A81A828D2900162D41 /* ASBatchContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBatchContext.mm; sourceTree = ""; }; + 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASBasicImageDownloaderContextTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionReusableView.h; sourceTree = ""; }; + 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionReusableView.mm; sourceTree = ""; }; + 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTableViewTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDataController.h; sourceTree = ""; }; + 4640521A1A3F83C40061C0BA /* ASDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDataController.mm; sourceTree = ""; }; + 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; + 4640521C1A3F83C40061C0BA /* ASTableLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableLayoutController.mm; sourceTree = ""; }; + 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; + 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRunLoopQueueTests.mm; sourceTree = ""; }; + 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; + 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPINRemoteImageDownloader.mm; sourceTree = ""; }; + 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = ""; }; + 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASImageContainerProtocolCategories.mm; sourceTree = ""; }; + 68355B391CB57A5A001D4E68 /* ASPINRemoteImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPINRemoteImageDownloader.h; sourceTree = ""; }; + 683F563620E409D600CEB7A3 /* ASDisplayNode+InterfaceState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+InterfaceState.h"; sourceTree = ""; }; + 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Beta.h"; sourceTree = ""; }; + 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+AnimatedImagePrivate.h"; sourceTree = ""; }; + 68B8A4DF1CBDB958007E4543 /* ASWeakProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakProxy.h; sourceTree = ""; }; + 68B8A4E01CBDB958007E4543 /* ASWeakProxy.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakProxy.mm; sourceTree = ""; }; + 68C215561DE10D330019C4BC /* ASCollectionViewLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewLayoutInspector.h; sourceTree = ""; }; + 68C215571DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewLayoutInspector.mm; sourceTree = ""; }; + 68EE0DBB1C1B4ED300BA1B99 /* ASMainSerialQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMainSerialQueue.h; sourceTree = ""; }; + 68EE0DBC1C1B4ED300BA1B99 /* ASMainSerialQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMainSerialQueue.mm; sourceTree = ""; }; + 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNavigationController.h; sourceTree = ""; }; + 68FC85DD1CE29AB700EDD713 /* ASNavigationController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNavigationController.mm; sourceTree = ""; }; + 68FC85E01CE29B7E00EDD713 /* ASTabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTabBarController.h; sourceTree = ""; }; + 68FC85E11CE29B7E00EDD713 /* ASTabBarController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTabBarController.mm; sourceTree = ""; }; + 68FC85E71CE29C7D00EDD713 /* ASVisibilityProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASVisibilityProtocols.h; sourceTree = ""; }; + 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASVisibilityProtocols.mm; sourceTree = ""; }; + 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+Private.h"; sourceTree = ""; }; + 6907C2561DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASObjectDescriptionHelpers.h; sourceTree = ""; }; + 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASObjectDescriptionHelpers.mm; sourceTree = ""; }; + 690BC8BF20F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeCornerLayerDelegate.h; sourceTree = ""; }; + 690BC8C020F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeCornerLayerDelegate.mm; sourceTree = ""; }; + 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDimensionInternal.mm; sourceTree = ""; }; + 690C35631E055C7B00069B91 /* ASDimensionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDimensionInternal.h; sourceTree = ""; }; + 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementStylePrivate.h; sourceTree = ""; }; + 690ED5931E36D118000627C0 /* ASControlNode+tvOS.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASControlNode+tvOS.mm"; sourceTree = ""; }; + 690ED5951E36D118000627C0 /* ASImageNode+tvOS.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+tvOS.mm"; sourceTree = ""; }; + 692510131E74FB44003F2DD0 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 692BE8D61E36B65B00C86D87 /* ASLayoutSpecPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutSpecPrivate.h; sourceTree = ""; }; + 6947B0BC1E36B4E30007C478 /* ASStackUnpositionedLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackUnpositionedLayout.h; sourceTree = ""; }; + 6947B0BD1E36B4E30007C478 /* ASStackUnpositionedLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackUnpositionedLayout.mm; sourceTree = ""; }; + 6947B0C11E36B5040007C478 /* ASStackPositionedLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackPositionedLayout.h; sourceTree = ""; }; + 6947B0C21E36B5040007C478 /* ASStackPositionedLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackPositionedLayout.mm; sourceTree = ""; }; + 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeLayout.h; sourceTree = ""; }; + 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWrapperSpecSnapshotTests.mm; sourceTree = ""; }; + 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEventLog.h; sourceTree = ""; }; + 696F01EB1DD2AF450049FBD5 /* ASEventLog.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASEventLog.mm; sourceTree = ""; }; + 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASBackgroundLayoutSpecSnapshotTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 6977965D1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASLayoutSpec+Subclasses.h"; sourceTree = ""; }; + 6977965E1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayoutSpec+Subclasses.mm"; sourceTree = ""; }; + 697B31591CFE4B410049936F /* ASEditableTextNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASEditableTextNodeTests.mm; sourceTree = ""; }; + 698371D91E4379CD00437585 /* ASNodeController+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASNodeController+Beta.h"; sourceTree = ""; }; + 698371DA1E4379CD00437585 /* ASNodeController+Beta.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASNodeController+Beta.mm"; sourceTree = ""; }; + 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementExtensibility.h; sourceTree = ""; }; + 698DFF431E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutSpecUtilities.h; sourceTree = ""; }; + 698DFF461E36B7E9002891F1 /* ASLayoutSpecUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutSpecUtilities.h; sourceTree = ""; }; + 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutSpecTests.mm; sourceTree = ""; }; + 69B225661D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeLayoutTests.mm; sourceTree = ""; }; + 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Layout.mm"; sourceTree = ""; }; + 69CB62A91CB8165900024920 /* _ASDisplayViewAccessiblity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASDisplayViewAccessiblity.h; sourceTree = ""; }; + 69CB62AA1CB8165900024920 /* _ASDisplayViewAccessiblity.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASDisplayViewAccessiblity.mm; sourceTree = ""; }; + 69F10C851C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASRangeControllerUpdateRangeProtocol+Beta.h"; sourceTree = ""; }; + 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElementStyleTests.mm; sourceTree = ""; }; + 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = ""; }; + 764D83D21C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit+Debug.h"; sourceTree = ""; }; + 764D83D31C8EA515009B4FB8 /* AsyncDisplayKit+Debug.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "AsyncDisplayKit+Debug.mm"; sourceTree = ""; }; + 7A06A7381C35F08800FE8DAA /* ASRelativeLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRelativeLayoutSpec.mm; sourceTree = ""; }; + 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRelativeLayoutSpec.h; sourceTree = ""; }; + 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRelativeLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+ASConvenience.h"; sourceTree = ""; }; + 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UIImage+ASConvenience.mm"; sourceTree = ""; }; + 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNodeSnapshotTests.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 = ""; }; + 83A7D9581D44542100BF333E /* ASWeakMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakMap.h; sourceTree = ""; }; + 83A7D9591D44542100BF333E /* ASWeakMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakMap.mm; sourceTree = ""; }; + 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakMapTests.mm; sourceTree = ""; }; + 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDefaultPlaybackButton.h; sourceTree = ""; }; + 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDefaultPlaybackButton.mm; 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 = ""; }; + 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASYogaUtilities.h; sourceTree = ""; }; + 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASYogaUtilities.mm; sourceTree = ""; }; + 909C4C731F09C98B00D6B76F /* ASTextNode2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextNode2.h; sourceTree = ""; }; + 909C4C741F09C98B00D6B76F /* ASTextNode2.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNode2.mm; sourceTree = ""; }; + 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Yoga.mm"; sourceTree = ""; }; + 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = ""; }; + 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = ""; }; + 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + 9644CFDE2193777C00213478 /* ASThrashUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASThrashUtility.h; sourceTree = ""; }; + 9644CFDF2193777C00213478 /* ASThrashUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASThrashUtility.m; sourceTree = ""; }; + 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewThrashTests.mm; sourceTree = ""; }; + 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutElement.h; sourceTree = ""; }; + 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAsciiArtBoxCreator.h; sourceTree = ""; }; + 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASAsciiArtBoxCreator.mm; sourceTree = ""; }; + 9C6BB3B01B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAbsoluteLayoutElement.h; sourceTree = ""; }; + 9C70F2011CDA4EFA007D6C76 /* ASTraitCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTraitCollection.h; sourceTree = ""; }; + 9C70F2021CDA4EFA007D6C76 /* ASTraitCollection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTraitCollection.mm; sourceTree = ""; }; + 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitFontSizeAdjuster.mm; path = TextKit/ASTextKitFontSizeAdjuster.mm; sourceTree = ""; }; + 9CDC18CB1B910E12004965E2 /* ASLayoutElementPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementPrivate.h; sourceTree = ""; }; + 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASViewController.mm; sourceTree = ""; }; + 9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableNode.mm; sourceTree = ""; }; + 9D302F9A2231B07E005739C3 /* ASButtonNode+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASButtonNode+Private.h"; sourceTree = ""; }; + 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASButtonNode+Yoga.h"; sourceTree = ""; }; + 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASButtonNode+Yoga.mm"; sourceTree = ""; }; + 9D9AA56721E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+LayoutSpec.mm"; sourceTree = ""; }; + 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Yoga.h"; sourceTree = ""; }; + 9D9AA56C21E2568500172C09 /* ASDisplayNode+LayoutSpec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+LayoutSpec.h"; sourceTree = ""; }; + 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionViewTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 9F98C0231DBDF2A300476D92 /* ASControlTargetAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASControlTargetAction.h; sourceTree = ""; }; + 9F98C0241DBDF2A300476D92 /* ASControlTargetAction.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASControlTargetAction.mm; sourceTree = ""; }; + A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASPINRemoteImageDownloader.h; path = Details/ASPINRemoteImageDownloader.h; sourceTree = ""; }; + A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASPINRemoteImageDownloader.mm; path = Details/ASPINRemoteImageDownloader.mm; sourceTree = ""; }; + A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitFontSizeAdjuster.h; path = TextKit/ASTextKitFontSizeAdjuster.h; sourceTree = ""; }; + A373200E1C571B050011FC94 /* ASTextNode+Beta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASTextNode+Beta.h"; sourceTree = ""; }; + AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASAbsoluteLayoutSpecSnapshotTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASHierarchyChangeSet.h; sourceTree = ""; }; + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASHierarchyChangeSet.mm; sourceTree = ""; }; + AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutDefines.h; sourceTree = ""; }; + AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionView.h; sourceTree = ""; }; + AC3C4A501A1139C100143C57 /* ASCollectionView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionView.mm; sourceTree = ""; }; + AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewProtocols.h; sourceTree = ""; }; + AC6145401D8AFAE8003D62A2 /* ASSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASSection.h; path = ../Private/ASSection.h; sourceTree = ""; }; + AC6145421D8AFD4F003D62A2 /* ASSection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASSection.mm; path = ../Private/ASSection.mm; sourceTree = ""; }; + AC6456071B0A335000CF11B8 /* ASCellNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCellNode.mm; sourceTree = ""; }; + AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableViewInternal.h; sourceTree = ""; }; + ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASViewController.h; sourceTree = ""; }; + ACE87A2B1D73696800D7FF06 /* ASSectionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASSectionContext.h; path = Details/ASSectionContext.h; sourceTree = ""; }; + ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBackgroundLayoutSpec.h; sourceTree = ""; }; + ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASBackgroundLayoutSpec.mm; sourceTree = ""; }; + ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCenterLayoutSpec.h; sourceTree = ""; }; + ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCenterLayoutSpec.mm; sourceTree = ""; }; + ACF6ED071B17843500DA7C62 /* ASDimension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDimension.h; sourceTree = ""; }; + ACF6ED081B17843500DA7C62 /* ASDimension.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDimension.mm; sourceTree = ""; }; + ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASInsetLayoutSpec.h; sourceTree = ""; }; + ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASInsetLayoutSpec.mm; sourceTree = ""; }; + ACF6ED0B1B17843500DA7C62 /* ASLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayout.h; sourceTree = ""; }; + ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayout.mm; sourceTree = ""; }; + ACF6ED0D1B17843500DA7C62 /* ASLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutSpec.h; sourceTree = ""; }; + ACF6ED0E1B17843500DA7C62 /* ASLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASLayoutSpec.mm; sourceTree = ""; }; + ACF6ED111B17843500DA7C62 /* ASLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElement.h; sourceTree = ""; }; + ACF6ED121B17843500DA7C62 /* ASOverlayLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASOverlayLayoutSpec.h; sourceTree = ""; }; + ACF6ED131B17843500DA7C62 /* ASOverlayLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASOverlayLayoutSpec.mm; sourceTree = ""; }; + ACF6ED141B17843500DA7C62 /* ASRatioLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRatioLayoutSpec.h; sourceTree = ""; }; + ACF6ED151B17843500DA7C62 /* ASRatioLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASRatioLayoutSpec.mm; sourceTree = ""; }; + ACF6ED161B17843500DA7C62 /* ASStackLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutSpec.h; sourceTree = ""; }; + ACF6ED171B17843500DA7C62 /* ASStackLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackLayoutSpec.mm; sourceTree = ""; }; + ACF6ED181B17843500DA7C62 /* ASAbsoluteLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAbsoluteLayoutSpec.h; sourceTree = ""; }; + ACF6ED191B17843500DA7C62 /* ASAbsoluteLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASAbsoluteLayoutSpec.mm; sourceTree = ""; }; + ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASInternalHelpers.h; sourceTree = ""; }; + ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASInternalHelpers.mm; sourceTree = ""; }; + ACF6ED531B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCenterLayoutSpecSnapshotTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + ACF6ED541B178DC700DA7C62 /* ASDimensionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDimensionTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASInsetLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + ACF6ED571B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutSpecSnapshotTestsHelper.h; sourceTree = ""; }; + ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASLayoutSpecSnapshotTestsHelper.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASOverlayLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRatioLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + AE440174210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitFontSizeAdjusterTests.mm; sourceTree = ""; }; + AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPagerNodeTests.mm; sourceTree = ""; }; + AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDefaultPlayButton.h; sourceTree = ""; }; + AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDefaultPlayButton.mm; sourceTree = ""; }; + AEEC47DF1C20C2DD00EC1693 /* ASVideoNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASVideoNode.h; sourceTree = ""; }; + AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASVideoNode.mm; sourceTree = ""; }; + AEEC47E31C21D3D200EC1693 /* ASVideoNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASVideoNodeTests.mm; sourceTree = ""; }; + B0F880581BEAEC7500D17647 /* ASTableNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableNode.h; sourceTree = ""; }; + B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewLayoutFacilitatorProtocol.h; sourceTree = ""; }; + B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionNode+Beta.h"; sourceTree = ""; }; + B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutManager.h; path = TextKit/ASLayoutManager.h; sourceTree = ""; }; + B30BF6511C5964B0004FCD53 /* ASLayoutManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASLayoutManager.mm; path = TextKit/ASLayoutManager.mm; sourceTree = ""; }; + B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNavigationControllerTests.mm; sourceTree = ""; }; + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTabBarControllerTests.mm; sourceTree = ""; }; + BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; + C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASAbstractLayoutController+FrameworkPrivate.h"; sourceTree = ""; }; + C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNode2SnapshotTests.mm; sourceTree = ""; }; + CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Convenience.h"; sourceTree = ""; }; + CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Convenience.mm"; sourceTree = ""; }; + CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit+IGListKitMethods.h"; sourceTree = ""; }; + CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "AsyncDisplayKit+IGListKitMethods.mm"; sourceTree = ""; }; + CC051F1E1D7A286A006434CB /* ASCALayerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCALayerTests.mm; sourceTree = ""; }; + CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASUICollectionViewTests.mm; sourceTree = ""; }; + CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewFlowLayoutInspector.mm; sourceTree = ""; }; + CC0F885A1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewFlowLayoutInspector.h; sourceTree = ""; }; + CC0F885D1E4280B800576FED /* _ASCollectionViewCell.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionViewCell.mm; sourceTree = ""; }; + CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionViewCell.h; sourceTree = ""; }; + CC0F88691E4286FA00576FED /* ReferenceImages_64 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ReferenceImages_64; sourceTree = ""; }; + CC0F886A1E4286FA00576FED /* ReferenceImages_iOS_10 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ReferenceImages_iOS_10; sourceTree = ""; }; + CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNetworkImageNodeTests.mm; sourceTree = ""; }; + CC18248B200D49C800875940 /* ASTextNodeCommon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTextNodeCommon.h; sourceTree = ""; }; + CC224E952066CA6D00BBA57F /* configuration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = configuration.json; sourceTree = ""; }; + CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionView+Undeprecated.h"; sourceTree = ""; }; + CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableElementMap.h; sourceTree = ""; }; + CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMutableElementMap.mm; sourceTree = ""; }; + CC35CEC120DD7F600006448D /* ASCollections.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASCollections.h; sourceTree = ""; }; + CC35CEC220DD7F600006448D /* ASCollections.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollections.mm; sourceTree = ""; }; + CC35CEC520DD87280006448D /* ASCollectionsTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionsTests.mm; sourceTree = ""; }; + CC36C18E218B841600232F23 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + CC36C190218B841A00232F23 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; + CC36C192218B842E00232F23 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + CC36C195218B845B00232F23 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + CC36C197218B846300232F23 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + CC36C199218B846F00232F23 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + CC36C19B218B847400232F23 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + CC3B20811C3F76D600798563 /* ASPendingStateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPendingStateController.h; sourceTree = ""; }; + CC3B20821C3F76D600798563 /* ASPendingStateController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPendingStateController.mm; sourceTree = ""; }; + CC3B20871C3F7A5400798563 /* ASWeakSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakSet.h; sourceTree = ""; }; + CC3B20881C3F7A5400798563 /* ASWeakSet.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakSet.mm; sourceTree = ""; }; + CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakSetTests.mm; sourceTree = ""; }; + CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASBridgedPropertiesTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableViewThrashTests.mm; sourceTree = ""; }; + CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSIndexSet+ASHelpers.h"; sourceTree = ""; }; + CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSIndexSet+ASHelpers.mm"; sourceTree = ""; }; + CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTraceEvent.h; sourceTree = ""; }; + CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTraceEvent.mm; sourceTree = ""; }; + CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableView+Undeprecated.h"; sourceTree = ""; }; + CC54A81B1D70077A00296A24 /* ASDispatch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASDispatch.h; sourceTree = ""; }; + CC54A81D1D7008B300296A24 /* ASDispatchTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDispatchTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + CC55A70B1E529FA200594372 /* UIResponder+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIResponder+AsyncDisplayKit.h"; sourceTree = ""; }; + CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UIResponder+AsyncDisplayKit.mm"; sourceTree = ""; }; + CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASResponderChainEnumerator.h; sourceTree = ""; }; + CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASResponderChainEnumerator.mm; sourceTree = ""; }; + CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIntegerMap.h; sourceTree = ""; }; + CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASIntegerMap.mm; sourceTree = ""; }; + CC57EAF91E394EA40034C595 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+OCMock.mm"; sourceTree = ""; }; + CC583AC11EF9BAB400134156 /* ASTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTestCase.h; sourceTree = ""; }; + CC583AC21EF9BAB400134156 /* ASTestCase.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTestCase.mm; sourceTree = ""; }; + CC583AC31EF9BAB400134156 /* ASXCTExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASXCTExtensions.h; sourceTree = ""; }; + CC583AC41EF9BAB400134156 /* NSInvocation+ASTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+ASTestHelpers.h"; sourceTree = ""; }; + CC583AC51EF9BAB400134156 /* NSInvocation+ASTestHelpers.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSInvocation+ASTestHelpers.mm"; sourceTree = ""; }; + CC583AC61EF9BAB400134156 /* OCMockObject+ASAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCMockObject+ASAdditions.h"; sourceTree = ""; }; + CC583AC71EF9BAB400134156 /* OCMockObject+ASAdditions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "OCMockObject+ASAdditions.mm"; sourceTree = ""; }; + CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBlockTypes.h; sourceTree = ""; }; + CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ASDisplayNode+Ancestry.h"; path = "Base/ASDisplayNode+Ancestry.h"; sourceTree = ""; }; + CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "ASDisplayNode+Ancestry.mm"; path = "Base/ASDisplayNode+Ancestry.mm"; sourceTree = ""; }; + CC7AF195200D9BD500A21BDE /* ASExperimentalFeatures.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASExperimentalFeatures.h; sourceTree = ""; }; + CC7AF197200D9E8400A21BDE /* ASExperimentalFeatures.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASExperimentalFeatures.mm; sourceTree = ""; }; + CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; + CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPhotosFrameworkImageRequest.mm; sourceTree = ""; }; + CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPhotosFrameworkImageRequestTests.mm; sourceTree = ""; }; + CC84C7F020474C5300A3851B /* ASCGImageBuffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASCGImageBuffer.h; sourceTree = ""; }; + CC84C7F120474C5300A3851B /* ASCGImageBuffer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCGImageBuffer.mm; sourceTree = ""; }; + CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCellNode+Internal.h"; sourceTree = ""; }; + CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPerformanceTestContext.h; sourceTree = ""; }; + CC8B05D51D73836400F54286 /* ASPerformanceTestContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPerformanceTestContext.mm; sourceTree = ""; }; + CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNodePerformanceTests.mm; sourceTree = ""; }; + CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASViewControllerTests.mm; sourceTree = ""; }; + CCA282B21E9EA7310037E8B7 /* ASTipsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipsController.h; sourceTree = ""; }; + CCA282B31E9EA7310037E8B7 /* ASTipsController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTipsController.mm; sourceTree = ""; }; + CCA282B61E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit+Tips.h"; sourceTree = ""; }; + CCA282B71E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "AsyncDisplayKit+Tips.mm"; sourceTree = ""; }; + CCA282BA1E9EABDD0037E8B7 /* ASTipProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipProvider.h; sourceTree = ""; }; + CCA282BB1E9EABDD0037E8B7 /* ASTipProvider.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTipProvider.mm; sourceTree = ""; }; + CCA282BE1E9EAE010037E8B7 /* ASTip.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTip.h; sourceTree = ""; }; + CCA282BF1E9EAE010037E8B7 /* ASTip.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTip.mm; sourceTree = ""; }; + CCA282C21E9EAE630037E8B7 /* ASLayerBackingTipProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayerBackingTipProvider.h; sourceTree = ""; }; + CCA282C31E9EAE630037E8B7 /* ASLayerBackingTipProvider.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayerBackingTipProvider.mm; sourceTree = ""; }; + CCA282C61E9EB64B0037E8B7 /* ASDisplayNodeTipState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeTipState.h; sourceTree = ""; }; + CCA282C71E9EB64B0037E8B7 /* ASDisplayNodeTipState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeTipState.mm; sourceTree = ""; }; + CCA282CA1E9EB73E0037E8B7 /* ASTipNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipNode.h; sourceTree = ""; }; + CCA282CB1E9EB73E0037E8B7 /* ASTipNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTipNode.mm; sourceTree = ""; }; + CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipsWindow.h; sourceTree = ""; }; + CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTipsWindow.mm; sourceTree = ""; }; + CCA5F62D1EECC2A80060C137 /* ASAssert.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASAssert.mm; sourceTree = ""; }; + CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRecursiveUnfairLock.h; sourceTree = ""; }; + CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRecursiveUnfairLock.mm; sourceTree = ""; }; + CCAA0B81206ADECB0057B336 /* ASRecursiveUnfairLockTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRecursiveUnfairLockTests.mm; sourceTree = ""; }; + CCB1F9591EFB60A5009C7475 /* ASLog.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLog.mm; sourceTree = ""; }; + CCB1F95B1EFB6316009C7475 /* ASSignpost.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASSignpost.h; sourceTree = ""; }; + CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeSnapshotTests.mm; sourceTree = ""; }; + CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeManagingNode.h; sourceTree = ""; }; + CCBD05DE1E4147B000D18509 /* ASIGListAdapterBasedDataSource.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASIGListAdapterBasedDataSource.mm; sourceTree = ""; }; + CCBD05DF1E4147B000D18509 /* ASIGListAdapterBasedDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIGListAdapterBasedDataSource.h; sourceTree = ""; }; + CCBDDD0320C62A2D00CBA922 /* ASMainThreadDeallocation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASMainThreadDeallocation.h; sourceTree = ""; }; + CCBDDD0420C62A2D00CBA922 /* ASMainThreadDeallocation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMainThreadDeallocation.mm; sourceTree = ""; }; + CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextDebugOption.h; sourceTree = ""; }; + CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextDebugOption.mm; sourceTree = ""; }; + CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextInput.h; sourceTree = ""; }; + CCCCCCC61EC3EF060087FE10 /* ASTextInput.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextInput.mm; sourceTree = ""; }; + CCCCCCC71EC3EF060087FE10 /* ASTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextLayout.h; sourceTree = ""; }; + CCCCCCC81EC3EF060087FE10 /* ASTextLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextLayout.mm; sourceTree = ""; }; + CCCCCCC91EC3EF060087FE10 /* ASTextLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextLine.h; sourceTree = ""; }; + CCCCCCCA1EC3EF060087FE10 /* ASTextLine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextLine.mm; sourceTree = ""; }; + CCCCCCCC1EC3EF060087FE10 /* ASTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextAttribute.h; sourceTree = ""; }; + CCCCCCCD1EC3EF060087FE10 /* ASTextAttribute.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextAttribute.mm; sourceTree = ""; }; + CCCCCCCE1EC3EF060087FE10 /* ASTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextRunDelegate.h; sourceTree = ""; }; + CCCCCCCF1EC3EF060087FE10 /* ASTextRunDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextRunDelegate.mm; sourceTree = ""; }; + CCCCCCD11EC3EF060087FE10 /* ASTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextUtilities.h; sourceTree = ""; }; + CCCCCCD21EC3EF060087FE10 /* ASTextUtilities.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextUtilities.mm; sourceTree = ""; }; + CCCCCCD31EC3EF060087FE10 /* NSParagraphStyle+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+ASText.h"; sourceTree = ""; }; + CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSParagraphStyle+ASText.mm"; sourceTree = ""; }; + CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+ASText.h"; sourceTree = ""; }; + CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSAttributedString+ASText.mm"; sourceTree = ""; }; + CCDC9B4B200991D10063C1F8 /* ASGraphicsContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASGraphicsContext.h; sourceTree = ""; }; + CCDC9B4C200991D10063C1F8 /* ASGraphicsContext.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASGraphicsContext.mm; sourceTree = ""; }; + CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionModernDataSourceTests.mm; sourceTree = ""; }; + CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSectionController.h; sourceTree = ""; }; + CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = ""; }; + CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "IGListAdapter+AsyncDisplayKit.mm"; sourceTree = ""; }; + CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = ""; }; + CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASIntegerMapTests.mm; sourceTree = ""; }; + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutEngineTests.mm; sourceTree = ""; }; + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTestNode.h; sourceTree = ""; }; + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTestNode.mm; sourceTree = ""; }; + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = debugbreak.h; sourceTree = ""; }; + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTLayoutFixture.h; sourceTree = ""; }; + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTLayoutFixture.mm; sourceTree = ""; }; + CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASNetworkImageLoadInfo.h; sourceTree = ""; }; + CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNetworkImageLoadInfo.mm; sourceTree = ""; }; + CCED5E402020D41600395C40 /* ASNetworkImageLoadInfo+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASNetworkImageLoadInfo+Private.h"; sourceTree = ""; }; + CCEDDDC8200C2AC300FFCD0A /* ASConfigurationInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASConfigurationInternal.h; sourceTree = ""; }; + CCEDDDC9200C2AC300FFCD0A /* ASConfigurationInternal.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASConfigurationInternal.mm; sourceTree = ""; }; + CCEDDDCC200C2CB900FFCD0A /* ASConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASConfiguration.h; sourceTree = ""; }; + CCEDDDCE200C42A200FFCD0A /* ASConfigurationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASConfigurationDelegate.h; sourceTree = ""; }; + CCEDDDD0200C488000FFCD0A /* ASConfiguration.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASConfiguration.mm; sourceTree = ""; }; + CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASConfigurationTests.mm; sourceTree = ""; }; + CCF1FF5D20C4785000AAD8FC /* ASLocking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASLocking.h; sourceTree = ""; }; + D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; + D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; + D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = ""; }; + DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = _ASTransitionContext.h; path = ../_ASTransitionContext.h; sourceTree = ""; }; + DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = _ASTransitionContext.mm; path = ../_ASTransitionContext.mm; sourceTree = ""; }; + DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASContextTransitioning.h; sourceTree = ""; }; + DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Diffing.h"; sourceTree = ""; }; + DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSArray+Diffing.mm"; sourceTree = ""; }; + DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ArrayDiffingTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDisplayNodeImplicitHierarchyTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPagerFlowLayout.h; sourceTree = ""; }; + DBDB83931C6E879900D0098C /* ASPagerFlowLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPagerFlowLayout.mm; sourceTree = ""; }; + DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkPrivate.h"; sourceTree = ""; }; + DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDelegateProxy.h; sourceTree = ""; }; + DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDelegateProxy.mm; sourceTree = ""; }; + DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASCollectionInternal.h; path = Details/ASCollectionInternal.h; sourceTree = ""; }; + DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; + DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; + E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutFlatteningTests.mm; sourceTree = ""; }; + E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; + E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; + E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASPagerNode+Beta.h"; sourceTree = ""; }; + E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; + E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPageTable.mm; sourceTree = ""; }; + E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutInfo.h; sourceTree = ""; }; + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionGalleryLayoutInfo.mm; sourceTree = ""; }; + E5711A2A1C840C81009619D4 /* ASCollectionElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionElement.h; sourceTree = ""; }; + E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionElement.mm; sourceTree = ""; }; + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutItem.h; sourceTree = ""; }; + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionGalleryLayoutItem.mm; sourceTree = ""; }; + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutState+Private.h"; sourceTree = ""; }; + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutCache.h; sourceTree = ""; }; + E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutCache.mm; sourceTree = ""; }; + E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutDefines.mm; sourceTree = ""; }; + E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDefines.h; sourceTree = ""; }; + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNodeTests.mm; sourceTree = ""; }; + E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; + E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionFlowLayoutDelegate.mm; sourceTree = ""; }; + E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutContext.h; sourceTree = ""; }; + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutContext.mm; sourceTree = ""; }; + E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDelegate.h; sourceTree = ""; }; + E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayout.h; sourceTree = ""; }; + E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayout.mm; sourceTree = ""; }; + E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASElementMap.h; sourceTree = ""; }; + E5B077FE1E69F4EB00C24B5B /* ASElementMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASElementMap.mm; sourceTree = ""; }; + E5B225261F1790B5001E1431 /* ASHashing.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASHashing.mm; sourceTree = ""; }; + E5B225271F1790B5001E1431 /* ASHashing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; + E5B2252D1F17E521001E1431 /* ASDispatch.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDispatch.mm; sourceTree = ""; }; + E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutContext+Private.h"; sourceTree = ""; }; + E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetchingDelegate.h; sourceTree = ""; }; + E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = ""; }; + E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; sourceTree = ""; }; + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutState.mm; sourceTree = ""; }; + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionGalleryLayoutDelegate.h; sourceTree = ""; }; + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F325E48B21745F9E00AC93A4 /* ASButtonNodeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNodeTests.mm; sourceTree = ""; }; + F325E48F217460B000AC93A4 /* ASTextNode2Tests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNode2Tests.mm; sourceTree = ""; }; + F3F698D1211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayViewAccessibilityTests.mm; sourceTree = ""; }; + F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeExtrasTests.mm; sourceTree = ""; }; + FA4FAF14200A850200E735BD /* ASControlNode+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+Private.h"; sourceTree = ""; }; + FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 057D02BC1AC0A66700C7AC3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CC36C19D218B849C00232F23 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058D09B9195D04C000B7D73C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CC36C19F218B894800232F23 /* CoreMedia.framework in Frameworks */, + CC36C19E218B894400232F23 /* AVFoundation.framework in Frameworks */, + CC90E1F41E383C0400FED591 /* AsyncDisplayKit.framework in Frameworks */, + 058D09BE195D04C000B7D73C /* XCTest.framework in Frameworks */, + 058D09C1195D04C000B7D73C /* UIKit.framework in Frameworks */, + 058D09BF195D04C000B7D73C /* Foundation.framework in Frameworks */, + DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B35061D61B010EDF0018CF92 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CC36C19C218B847400232F23 /* CoreMedia.framework in Frameworks */, + CC36C19A218B846F00232F23 /* CoreLocation.framework in Frameworks */, + CC36C198218B846300232F23 /* QuartzCore.framework in Frameworks */, + CC36C196218B845B00232F23 /* AVFoundation.framework in Frameworks */, + CC36C194218B844800232F23 /* Foundation.framework in Frameworks */, + CC36C193218B842E00232F23 /* CoreGraphics.framework in Frameworks */, + CC36C191218B841A00232F23 /* CoreText.framework in Frameworks */, + CC36C18F218B841600232F23 /* UIKit.framework in Frameworks */, + 92DD2FE61BF4D05E0074C9DD /* MapKit.framework in Frameworks */, + B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */, + B350625D1B0111740018CF92 /* Photos.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */ = { + isa = PBXGroup; + children = ( + 057D02C51AC0A66700C7AC3C /* AppDelegate.h */, + 057D02C61AC0A66700C7AC3C /* AppDelegate.mm */, + 057D02C11AC0A66700C7AC3C /* Supporting Files */, + ); + name = AsyncDisplayKitTestHost; + path = TestHost; + sourceTree = ""; + }; + 057D02C11AC0A66700C7AC3C /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 692510131E74FB44003F2DD0 /* Default-568h@2x.png */, + 057D02C21AC0A66700C7AC3C /* Info.plist */, + 057D02C31AC0A66700C7AC3C /* main.mm */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 058D09A3195D04C000B7D73C = { + isa = PBXGroup; + children = ( + 058D09B1195D04C000B7D73C /* Source */, + CC224E942066CA6D00BBA57F /* Schemas */, + 058D09C5195D04C000B7D73C /* Tests */, + 058D09AE195D04C000B7D73C /* Frameworks */, + 058D09AD195D04C000B7D73C /* Products */, + FD40E2760492F0CAAEAD552D /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 058D09AD195D04C000B7D73C /* Products */ = { + isa = PBXGroup; + children = ( + 058D09BC195D04C000B7D73C /* AsyncDisplayKitTests.xctest */, + 057D02BF1AC0A66700C7AC3C /* AsyncDisplayKitTestHost.app */, + B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */, + ); + name = Products; + sourceTree = ""; + }; + 058D09AE195D04C000B7D73C /* Frameworks */ = { + isa = PBXGroup; + children = ( + CC36C19B218B847400232F23 /* CoreMedia.framework */, + CC36C199218B846F00232F23 /* CoreLocation.framework */, + CC36C197218B846300232F23 /* QuartzCore.framework */, + CC36C195218B845B00232F23 /* AVFoundation.framework */, + CC36C192218B842E00232F23 /* CoreGraphics.framework */, + CC36C190218B841A00232F23 /* CoreText.framework */, + CC36C18E218B841600232F23 /* UIKit.framework */, + 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */, + 051943141A1575670030A7D0 /* Photos.framework */, + 051943121A1575630030A7D0 /* AssetsLibrary.framework */, + 058D09AF195D04C000B7D73C /* Foundation.framework */, + 058D09BD195D04C000B7D73C /* XCTest.framework */, + 058D09C0195D04C000B7D73C /* UIKit.framework */, + EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 058D09B1195D04C000B7D73C /* Source */ = { + isa = PBXGroup; + children = ( + CC35CEC120DD7F600006448D /* ASCollections.h */, + CC35CEC220DD7F600006448D /* ASCollections.mm */, + 058D0A42195D058D00B7D73C /* Base */, + CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */, + DE89C1691DCEB9CC00D49D74 /* Debug */, + 058D09E1195D050800B7D73C /* Details */, + AC6456051B0A333200CF11B8 /* Layout */, + 058D0A01195D050800B7D73C /* Private */, + 058D09B2195D04C000B7D73C /* Supporting Files */, + 257754661BED245B00737CA5 /* TextKit */, + 690ED5911E36D118000627C0 /* tvOS */, + CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */, + DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */, + DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */, + CCEDDDCC200C2CB900FFCD0A /* ASConfiguration.h */, + CCEDDDD0200C488000FFCD0A /* ASConfiguration.mm */, + CCEDDDC8200C2AC300FFCD0A /* ASConfigurationInternal.h */, + CCEDDDC9200C2AC300FFCD0A /* ASConfigurationInternal.mm */, + CCEDDDCE200C42A200FFCD0A /* ASConfigurationDelegate.h */, + 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */, + AC6456071B0A335000CF11B8 /* ASCellNode.mm */, + CC84C7F020474C5300A3851B /* ASCGImageBuffer.h */, + CC84C7F120474C5300A3851B /* ASCGImageBuffer.mm */, + DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */, + 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */, + 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */, + B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */, + AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */, + AC3C4A501A1139C100143C57 /* ASCollectionView.mm */, + B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */, + AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */, + DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */, + 058D09D5195D050800B7D73C /* ASControlNode.h */, + 058D09D6195D050800B7D73C /* ASControlNode.mm */, + 058D09D7195D050800B7D73C /* ASControlNode+Subclasses.h */, + 058D09D8195D050800B7D73C /* ASDisplayNode.h */, + 058D09D9195D050800B7D73C /* ASDisplayNode.mm */, + CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */, + CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.mm */, + 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */, + CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */, + CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.mm */, + 683F563620E409D600CEB7A3 /* ASDisplayNode+InterfaceState.h */, + 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */, + 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, + 9D9AA56C21E2568500172C09 /* ASDisplayNode+LayoutSpec.h */, + 9D9AA56721E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm */, + 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */, + 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */, + 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */, + 058D09DC195D050800B7D73C /* ASDisplayNodeExtras.mm */, + 0587F9BB1A7309ED00AFF0BA /* ASEditableTextNode.h */, + 0587F9BC1A7309ED00AFF0BA /* ASEditableTextNode.mm */, + CC7AF195200D9BD500A21BDE /* ASExperimentalFeatures.h */, + CC7AF197200D9E8400A21BDE /* ASExperimentalFeatures.mm */, + 058D09DD195D050800B7D73C /* ASImageNode.h */, + 058D09DE195D050800B7D73C /* ASImageNode.mm */, + 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */, + CCF1FF5D20C4785000AAD8FC /* ASLocking.h */, + CCBDDD0320C62A2D00CBA922 /* ASMainThreadDeallocation.h */, + CCBDDD0420C62A2D00CBA922 /* ASMainThreadDeallocation.mm */, + 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */, + 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */, + 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */, + 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */, + 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */, + 68FC85DD1CE29AB700EDD713 /* ASNavigationController.mm */, + CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */, + CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.mm */, + 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */, + 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */, + 698371D91E4379CD00437585 /* ASNodeController+Beta.h */, + 698371DA1E4379CD00437585 /* ASNodeController+Beta.mm */, + DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */, + DBDB83931C6E879900D0098C /* ASPagerFlowLayout.mm */, + 25E327541C16819500A2170C /* ASPagerNode.h */, + 25E327551C16819500A2170C /* ASPagerNode.mm */, + E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */, + A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */, + A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.mm */, + CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */, + D785F6601A74327E00291744 /* ASScrollNode.h */, + D785F6611A74327E00291744 /* ASScrollNode.mm */, + ACE87A2B1D73696800D7FF06 /* ASSectionContext.h */, + 68FC85E01CE29B7E00EDD713 /* ASTabBarController.h */, + 68FC85E11CE29B7E00EDD713 /* ASTabBarController.mm */, + B0F880581BEAEC7500D17647 /* ASTableNode.h */, + 9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */, + E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */, + 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */, + 055F1A3319ABD3E3004DAFF1 /* ASTableView.mm */, + AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */, + 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */, + CC18248B200D49C800875940 /* ASTextNodeCommon.h */, + 058D09DF195D050800B7D73C /* ASTextNode.h */, + 058D09E0195D050800B7D73C /* ASTextNode.mm */, + A373200E1C571B050011FC94 /* ASTextNode+Beta.h */, + 909C4C731F09C98B00D6B76F /* ASTextNode2.h */, + 909C4C741F09C98B00D6B76F /* ASTextNode2.mm */, + AEEC47DF1C20C2DD00EC1693 /* ASVideoNode.h */, + AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */, + 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */, + 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */, + ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */, + 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */, + 68FC85E71CE29C7D00EDD713 /* ASVisibilityProtocols.h */, + 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.mm */, + 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */, + 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */, + 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */, + CC55A70B1E529FA200594372 /* UIResponder+AsyncDisplayKit.h */, + CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm */, + 9D302F9A2231B07E005739C3 /* ASButtonNode+Private.h */, + 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */, + 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */, + ); + path = Source; + sourceTree = ""; + }; + 058D09B2195D04C000B7D73C /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 044285011BAA3CC700D16268 /* AsyncDisplayKit.modulemap */, + CC57EAF91E394EA40034C595 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 058D09C5195D04C000B7D73C /* Tests */ = { + isa = PBXGroup; + children = ( + 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */, + F325E48F217460B000AC93A4 /* ASTextNode2Tests.mm */, + F325E48B21745F9E00AC93A4 /* ASButtonNodeTests.mm */, + F3F698D1211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm */, + DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.mm */, + AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.mm */, + 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */, + 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm */, + 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.mm */, + 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.mm */, + CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */, + CC051F1E1D7A286A006434CB /* ASCALayerTests.mm */, + ACF6ED531B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm */, + CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.mm */, + CC35CEC520DD87280006448D /* ASCollectionsTests.mm */, + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.mm */, + 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */, + CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.mm */, + 2911485B1A77147A005D0878 /* ASControlNodeTests.mm */, + 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */, + ACF6ED541B178DC700DA7C62 /* ASDimensionTests.mm */, + CC54A81D1D7008B300296A24 /* ASDispatchTests.mm */, + 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.mm */, + 058D0A2E195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm */, + F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.mm */, + DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.mm */, + 69B225661D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm */, + CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.mm */, + 058D0A2F195D057000B7D73C /* ASDisplayNodeTests.mm */, + 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */, + 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.mm */, + 697B31591CFE4B410049936F /* ASEditableTextNodeTests.mm */, + 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.mm */, + ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */, + CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.mm */, + 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm */, + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */, + E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.mm */, + ACF6ED571B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.h */, + ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.mm */, + 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.mm */, + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */, + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */, + 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.mm */, + 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm */, + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */, + CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.mm */, + ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */, + AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.mm */, + CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */, + CC8B05D51D73836400F54286 /* ASPerformanceTestContext.mm */, + CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.mm */, + ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */, + CCAA0B81206ADECB0057B336 /* ASRecursiveUnfairLockTests.mm */, + 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */, + 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */, + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.mm */, + 056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */, + 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */, + ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */, + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.mm */, + 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, + CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.mm */, + 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.mm */, + AE440174210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm */, + 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */, + 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, + C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm */, + CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.mm */, + 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.mm */, + 058D0A36195D057000B7D73C /* ASTextNodeTests.mm */, + 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */, + 9644CFDE2193777C00213478 /* ASThrashUtility.h */, + 9644CFDF2193777C00213478 /* ASThrashUtility.m */, + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */, + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */, + CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.mm */, + AEEC47E31C21D3D200EC1693 /* ASVideoNodeTests.mm */, + CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.mm */, + 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.mm */, + CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.mm */, + 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, + 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, + CC583ABF1EF9BAB400134156 /* Common */, + 058D09C6195D04C000B7D73C /* Supporting Files */, + 052EE06A1A15A0D8002C6279 /* TestResources */, + ); + path = Tests; + sourceTree = ""; + }; + 058D09C6195D04C000B7D73C /* Supporting Files */ = { + isa = PBXGroup; + children = ( + CC0F88691E4286FA00576FED /* ReferenceImages_64 */, + CC0F886A1E4286FA00576FED /* ReferenceImages_iOS_10 */, + 058D09C7195D04C000B7D73C /* AsyncDisplayKitTests-Info.plist */, + 058D09C8195D04C000B7D73C /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 058D09E1195D050800B7D73C /* Details */ = { + isa = PBXGroup; + children = ( + E5B077EB1E6843AF00C24B5B /* Collection Layout */, + 25B171EA1C12242700508A7A /* Data Controller */, + 058D09F7195D050800B7D73C /* Transactions */, + 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */, + 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.mm */, + CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */, + CC0F885D1E4280B800576FED /* _ASCollectionViewCell.mm */, + 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, + 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, + 058D09E4195D050800B7D73C /* _ASDisplayView.h */, + 058D09E5195D050800B7D73C /* _ASDisplayView.mm */, + 69CB62A91CB8165900024920 /* _ASDisplayViewAccessiblity.h */, + 69CB62AA1CB8165900024920 /* _ASDisplayViewAccessiblity.mm */, + 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */, + 205F0E181B37339C007741D0 /* ASAbstractLayoutController.mm */, + 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */, + 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */, + 299DA1A71A828D2900162D41 /* ASBatchContext.h */, + 299DA1A81A828D2900162D41 /* ASBatchContext.mm */, + E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */, + 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */, + 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */, + 68C215561DE10D330019C4BC /* ASCollectionViewLayoutInspector.h */, + 68C215571DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm */, + 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */, + 696F01EB1DD2AF450049FBD5 /* ASEventLog.mm */, + CCDC9B4B200991D10063C1F8 /* ASGraphicsContext.h */, + CCDC9B4C200991D10063C1F8 /* ASGraphicsContext.mm */, + E5B225271F1790B5001E1431 /* ASHashing.h */, + E5B225261F1790B5001E1431 /* ASHashing.mm */, + 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */, + 058D09E7195D050800B7D73C /* ASHighlightOverlayLayer.mm */, + 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */, + 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.mm */, + 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */, + CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */, + CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */, + 4640521D1A3F83C40061C0BA /* ASLayoutController.h */, + 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */, + 68EE0DBB1C1B4ED300BA1B99 /* ASMainSerialQueue.h */, + 68EE0DBC1C1B4ED300BA1B99 /* ASMainSerialQueue.mm */, + 058D09E8195D050800B7D73C /* ASMutableAttributedStringBuilder.h */, + 058D09E9195D050800B7D73C /* ASMutableAttributedStringBuilder.mm */, + 6907C2561DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h */, + 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.mm */, + CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */, + CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.mm */, + 68355B391CB57A5A001D4E68 /* ASPINRemoteImageDownloader.h */, + 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.mm */, + 055F1A3619ABD413004DAFF1 /* ASRangeController.h */, + 055F1A3719ABD413004DAFF1 /* ASRangeController.mm */, + 69F10C851C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h */, + CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */, + CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.mm */, + 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */, + 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */, + 296A0A311A951715005ACEAA /* ASScrollDirection.h */, + 205F0E111B371BD7007741D0 /* ASScrollDirection.mm */, + 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */, + 4640521C1A3F83C40061C0BA /* ASTableLayoutController.mm */, + 058D0A12195D050800B7D73C /* ASThread.h */, + CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */, + CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.mm */, + 9C70F2011CDA4EFA007D6C76 /* ASTraitCollection.h */, + 9C70F2021CDA4EFA007D6C76 /* ASTraitCollection.mm */, + 68B8A4DF1CBDB958007E4543 /* ASWeakProxy.h */, + 68B8A4E01CBDB958007E4543 /* ASWeakProxy.mm */, + CC3B20871C3F7A5400798563 /* ASWeakSet.h */, + CC3B20881C3F7A5400798563 /* ASWeakSet.mm */, + 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */, + DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */, + DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */, + CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */, + CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.mm */, + 058D09F5195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.h */, + 058D09F6195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.mm */, + 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */, + 205F0E0E1B371875007741D0 /* UICollectionViewLayout+ASConvenience.mm */, + 058D09FF195D050800B7D73C /* UIView+ASConvenience.h */, + ); + path = Details; + sourceTree = ""; + }; + 058D09F7195D050800B7D73C /* Transactions */ = { + isa = PBXGroup; + children = ( + 058D09F8195D050800B7D73C /* _ASAsyncTransaction.h */, + 058D09F9195D050800B7D73C /* _ASAsyncTransaction.mm */, + 058D09FB195D050800B7D73C /* _ASAsyncTransactionContainer.h */, + 058D09FC195D050800B7D73C /* _ASAsyncTransactionContainer.mm */, + 058D09FA195D050800B7D73C /* _ASAsyncTransactionContainer+Private.h */, + 058D09FD195D050800B7D73C /* _ASAsyncTransactionGroup.h */, + 058D09FE195D050800B7D73C /* _ASAsyncTransactionGroup.mm */, + ); + path = Transactions; + sourceTree = ""; + }; + 058D0A01195D050800B7D73C /* Private */ = { + isa = PBXGroup; + children = ( + CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, + E52F8AEE1EAE659600B5A912 /* Collection Layout */, + 6947B0BB1E36B4E30007C478 /* Layout */, + CCCCCCC11EC3EF060087FE10 /* TextExperiment */, + 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, + 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, + C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */, + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */, + 058D0A05195D050800B7D73C /* _ASPendingState.h */, + 058D0A06195D050800B7D73C /* _ASPendingState.mm */, + 058D0A07195D050800B7D73C /* _ASScopeTimer.h */, + DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */, + DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.mm */, + 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */, + 044285051BAA63FE00D16268 /* ASBatchFetching.h */, + 044285061BAA63FE00D16268 /* ASBatchFetching.mm */, + CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */, + CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */, + CC0F885A1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h */, + CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.mm */, + FA4FAF14200A850200E735BD /* ASControlNode+Private.h */, + 9F98C0231DBDF2A300476D92 /* ASControlTargetAction.h */, + 9F98C0241DBDF2A300476D92 /* ASControlTargetAction.mm */, + 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */, + 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.mm */, + AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */, + AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.mm */, + CC54A81B1D70077A00296A24 /* ASDispatch.h */, + E5B2252D1F17E521001E1431 /* ASDispatch.mm */, + 058D0A08195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm */, + 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */, + 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, + DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */, + 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */, + 690BC8BF20F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.h */, + 690BC8C020F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.mm */, + 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, + 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */, + CCA282C61E9EB64B0037E8B7 /* ASDisplayNodeTipState.h */, + CCA282C71E9EB64B0037E8B7 /* ASDisplayNodeTipState.mm */, + 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */, + 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, + 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.mm */, + 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */, + ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */, + ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.mm */, + CCA282C21E9EAE630037E8B7 /* ASLayerBackingTipProvider.h */, + CCA282C31E9EAE630037E8B7 /* ASLayerBackingTipProvider.mm */, + E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */, + E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */, + CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */, + CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */, + CCED5E402020D41600395C40 /* ASNetworkImageLoadInfo+Private.h */, + CC3B20811C3F76D600798563 /* ASPendingStateController.h */, + CC3B20821C3F76D600798563 /* ASPendingStateController.mm */, + CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */, + CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.mm */, + CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */, + CCA282BE1E9EAE010037E8B7 /* ASTip.h */, + CCA282BF1E9EAE010037E8B7 /* ASTip.mm */, + CCA282CA1E9EB73E0037E8B7 /* ASTipNode.h */, + CCA282CB1E9EB73E0037E8B7 /* ASTipNode.mm */, + CCA282BA1E9EABDD0037E8B7 /* ASTipProvider.h */, + CCA282BB1E9EABDD0037E8B7 /* ASTipProvider.mm */, + CCA282B21E9EA7310037E8B7 /* ASTipsController.h */, + CCA282B31E9EA7310037E8B7 /* ASTipsController.mm */, + CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */, + CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.mm */, + 0442850B1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h */, + 0442850C1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.mm */, + 83A7D9581D44542100BF333E /* ASWeakMap.h */, + 83A7D9591D44542100BF333E /* ASWeakMap.mm */, + ); + path = Private; + sourceTree = ""; + }; + 058D0A42195D058D00B7D73C /* Base */ = { + isa = PBXGroup; + children = ( + 058D0A43195D058D00B7D73C /* ASAssert.h */, + CCA5F62D1EECC2A80060C137 /* ASAssert.mm */, + 0516FA3A1A15563400B4EBED /* ASAvailability.h */, + 058D0A44195D058D00B7D73C /* ASBaseDefines.h */, + 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */, + 0516FA3B1A15563400B4EBED /* ASLog.h */, + CCB1F9591EFB60A5009C7475 /* ASLog.mm */, + CCB1F95B1EFB6316009C7475 /* ASSignpost.h */, + ); + path = Base; + sourceTree = ""; + }; + 257754661BED245B00737CA5 /* TextKit */ = { + isa = PBXGroup; + children = ( + B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */, + B30BF6511C5964B0004FCD53 /* ASLayoutManager.mm */, + 257754951BEE44CD00737CA5 /* ASTextKitAttributes.h */, + 257754941BEE44CD00737CA5 /* ASTextKitAttributes.mm */, + 257754BA1BEE458E00737CA5 /* ASTextKitComponents.h */, + 257754B71BEE458D00737CA5 /* ASTextKitComponents.mm */, + 257754961BEE44CD00737CA5 /* ASTextKitContext.h */, + 257754971BEE44CD00737CA5 /* ASTextKitContext.mm */, + 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */, + 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.mm */, + 257754981BEE44CD00737CA5 /* ASTextKitEntityAttribute.h */, + 257754991BEE44CD00737CA5 /* ASTextKitEntityAttribute.mm */, + A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */, + 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */, + 257754931BEE44CD00737CA5 /* ASTextKitRenderer.h */, + 2577549A1BEE44CD00737CA5 /* ASTextKitRenderer.mm */, + 2577549B1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.h */, + 2577549C1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.mm */, + 2577549D1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.h */, + 2577549E1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.mm */, + 2577549F1BEE44CD00737CA5 /* ASTextKitShadower.h */, + 257754A01BEE44CD00737CA5 /* ASTextKitShadower.mm */, + 257754A11BEE44CD00737CA5 /* ASTextKitTailTruncater.h */, + 257754A21BEE44CD00737CA5 /* ASTextKitTailTruncater.mm */, + 257754A31BEE44CD00737CA5 /* ASTextKitTruncating.h */, + 257754BC1BEE458E00737CA5 /* ASTextNodeTypes.h */, + 257754B91BEE458E00737CA5 /* ASTextNodeWordKerner.h */, + 257754BD1BEE458E00737CA5 /* ASTextNodeWordKerner.mm */, + ); + name = TextKit; + sourceTree = ""; + }; + 25B171EA1C12242700508A7A /* Data Controller */ = { + isa = PBXGroup; + children = ( + E5711A2A1C840C81009619D4 /* ASCollectionElement.h */, + E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */, + 464052191A3F83C40061C0BA /* ASDataController.h */, + 4640521A1A3F83C40061C0BA /* ASDataController.mm */, + DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */, + DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.mm */, + E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */, + E5B077FE1E69F4EB00C24B5B /* ASElementMap.mm */, + AC6145401D8AFAE8003D62A2 /* ASSection.h */, + AC6145421D8AFD4F003D62A2 /* ASSection.mm */, + ); + name = "Data Controller"; + sourceTree = ""; + }; + 690ED5911E36D118000627C0 /* tvOS */ = { + isa = PBXGroup; + children = ( + 690ED5931E36D118000627C0 /* ASControlNode+tvOS.mm */, + 690ED5951E36D118000627C0 /* ASImageNode+tvOS.mm */, + ); + path = tvOS; + sourceTree = ""; + }; + 6947B0BB1E36B4E30007C478 /* Layout */ = { + isa = PBXGroup; + children = ( + 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */, + 692BE8D61E36B65B00C86D87 /* ASLayoutSpecPrivate.h */, + 698DFF461E36B7E9002891F1 /* ASLayoutSpecUtilities.h */, + 698DFF431E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h */, + 6947B0C11E36B5040007C478 /* ASStackPositionedLayout.h */, + 6947B0C21E36B5040007C478 /* ASStackPositionedLayout.mm */, + 6947B0BC1E36B4E30007C478 /* ASStackUnpositionedLayout.h */, + 6947B0BD1E36B4E30007C478 /* ASStackUnpositionedLayout.mm */, + ); + path = Layout; + sourceTree = ""; + }; + AC6456051B0A333200CF11B8 /* Layout */ = { + isa = PBXGroup; + children = ( + 9C6BB3B01B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h */, + ACF6ED181B17843500DA7C62 /* ASAbsoluteLayoutSpec.h */, + ACF6ED191B17843500DA7C62 /* ASAbsoluteLayoutSpec.mm */, + 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */, + 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */, + ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */, + ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */, + ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */, + ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */, + 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */, + 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */, + ACF6ED071B17843500DA7C62 /* ASDimension.h */, + ACF6ED081B17843500DA7C62 /* ASDimension.mm */, + 690C35631E055C7B00069B91 /* ASDimensionInternal.h */, + 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */, + ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */, + ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */, + ACF6ED0B1B17843500DA7C62 /* ASLayout.h */, + ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */, + 0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */, + 0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */, + ACF6ED111B17843500DA7C62 /* ASLayoutElement.h */, + E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */, + 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */, + 9CDC18CB1B910E12004965E2 /* ASLayoutElementPrivate.h */, + ACF6ED0D1B17843500DA7C62 /* ASLayoutSpec.h */, + ACF6ED0E1B17843500DA7C62 /* ASLayoutSpec.mm */, + 6977965D1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h */, + 6977965E1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm */, + ACF6ED121B17843500DA7C62 /* ASOverlayLayoutSpec.h */, + ACF6ED131B17843500DA7C62 /* ASOverlayLayoutSpec.mm */, + ACF6ED141B17843500DA7C62 /* ASRatioLayoutSpec.h */, + ACF6ED151B17843500DA7C62 /* ASRatioLayoutSpec.mm */, + 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */, + 7A06A7381C35F08800FE8DAA /* ASRelativeLayoutSpec.mm */, + AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */, + 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */, + ACF6ED161B17843500DA7C62 /* ASStackLayoutSpec.h */, + ACF6ED171B17843500DA7C62 /* ASStackLayoutSpec.mm */, + 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */, + 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */, + ); + path = Layout; + sourceTree = ""; + }; + CC224E942066CA6D00BBA57F /* Schemas */ = { + isa = PBXGroup; + children = ( + CC224E952066CA6D00BBA57F /* configuration.json */, + ); + path = Schemas; + sourceTree = ""; + }; + CC583ABF1EF9BAB400134156 /* Common */ = { + isa = PBXGroup; + children = ( + CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.mm */, + CC583AC11EF9BAB400134156 /* ASTestCase.h */, + CC583AC21EF9BAB400134156 /* ASTestCase.mm */, + CC583AC31EF9BAB400134156 /* ASXCTExtensions.h */, + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */, + CC583AC41EF9BAB400134156 /* NSInvocation+ASTestHelpers.h */, + CC583AC51EF9BAB400134156 /* NSInvocation+ASTestHelpers.mm */, + CC583AC61EF9BAB400134156 /* OCMockObject+ASAdditions.h */, + CC583AC71EF9BAB400134156 /* OCMockObject+ASAdditions.mm */, + ); + path = Common; + sourceTree = ""; + }; + CCCCCCC11EC3EF060087FE10 /* TextExperiment */ = { + isa = PBXGroup; + children = ( + CCCCCCC21EC3EF060087FE10 /* Component */, + CCCCCCCB1EC3EF060087FE10 /* String */, + CCCCCCD01EC3EF060087FE10 /* Utility */, + ); + path = TextExperiment; + sourceTree = ""; + }; + CCCCCCC21EC3EF060087FE10 /* Component */ = { + isa = PBXGroup; + children = ( + CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */, + CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.mm */, + CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */, + CCCCCCC61EC3EF060087FE10 /* ASTextInput.mm */, + CCCCCCC71EC3EF060087FE10 /* ASTextLayout.h */, + CCCCCCC81EC3EF060087FE10 /* ASTextLayout.mm */, + CCCCCCC91EC3EF060087FE10 /* ASTextLine.h */, + CCCCCCCA1EC3EF060087FE10 /* ASTextLine.mm */, + ); + path = Component; + sourceTree = ""; + }; + CCCCCCCB1EC3EF060087FE10 /* String */ = { + isa = PBXGroup; + children = ( + CCCCCCCC1EC3EF060087FE10 /* ASTextAttribute.h */, + CCCCCCCD1EC3EF060087FE10 /* ASTextAttribute.mm */, + CCCCCCCE1EC3EF060087FE10 /* ASTextRunDelegate.h */, + CCCCCCCF1EC3EF060087FE10 /* ASTextRunDelegate.mm */, + ); + path = String; + sourceTree = ""; + }; + CCCCCCD01EC3EF060087FE10 /* Utility */ = { + isa = PBXGroup; + children = ( + CCCCCCD11EC3EF060087FE10 /* ASTextUtilities.h */, + CCCCCCD21EC3EF060087FE10 /* ASTextUtilities.mm */, + CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */, + CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.mm */, + CCCCCCD31EC3EF060087FE10 /* NSParagraphStyle+ASText.h */, + CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.mm */, + ); + path = Utility; + sourceTree = ""; + }; + CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */ = { + isa = PBXGroup; + children = ( + CCF92DCE1E315FC50019E9C6 /* IGListKit Support */, + CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */, + CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */, + ); + name = "Collection Data Adapter"; + sourceTree = ""; + }; + CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */ = { + isa = PBXGroup; + children = ( + CCBD05DF1E4147B000D18509 /* ASIGListAdapterBasedDataSource.h */, + CCBD05DE1E4147B000D18509 /* ASIGListAdapterBasedDataSource.mm */, + ); + name = "Collection Data Adapter"; + sourceTree = ""; + }; + CCF92DCE1E315FC50019E9C6 /* IGListKit Support */ = { + isa = PBXGroup; + children = ( + CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */, + CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.mm */, + CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */, + CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.mm */, + ); + name = "IGListKit Support"; + sourceTree = ""; + }; + DE89C1691DCEB9CC00D49D74 /* Debug */ = { + isa = PBXGroup; + children = ( + 764D83D21C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h */, + 764D83D31C8EA515009B4FB8 /* AsyncDisplayKit+Debug.mm */, + CCA282B61E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h */, + CCA282B71E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.mm */, + ); + path = Debug; + sourceTree = ""; + }; + E52F8AEE1EAE659600B5A912 /* Collection Layout */ = { + isa = PBXGroup; + children = ( + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */, + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm */, + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, + E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, + E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */, + E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */, + E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, + E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, + E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.mm */, + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, + ); + name = "Collection Layout"; + sourceTree = ""; + }; + E5B077EB1E6843AF00C24B5B /* Collection Layout */ = { + isa = PBXGroup; + children = ( + E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, + E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.mm */, + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */, + E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */, + E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, + E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, + E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, + E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */, + ); + name = "Collection Layout"; + sourceTree = ""; + }; + FD40E2760492F0CAAEAD552D /* Pods */ = { + isa = PBXGroup; + children = ( + FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */, + D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */, + BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + B35061D71B010EDF0018CF92 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */, + E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */, + E5B225281F1790D6001E1431 /* ASHashing.h in Headers */, + CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, + 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, + E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */, + E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, + CCBDDD0520C62A2D00CBA922 /* ASMainThreadDeallocation.h in Headers */, + CCAA0B7F206ADBF30057B336 /* ASRecursiveUnfairLock.h in Headers */, + E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, + 9D9AA56D21E2568500172C09 /* ASDisplayNode+LayoutSpec.h in Headers */, + E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */, + CCCCCCE31EC3EF060087FE10 /* NSParagraphStyle+ASText.h in Headers */, + E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */, + E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */, + 696F01EC1DD2AF450049FBD5 /* ASEventLog.h in Headers */, + 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */, + 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */, + CC18248C200D49C800875940 /* ASTextNodeCommon.h in Headers */, + 698371DB1E4379CD00437585 /* ASNodeController+Beta.h in Headers */, + 6907C2581DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h in Headers */, + 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */, + 698C8B621CAB49FC0052DC3F /* ASLayoutElementExtensibility.h in Headers */, + 69F10C871C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h in Headers */, + 0FAFDF7520EC1C90003A51C0 /* ASLayout+IGListKit.h in Headers */, + B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */, + 68355B411CB57A6C001D4E68 /* ASImageContainerProtocolCategories.h in Headers */, + 7630FFA81C9E267E007A7C0E /* ASVideoNode.h in Headers */, + B350623F1B010EFD0018CF92 /* _ASAsyncTransactionContainer.h in Headers */, + B13CA1011C52004900E031AB /* ASCollectionNode+Beta.h in Headers */, + 68C215581DE10D330019C4BC /* ASCollectionViewLayoutInspector.h in Headers */, + B35062411B010EFD0018CF92 /* _ASAsyncTransactionGroup.h in Headers */, + B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, + CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */, + B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */, + 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */, + 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */, + CCA282B81E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h in Headers */, + B35062571B010F070018CF92 /* ASAssert.h in Headers */, + CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */, + 9D302F9B2231B07E005739C3 /* ASButtonNode+Private.h in Headers */, + B35062581B010F070018CF92 /* ASAvailability.h in Headers */, + 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */, + DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */, + CC0F88621E4281E200576FED /* ASSectionController.h in Headers */, + CCB1F95C1EFB6350009C7475 /* ASSignpost.h in Headers */, + A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */, + C018DF21216BF26700181FDA /* ASAbstractLayoutController+FrameworkPrivate.h in Headers */, + 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */, + B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */, + B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */, + B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */, + B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */, + 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, + CC55A7111E52A0F200594372 /* ASResponderChainEnumerator.h in Headers */, + 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, + B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */, + ACE87A2C1D73696800D7FF06 /* ASSectionContext.h in Headers */, + 509E68631B3AEDB4009B9150 /* ASCollectionViewLayoutController.h in Headers */, + B35061F71B010EFD0018CF92 /* ASCollectionViewProtocols.h in Headers */, + 68FC85E31CE29B7E00EDD713 /* ASTabBarController.h in Headers */, + CC56013B1F06E9A700DC4FBE /* ASIntegerMap.h in Headers */, + B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */, + E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */, + CCF1FF5E20C4785000AAD8FC /* ASLocking.h in Headers */, + B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */, + B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, + 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, + 68FC85EA1CE29C7D00EDD713 /* ASVisibilityProtocols.h in Headers */, + CCCCCCD91EC3EF060087FE10 /* ASTextLayout.h in Headers */, + A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */, + 9C70F2061CDA4F0C007D6C76 /* ASTraitCollection.h in Headers */, + CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */, + 8021EC1D1D2B00B100799119 /* UIImage+ASConvenience.h in Headers */, + B35061FD1B010EFD0018CF92 /* ASDisplayNode+Subclasses.h in Headers */, + CCEDDDCD200C2CB900FFCD0A /* ASConfiguration.h in Headers */, + B35061FB1B010EFD0018CF92 /* ASDisplayNode.h in Headers */, + B35061FE1B010EFD0018CF92 /* ASDisplayNodeExtras.h in Headers */, + CC0F88601E4280B800576FED /* _ASCollectionViewCell.h in Headers */, + B35062001B010EFD0018CF92 /* ASEditableTextNode.h in Headers */, + 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */, + B350621B1B010EFD0018CF92 /* ASTableLayoutController.h in Headers */, + B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, + C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */, + 7AB338671C55B3460055FDE8 /* ASRelativeLayoutSpec.h in Headers */, + B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */, + B350621F1B010EFD0018CF92 /* ASImageProtocols.h in Headers */, + 34EFC75F1B701C8600AD841F /* ASInsetLayoutSpec.h in Headers */, + CCEDDDCA200C2AC300FFCD0A /* ASConfigurationInternal.h in Headers */, + 34EFC7671B701CD900AD841F /* ASLayout.h in Headers */, + DBDB83951C6E879900D0098C /* ASPagerFlowLayout.h in Headers */, + 34EFC7691B701CE100AD841F /* ASLayoutElement.h in Headers */, + 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */, + 9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */, + 9D302F9E2231B373005739C3 /* ASButtonNode+Yoga.h in Headers */, + CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */, + CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */, + 254C6B7E1BF94DF4003EC431 /* ASTextKitTailTruncater.h in Headers */, + B35062491B010EFD0018CF92 /* _ASCoreAnimationExtras.h in Headers */, + 683F563720E409D700CEB7A3 /* ASDisplayNode+InterfaceState.h in Headers */, + 690BC8C120F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.h in Headers */, + 68EE0DBE1C1B4ED300BA1B99 /* ASMainSerialQueue.h in Headers */, + CCCCCCE11EC3EF060087FE10 /* ASTextUtilities.h in Headers */, + B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */, + CCDC9B4D200991D10063C1F8 /* ASGraphicsContext.h in Headers */, + E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */, + CC54A81C1D70079800296A24 /* ASDispatch.h in Headers */, + B350624D1B010EFD0018CF92 /* _ASScopeTimer.h in Headers */, + CC0F88631E4281E700576FED /* ASSupplementaryNodeSource.h in Headers */, + 254C6B771BF94DF4003EC431 /* ASTextKitAttributes.h in Headers */, + 254C6B7D1BF94DF4003EC431 /* ASTextKitShadower.h in Headers */, + 690ED58E1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h in Headers */, + CC55A70D1E529FA200594372 /* UIResponder+AsyncDisplayKit.h in Headers */, + 254C6B731BF94DF4003EC431 /* ASTextKitCoreTextAdditions.h in Headers */, + 254C6B7A1BF94DF4003EC431 /* ASTextKitRenderer.h in Headers */, + 69CB62AC1CB8165900024920 /* _ASDisplayViewAccessiblity.h in Headers */, + 254C6B7C1BF94DF4003EC431 /* ASTextKitRenderer+TextChecking.h in Headers */, + 68AF37DB1CBEF4D80077BF76 /* ASImageNode+AnimatedImagePrivate.h in Headers */, + CCCCCCDD1EC3EF060087FE10 /* ASTextAttribute.h in Headers */, + B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */, + 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */, + AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, + CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */, + E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, + E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */, + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, + E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, + 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, + 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */, + CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */, + DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */, + 9C70F20F1CDBE9FF007D6C76 /* ASLayoutManager.h in Headers */, + 6947B0C31E36B5040007C478 /* ASStackPositionedLayout.h in Headers */, + DBABFAFC1C6A8D2F0039EA4A /* _ASTransitionContext.h in Headers */, + FA4FAF15200A850200E735BD /* ASControlNode+Private.h in Headers */, + B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */, + CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */, + B35062521B010EFD0018CF92 /* ASDisplayNodeInternal.h in Headers */, + AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, + B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, + E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */, + 254C6B7F1BF94DF4003EC431 /* ASTextKitTruncating.h in Headers */, + 9D9AA56B21E254B800172C09 /* ASDisplayNode+Yoga.h in Headers */, + CC58AA4B1E398E1D002C8CB4 /* ASBlockTypes.h in Headers */, + CCA282BC1E9EABDD0037E8B7 /* ASTipProvider.h in Headers */, + 6977965F1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h in Headers */, + 692BE8D71E36B65B00C86D87 /* ASLayoutSpecPrivate.h in Headers */, + 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */, + DEC146B71C37A16A004A0EE7 /* ASCollectionInternal.h in Headers */, + 68B8A4E21CBDB958007E4543 /* ASWeakProxy.h in Headers */, + 9F98C0271DBE29FC00476D92 /* ASControlTargetAction.h in Headers */, + CC84C7F220474C5300A3851B /* ASCGImageBuffer.h in Headers */, + 695943401D70815300B0EE1F /* ASDisplayNodeLayout.h in Headers */, + 0442850E1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h in Headers */, + DE8BEAC21C2DF3FC00D57C12 /* ASDelegateProxy.h in Headers */, + B350623E1B010EFD0018CF92 /* _ASAsyncTransactionContainer+Private.h in Headers */, + AC6145411D8AFAE8003D62A2 /* ASSection.h in Headers */, + 8BBBAB8C1CEBAF1700107FC6 /* ASDefaultPlaybackButton.h in Headers */, + 254C6B741BF94DF4003EC431 /* ASTextNodeWordKerner.h in Headers */, + 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */, + CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */, + 83A7D95C1D44548100BF333E /* ASWeakMap.h in Headers */, + E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */, + 6947B0BE1E36B4E30007C478 /* ASStackUnpositionedLayout.h in Headers */, + CC4C2A771D88E3BF0039ACAB /* ASTraceEvent.h in Headers */, + 254C6B7B1BF94DF4003EC431 /* ASTextKitRenderer+Positioning.h in Headers */, + DE4843DC1C93EAC100A1F33B /* ASLayoutTransition.h in Headers */, + CC57EAF81E3939450034C595 /* ASTableView+Undeprecated.h in Headers */, + 254C6B781BF94DF4003EC431 /* ASTextKitContext.h in Headers */, + CCED5E412020D49D00395C40 /* ASNetworkImageLoadInfo+Private.h in Headers */, + 9CDC18CD1B910E12004965E2 /* ASLayoutElementPrivate.h in Headers */, + B35062201B010EFD0018CF92 /* ASLayoutController.h in Headers */, + B35062211B010EFD0018CF92 /* ASLayoutRangeType.h in Headers */, + CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */, + 34EFC76A1B701CE600AD841F /* ASLayoutSpec.h in Headers */, + CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */, + B350625C1B010F070018CF92 /* ASLog.h in Headers */, + CC3B208A1C3F7A5400798563 /* ASWeakSet.h in Headers */, + B35062041B010EFD0018CF92 /* ASMultiplexImageNode.h in Headers */, + DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */, + B35062241B010EFD0018CF92 /* ASMutableAttributedStringBuilder.h in Headers */, + B13CA0F81C519EBA00E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */, + 909C4C751F09C98B00D6B76F /* ASTextNode2.h in Headers */, + B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */, + CCA282C81E9EB64B0037E8B7 /* ASDisplayNodeTipState.h in Headers */, + 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */, + B35062261B010EFD0018CF92 /* ASRangeController.h in Headers */, + 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */, + DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */, + CCA282C41E9EAE630037E8B7 /* ASLayerBackingTipProvider.h in Headers */, + CCEDDDCF200C42A200FFCD0A /* ASConfigurationDelegate.h in Headers */, + E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */, + 6900C5F41E8072DA00BCD75C /* ASImageNode+Private.h in Headers */, + 68B0277B1C1A79D60041016B /* ASDisplayNode+Beta.h in Headers */, + B350622D1B010EFD0018CF92 /* ASScrollDirection.h in Headers */, + 254C6B751BF94DF4003EC431 /* ASTextKitComponents.h in Headers */, + B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */, + CCA282CC1E9EB73E0037E8B7 /* ASTipNode.h in Headers */, + 25E327571C16819500A2170C /* ASPagerNode.h in Headers */, + CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, + 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, + CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */, + CC35CEC320DD7F600006448D /* ASCollections.h in Headers */, + CC7AF196200D9BD500A21BDE /* ASExperimentalFeatures.h in Headers */, + CCCCCCDF1EC3EF060087FE10 /* ASTextRunDelegate.h in Headers */, + 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */, + 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, + CC0F885C1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h in Headers */, + 764D83D51C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h in Headers */, + CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, + 254C6B761BF94DF4003EC431 /* ASTextNodeTypes.h in Headers */, + CCA282B41E9EA7310037E8B7 /* ASTipsController.h in Headers */, + 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, + CCA282C01E9EAE010037E8B7 /* ASTip.h in Headers */, + 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */, + 92DD2FE81BF4D0A80074C9DD /* ASMapNode.h in Headers */, + 9C6BB3B31B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h in Headers */, + 34EFC7731B701D0700AD841F /* ASAbsoluteLayoutSpec.h in Headers */, + B350620A1B010EFD0018CF92 /* ASTableView.h in Headers */, + B350620C1B010EFD0018CF92 /* ASTableViewProtocols.h in Headers */, + B350620D1B010EFD0018CF92 /* ASTextNode.h in Headers */, + B35062391B010EFD0018CF92 /* ASThread.h in Headers */, + 2C107F5B1BA9F54500F13DE5 /* AsyncDisplayKit.h in Headers */, + 509E68651B3AEDC5009B9150 /* CoreGraphics+ASConvenience.h in Headers */, + CCED5E3E2020D36800395C40 /* ASNetworkImageLoadInfo.h in Headers */, + B350623A1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.h in Headers */, + 044284FF1BAA3BD600D16268 /* UICollectionViewLayout+ASConvenience.h in Headers */, + B35062431B010EFD0018CF92 /* UIView+ASConvenience.h in Headers */, + 8BDA5FC71CDBDF91007D13B2 /* ASVideoPlayerNode.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 057D02BE1AC0A66700C7AC3C /* AsyncDisplayKitTestHost */ = { + isa = PBXNativeTarget; + buildConfigurationList = 057D02E31AC0A66800C7AC3C /* Build configuration list for PBXNativeTarget "AsyncDisplayKitTestHost" */; + buildPhases = ( + 057D02BB1AC0A66700C7AC3C /* Sources */, + 057D02BC1AC0A66700C7AC3C /* Frameworks */, + 057D02BD1AC0A66700C7AC3C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AsyncDisplayKitTestHost; + productName = AsyncDisplayKitTestHost; + productReference = 057D02BF1AC0A66700C7AC3C /* AsyncDisplayKitTestHost.app */; + productType = "com.apple.product-type.application"; + }; + 058D09BB195D04C000B7D73C /* AsyncDisplayKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058D09D2195D04C000B7D73C /* Build configuration list for PBXNativeTarget "AsyncDisplayKitTests" */; + buildPhases = ( + 2E61B6A0DB0F436A9DDBE86F /* [CP] Check Pods Manifest.lock */, + 058D09B8195D04C000B7D73C /* Sources */, + 058D09B9195D04C000B7D73C /* Frameworks */, + 058D09BA195D04C000B7D73C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 057D02E61AC0A67000C7AC3C /* PBXTargetDependency */, + ); + name = AsyncDisplayKitTests; + productName = AsyncDisplayKitTests; + productReference = 058D09BC195D04C000B7D73C /* AsyncDisplayKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + B35061D91B010EDF0018CF92 /* AsyncDisplayKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = B35061ED1B010EDF0018CF92 /* Build configuration list for PBXNativeTarget "AsyncDisplayKit" */; + buildPhases = ( + B35061D51B010EDF0018CF92 /* Sources */, + B35061D61B010EDF0018CF92 /* Frameworks */, + B35061D71B010EDF0018CF92 /* Headers */, + B35061D81B010EDF0018CF92 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AsyncDisplayKit; + productName = AsyncDisplayKit; + productReference = B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 058D09A4195D04C000B7D73C /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = AS; + LastUpgradeCheck = 0940; + ORGANIZATIONNAME = Pinterest; + TargetAttributes = { + 057D02BE1AC0A66700C7AC3C = { + CreatedOnToolsVersion = 6.2; + }; + 058D09BB195D04C000B7D73C = { + TestTargetID = 057D02BE1AC0A66700C7AC3C; + }; + B35061D91B010EDF0018CF92 = { + CreatedOnToolsVersion = 6.3.1; + DevelopmentTeam = X834Q8SBVP; + DevelopmentTeamName = "TELEGRAM MESSENGER LLP"; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 058D09A7195D04C000B7D73C /* Build configuration list for PBXProject "AsyncDisplayKit_Xcode" */; + compatibilityVersion = "Xcode 6.3"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 058D09A3195D04C000B7D73C; + productRefGroup = 058D09AD195D04C000B7D73C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B35061D91B010EDF0018CF92 /* AsyncDisplayKit */, + 058D09BB195D04C000B7D73C /* AsyncDisplayKitTests */, + 057D02BE1AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 057D02BD1AC0A66700C7AC3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 692510141E74FB44003F2DD0 /* Default-568h@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058D09BA195D04C000B7D73C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CC0F886C1E4286FA00576FED /* ReferenceImages_64 in Resources */, + CC0F886D1E4286FA00576FED /* ReferenceImages_iOS_10 in Resources */, + 052EE06B1A15A0D8002C6279 /* TestResources in Resources */, + 058D09CA195D04C000B7D73C /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B35061D81B010EDF0018CF92 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CC224E962066CA6D00BBA57F /* configuration.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2E61B6A0DB0F436A9DDBE86F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-AsyncDisplayKitTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 057D02BB1AC0A66700C7AC3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 057D02C71AC0A66700C7AC3C /* AppDelegate.mm in Sources */, + 057D02C41AC0A66700C7AC3C /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058D09B8195D04C000B7D73C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.mm in Sources */, + AE440175210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm in Sources */, + E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.mm in Sources */, + 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm in Sources */, + CC583AD71EF9BDC100134156 /* NSInvocation+ASTestHelpers.mm in Sources */, + CC051F1F1D7A286A006434CB /* ASCALayerTests.mm in Sources */, + 242995D31B29743C00090100 /* ASBasicImageDownloaderTests.mm in Sources */, + 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.mm in Sources */, + ACF6ED5C1B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm in Sources */, + 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */, + 2911485C1A77147A005D0878 /* ASControlNodeTests.mm in Sources */, + CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.mm in Sources */, + F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.mm in Sources */, + BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.mm in Sources */, + ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, + BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.mm in Sources */, + 695BE2551DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm in Sources */, + CCA221D31D6FA7EF00AF6A0F /* ASViewControllerTests.mm in Sources */, + 058D0A38195D057000B7D73C /* ASDisplayLayerTests.mm in Sources */, + 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.mm in Sources */, + CC583AD61EF9BDBE00134156 /* ASTestCase.mm in Sources */, + 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm in Sources */, + CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.mm in Sources */, + AE6987C11DD04E1000B9E458 /* ASPagerNodeTests.mm in Sources */, + 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.mm in Sources */, + 9644CFE02193777C00213478 /* ASThrashUtility.m in Sources */, + 696FCB311D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm in Sources */, + CC583AD81EF9BDC300134156 /* OCMockObject+ASAdditions.mm in Sources */, + 69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm in Sources */, + 4E9127691F64157600499623 /* ASRunLoopQueueTests.mm in Sources */, + CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.mm in Sources */, + CC54A81E1D7008B300296A24 /* ASDispatchTests.mm in Sources */, + F3F698D2211CAD4600800CB1 /* ASDisplayViewAccessibilityTests.mm in Sources */, + CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.mm in Sources */, + 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.mm in Sources */, + 83A7D95E1D446A6E00BF333E /* ASWeakMapTests.mm in Sources */, + 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.mm in Sources */, + AC026B581BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.mm in Sources */, + ACF6ED5E1B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm in Sources */, + ACF6ED601B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.mm in Sources */, + CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.mm in Sources */, + 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.mm in Sources */, + 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm in Sources */, + F325E48C21745F9E00AC93A4 /* ASButtonNodeTests.mm in Sources */, + 9692B4FF219E12370060C2C3 /* ASCollectionViewThrashTests.mm in Sources */, + E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.mm in Sources */, + CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.mm in Sources */, + CC583AD91EF9BDC600134156 /* ASDisplayNode+OCMock.mm in Sources */, + 697B315A1CFE4B410049936F /* ASEditableTextNodeTests.mm in Sources */, + ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */, + CC8B05D61D73836400F54286 /* ASPerformanceTestContext.mm in Sources */, + CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.mm in Sources */, + CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */, + 69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */, + C057D9BD20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm in Sources */, + ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */, + 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */, + CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.mm in Sources */, + 1A6C00111FAB4EDD00D05926 /* ASCornerLayoutSpecSnapshotTests.mm in Sources */, + 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */, + 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.mm in Sources */, + CC35CEC620DD87280006448D /* ASCollectionsTests.mm in Sources */, + ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, + CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */, + CCAA0B82206ADECB0057B336 /* ASRecursiveUnfairLockTests.mm in Sources */, + 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.mm in Sources */, + 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */, + AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.mm in Sources */, + 254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */, + F325E490217460B100AC93A4 /* ASTextNode2Tests.mm in Sources */, + 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.mm in Sources */, + CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */, + CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */, + 058D0A40195D057000B7D73C /* ASTextNodeTests.mm in Sources */, + DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.mm in Sources */, + 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */, + DBC452DE1C5C6A6A00B16017 /* ArrayDiffingTests.mm in Sources */, + CC11F97A1DB181180024D77B /* ASNetworkImageNodeTests.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B35061D51B010EDF0018CF92 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E5B225291F1790EE001E1431 /* ASHashing.mm in Sources */, + DEB8ED7C1DD003D300DBDE55 /* ASLayoutTransition.mm in Sources */, + CCA5F62E1EECC2A80060C137 /* ASAssert.mm in Sources */, + 9F98C0261DBE29E000476D92 /* ASControlTargetAction.mm in Sources */, + 9C70F2091CDABA36007D6C76 /* ASViewController.mm in Sources */, + 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.mm in Sources */, + CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.mm in Sources */, + CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.mm in Sources */, + 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.mm in Sources */, + B30BF6541C59D889004FCD53 /* ASLayoutManager.mm in Sources */, + 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, + CCA282B91E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.mm in Sources */, + 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.mm in Sources */, + B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.mm in Sources */, + 6947B0C51E36B5040007C478 /* ASStackPositionedLayout.mm in Sources */, + B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.mm in Sources */, + AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */, + B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.mm in Sources */, + CCA282BD1E9EABDD0037E8B7 /* ASTipProvider.mm in Sources */, + 9019FBC01ED8061D00C45F72 /* ASYogaUtilities.mm in Sources */, + B350624A1B010EFD0018CF92 /* _ASCoreAnimationExtras.mm in Sources */, + 68EE0DC01C1B4ED300BA1B99 /* ASMainSerialQueue.mm in Sources */, + B35062101B010EFD0018CF92 /* _ASDisplayLayer.mm in Sources */, + 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.mm in Sources */, + B35062121B010EFD0018CF92 /* _ASDisplayView.mm in Sources */, + DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */, + CCA282C11E9EAE010037E8B7 /* ASTip.mm in Sources */, + B350624C1B010EFD0018CF92 /* _ASPendingState.mm in Sources */, + 698371DC1E4379CD00437585 /* ASNodeController+Beta.mm in Sources */, + CC6AA2DB1E9F03B900978E87 /* ASDisplayNode+Ancestry.mm in Sources */, + 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, + 254C6B861BF94F8A003EC431 /* ASTextKitContext.mm in Sources */, + DBDB83971C6E879900D0098C /* ASPagerFlowLayout.mm in Sources */, + E5B078001E69F4EB00C24B5B /* ASElementMap.mm in Sources */, + 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, + 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.mm in Sources */, + 0FAFDF7620EC1C90003A51C0 /* ASLayout+IGListKit.mm in Sources */, + CCDC9B4E200991D10063C1F8 /* ASGraphicsContext.mm in Sources */, + CCCCCCD81EC3EF060087FE10 /* ASTextInput.mm in Sources */, + 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, + 9D9AA56921E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm in Sources */, + DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.mm in Sources */, + B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, + B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, + AC47D9421B3B891B00AAEE9D /* ASCellNode.mm in Sources */, + E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.mm in Sources */, + 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */, + 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */, + E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */, + 68FC85EC1CE29C7D00EDD713 /* ASVisibilityProtocols.mm in Sources */, + CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.mm in Sources */, + CCED5E3F2020D36800395C40 /* ASNetworkImageLoadInfo.mm in Sources */, + 68B8A4E41CBDB958007E4543 /* ASWeakProxy.mm in Sources */, + E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */, + 9C70F20A1CDBE949007D6C76 /* ASTableNode.mm in Sources */, + 69CB62AE1CB8165900024920 /* _ASDisplayViewAccessiblity.mm in Sources */, + B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */, + CCA282C51E9EAE630037E8B7 /* ASLayerBackingTipProvider.mm in Sources */, + 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.mm in Sources */, + B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */, + 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.mm in Sources */, + CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.mm in Sources */, + CCBDDD0620C62A2D00CBA922 /* ASMainThreadDeallocation.mm in Sources */, + B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */, + CCB1F95A1EFB60A5009C7475 /* ASLog.mm in Sources */, + 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.mm in Sources */, + CCEDDDCB200C2AC300FFCD0A /* ASConfigurationInternal.mm in Sources */, + 9D302F9F2231B373005739C3 /* ASButtonNode+Yoga.mm in Sources */, + CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.mm in Sources */, + 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, + B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, + E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm in Sources */, + 25E327591C16819500A2170C /* ASPagerNode.mm in Sources */, + 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */, + B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, + 254C6B891BF94F8A003EC431 /* ASTextKitRenderer+Positioning.mm in Sources */, + 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */, + E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */, + B35062511B010EFD0018CF92 /* ASDisplayNode+UIViewBridge.mm in Sources */, + E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */, + B35061FC1B010EFD0018CF92 /* ASDisplayNode.mm in Sources */, + B35061FF1B010EFD0018CF92 /* ASDisplayNodeExtras.mm in Sources */, + B35062011B010EFD0018CF92 /* ASEditableTextNode.mm in Sources */, + 254C6B881BF94F8A003EC431 /* ASTextKitRenderer.mm in Sources */, + CC3B208C1C3F7A5400798563 /* ASWeakSet.mm in Sources */, + B350621C1B010EFD0018CF92 /* ASTableLayoutController.mm in Sources */, + B350621E1B010EFD0018CF92 /* ASHighlightOverlayLayer.mm in Sources */, + 9CC606651D24DF9E006581A0 /* NSIndexSet+ASHelpers.mm in Sources */, + CC0F885F1E4280B800576FED /* _ASCollectionViewCell.mm in Sources */, + CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.mm in Sources */, + B35062541B010EFD0018CF92 /* ASImageNode+CGExtras.mm in Sources */, + E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */, + 6947B0C01E36B4E30007C478 /* ASStackUnpositionedLayout.mm in Sources */, + 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.mm in Sources */, + E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.mm in Sources */, + B35062031B010EFD0018CF92 /* ASImageNode.mm in Sources */, + 690BC8C220F6D3490052A434 /* ASDisplayNodeCornerLayerDelegate.mm in Sources */, + 254C6B821BF94F8A003EC431 /* ASTextKitComponents.mm in Sources */, + 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */, + AC6145441D8AFD4F003D62A2 /* ASSection.mm in Sources */, + E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */, + 34EFC75E1B701BF000AD841F /* ASInternalHelpers.mm in Sources */, + 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */, + DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, + CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.mm in Sources */, + CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.mm in Sources */, + CCEDDDD1200C488000FFCD0A /* ASConfiguration.mm in Sources */, + 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.mm in Sources */, + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, + 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, + CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, + 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, + 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.mm in Sources */, + B35062051B010EFD0018CF92 /* ASMultiplexImageNode.mm in Sources */, + B35062251B010EFD0018CF92 /* ASMutableAttributedStringBuilder.mm in Sources */, + B35062071B010EFD0018CF92 /* ASNetworkImageNode.mm in Sources */, + 34EFC76D1B701CF100AD841F /* ASOverlayLayoutSpec.mm in Sources */, + 044285101BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.mm in Sources */, + CCCCCCDE1EC3EF060087FE10 /* ASTextAttribute.mm in Sources */, + CCA282B51E9EA7310037E8B7 /* ASTipsController.mm in Sources */, + B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, + 0442850A1BAA63FE00D16268 /* ASBatchFetching.mm in Sources */, + CC35CEC420DD7F600006448D /* ASCollections.mm in Sources */, + 68FC85E61CE29B9400EDD713 /* ASNavigationController.mm in Sources */, + CC4C2A791D88E3BF0039ACAB /* ASTraceEvent.mm in Sources */, + 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, + 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */, + 254C6B851BF94F8A003EC431 /* ASTextKitAttributes.mm in Sources */, + 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */, + CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.mm in Sources */, + 509E68601B3AED8E009B9150 /* ASScrollDirection.mm in Sources */, + B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, + 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */, + 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, + E54E81FD1EB357BD00FFE8E1 /* ASPageTable.mm in Sources */, + 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, + 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */, + CC7AF198200DAB2200A21BDE /* ASExperimentalFeatures.mm in Sources */, + E5B2252E1F17E521001E1431 /* ASDispatch.mm in Sources */, + 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */, + 9C70F2051CDA4F06007D6C76 /* ASTraitCollection.mm in Sources */, + 83A7D95B1D44547700BF333E /* ASWeakMap.mm in Sources */, + CC034A0A1E60BEB400626263 /* ASDisplayNode+Convenience.mm in Sources */, + E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.mm in Sources */, + DE84918E1C8FFF9F003D89E9 /* ASRunLoopQueue.mm in Sources */, + 68FC85E51CE29B7E00EDD713 /* ASTabBarController.mm in Sources */, + CCCCCCDC1EC3EF060087FE10 /* ASTextLine.mm in Sources */, + 34EFC7741B701D0A00AD841F /* ASAbsoluteLayoutSpec.mm in Sources */, + 1A6C000E1FAB4E2100D05926 /* ASCornerLayoutSpec.mm in Sources */, + CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.mm in Sources */, + 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */, + 909C4C761F09C98B00D6B76F /* ASTextNode2.mm in Sources */, + 68C2155A1DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm in Sources */, + DB78412E1C6BCE1600A9E2B4 /* _ASTransitionContext.mm in Sources */, + B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */, + B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */, + 68355B3E1CB57A60001D4E68 /* ASPINRemoteImageDownloader.mm in Sources */, + CC034A141E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.mm in Sources */, + 254C6B871BF94F8A003EC431 /* ASTextKitEntityAttribute.mm in Sources */, + 34566CB31BC1213700715E6B /* ASPhotosFrameworkImageRequest.mm in Sources */, + 254C6B831BF94F8A003EC431 /* ASTextKitCoreTextAdditions.mm in Sources */, + CCCCCCE21EC3EF060087FE10 /* ASTextUtilities.mm in Sources */, + CC55A70E1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm in Sources */, + CC56013C1F06E9A700DC4FBE /* ASIntegerMap.mm in Sources */, + 697796611D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm in Sources */, + B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.mm in Sources */, + CCA282CD1E9EB73E0037E8B7 /* ASTipNode.mm in Sources */, + CC84C7F320474C5300A3851B /* ASCGImageBuffer.mm in Sources */, + 044284FD1BAA365100D16268 /* UICollectionViewLayout+ASConvenience.mm in Sources */, + CC0F885B1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.mm in Sources */, + 690ED5981E36D118000627C0 /* ASControlNode+tvOS.mm in Sources */, + 254C6B8A1BF94F8A003EC431 /* ASTextKitRenderer+TextChecking.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 057D02E61AC0A67000C7AC3C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 057D02BE1AC0A66700C7AC3C /* AsyncDisplayKitTestHost */; + targetProxy = 057D02E51AC0A67000C7AC3C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 058D09C8195D04C000B7D73C /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 058D09C9195D04C000B7D73C /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 057D02DF1AC0A66800C7AC3C /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = DebugHockeyapp; + }; + 057D02E01AC0A66800C7AC3C /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = ReleaseAppStore; + }; + 058D09CD195D04C000B7D73C /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugHockeyapp; + }; + 058D09CE195D04C000B7D73C /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseAppStore; + }; + 058D09D3195D04C000B7D73C /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + INFOPLIST_FILE = "Tests/AsyncDisplayKitTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AsyncDisplayKitTestHost.app/AsyncDisplayKitTestHost"; + WRAPPER_EXTENSION = xctest; + }; + name = DebugHockeyapp; + }; + 058D09D4195D04C000B7D73C /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + INFOPLIST_FILE = "Tests/AsyncDisplayKitTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AsyncDisplayKitTestHost.app/AsyncDisplayKitTestHost"; + WRAPPER_EXTENSION = xctest; + }; + name = ReleaseAppStore; + }; + B35061EE1B010EDF0018CF92 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = YES; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + B35061EF1B010EDF0018CF92 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D021D4F0219CB1920064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugFork; + }; + D021D4F1219CB1920064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = YES; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugFork; + }; + D021D4F2219CB1920064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + INFOPLIST_FILE = "Tests/AsyncDisplayKitTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AsyncDisplayKitTestHost.app/AsyncDisplayKitTestHost"; + WRAPPER_EXTENSION = xctest; + }; + name = DebugFork; + }; + D021D4F3219CB1920064BEBA /* DebugFork */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = DebugFork; + }; + D063755A1E67124400F00C9A /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = AsyncDisplayKitTests; + }; + name = ReleaseHockeyapp; + }; + D079FD051F06BD7A0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugAppStore; + }; + D079FD061F06BD7A0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_CODE_COVERAGE = NO; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = YES; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D079FD071F06BD7A0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + INFOPLIST_FILE = "Tests/AsyncDisplayKitTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AsyncDisplayKitTestHost.app/AsyncDisplayKitTestHost"; + WRAPPER_EXTENSION = xctest; + }; + name = DebugAppStore; + }; + D079FD081F06BD7A0038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = DebugAppStore; + }; + D086A5691CC0115200F08284 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_CODE_COVERAGE = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseHockeyapp; + }; + D086A56C1CC0115200F08284 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = ReleaseHockeyapp; + }; + D086A56D1CC0115200F08284 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0924FD01FE52BD8003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_CODE_COVERAGE = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseHockeyappInternal; + }; + D0924FD11FE52BD8003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyappInternal; + }; + D0924FD21FE52BD8003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = AsyncDisplayKitTests; + }; + name = ReleaseHockeyappInternal; + }; + D0924FD31FE52BD8003F693F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = ReleaseHockeyappInternal; + }; + D0ADF91C212B3AB400310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugAppStoreLLC; + }; + D0ADF91D212B3AB400310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_CODE_COVERAGE = NO; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = YES; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStoreLLC; + }; + D0ADF91E212B3AB400310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + INFOPLIST_FILE = "Tests/AsyncDisplayKitTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AsyncDisplayKitTestHost.app/AsyncDisplayKitTestHost"; + WRAPPER_EXTENSION = xctest; + }; + name = DebugAppStoreLLC; + }; + D0ADF91F212B3AB400310BBC /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = DebugAppStoreLLC; + }; + D0CE6EDD213DB54100BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseAppStoreLLC; + }; + D0CE6EDE213DB54100BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = Source/AsyncDisplayKit.modulemap; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "-DMINIMAL_ASDK=1", + "-Wundef", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = AsyncDisplayKit; + PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStoreLLC; + }; + D0CE6EDF213DB54100BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + INFOPLIST_FILE = "Tests/AsyncDisplayKitTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AsyncDisplayKitTestHost.app/AsyncDisplayKitTestHost"; + WRAPPER_EXTENSION = xctest; + }; + name = ReleaseAppStoreLLC; + }; + D0CE6EE0213DB54100BCD44B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + INFOPLIST_FILE = Tests/TestHost/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = ReleaseAppStoreLLC; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 057D02E31AC0A66800C7AC3C /* Build configuration list for PBXNativeTarget "AsyncDisplayKitTestHost" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 057D02DF1AC0A66800C7AC3C /* DebugHockeyapp */, + D021D4F3219CB1920064BEBA /* DebugFork */, + D079FD081F06BD7A0038FADE /* DebugAppStore */, + D0ADF91F212B3AB400310BBC /* DebugAppStoreLLC */, + 057D02E01AC0A66800C7AC3C /* ReleaseAppStore */, + D0CE6EE0213DB54100BCD44B /* ReleaseAppStoreLLC */, + D086A56C1CC0115200F08284 /* ReleaseHockeyapp */, + D0924FD31FE52BD8003F693F /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + 058D09A7195D04C000B7D73C /* Build configuration list for PBXProject "AsyncDisplayKit_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058D09CD195D04C000B7D73C /* DebugHockeyapp */, + D021D4F0219CB1920064BEBA /* DebugFork */, + D079FD051F06BD7A0038FADE /* DebugAppStore */, + D0ADF91C212B3AB400310BBC /* DebugAppStoreLLC */, + 058D09CE195D04C000B7D73C /* ReleaseAppStore */, + D0CE6EDD213DB54100BCD44B /* ReleaseAppStoreLLC */, + D086A5691CC0115200F08284 /* ReleaseHockeyapp */, + D0924FD01FE52BD8003F693F /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + 058D09D2195D04C000B7D73C /* Build configuration list for PBXNativeTarget "AsyncDisplayKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058D09D3195D04C000B7D73C /* DebugHockeyapp */, + D021D4F2219CB1920064BEBA /* DebugFork */, + D079FD071F06BD7A0038FADE /* DebugAppStore */, + D0ADF91E212B3AB400310BBC /* DebugAppStoreLLC */, + 058D09D4195D04C000B7D73C /* ReleaseAppStore */, + D0CE6EDF213DB54100BCD44B /* ReleaseAppStoreLLC */, + D063755A1E67124400F00C9A /* ReleaseHockeyapp */, + D0924FD21FE52BD8003F693F /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + B35061ED1B010EDF0018CF92 /* Build configuration list for PBXNativeTarget "AsyncDisplayKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B35061EE1B010EDF0018CF92 /* DebugHockeyapp */, + D021D4F1219CB1920064BEBA /* DebugFork */, + D079FD061F06BD7A0038FADE /* DebugAppStore */, + D0ADF91D212B3AB400310BBC /* DebugAppStoreLLC */, + B35061EF1B010EDF0018CF92 /* ReleaseAppStore */, + D0CE6EDE213DB54100BCD44B /* ReleaseAppStoreLLC */, + D086A56D1CC0115200F08284 /* ReleaseHockeyapp */, + D0924FD11FE52BD8003F693F /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = 058D09A4195D04C000B7D73C /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme new file mode 100644 index 0000000000..4ffad37f1c --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig new file mode 100644 index 0000000000..6129951d1c --- /dev/null +++ b/submodules/AsyncDisplayKit/AsyncDisplayKit_Xcode.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig @@ -0,0 +1,115 @@ + +>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + version = "1.3"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/BUCK b/submodules/AsyncDisplayKit/BUCK new file mode 100755 index 0000000000..2f5686da4b --- /dev/null +++ b/submodules/AsyncDisplayKit/BUCK @@ -0,0 +1,64 @@ +load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'combined_config', 'configs_with_config') +load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'EXTENSION_LIB_SPECIFIC_CONFIG') + +COMMON_PREPROCESSOR_FLAGS = [ + '-fobjc-arc', + '-DMINIMAL_ASDK', + '-fno-exceptions', + '-fno-objc-arc-exceptions' +] + +COMMON_LANG_PREPROCESSOR_FLAGS = { + 'C': ['-std=gnu99'], + 'CXX': ['-std=c++11', '-stdlib=libc++'], + 'OBJCXX': ['-std=c++11', '-stdlib=libc++'], +} + +COMMON_LINKER_FLAGS = ['-ObjC++'] + +ASYNCDISPLAYKIT_EXPORTED_HEADERS = glob([ + 'Source/*.h', + 'Source/Details/**/*.h', + 'Source/Layout/*.h', + 'Source/Base/*.h', + 'Source/Debug/AsyncDisplayKit+Debug.h', + # Most TextKit components are not public because the C++ content + # in the headers will cause build errors when using + # `use_frameworks!` on 0.39.0 & Swift 2.1. + # See https://github.com/facebook/AsyncDisplayKit/issues/1153 + 'Source/TextKit/ASTextNodeTypes.h', + 'Source/TextKit/ASTextKitComponents.h' +]) + +ASYNCDISPLAYKIT_PRIVATE_HEADERS = glob([ + 'Source/**/*.h' + ], + exclude = ASYNCDISPLAYKIT_EXPORTED_HEADERS, +) + +apple_library( + name = "AsyncDisplayKit", + header_path_prefix = 'AsyncDisplayKit', + exported_headers = ASYNCDISPLAYKIT_EXPORTED_HEADERS, + headers = ASYNCDISPLAYKIT_PRIVATE_HEADERS, + srcs = glob([ + 'Source/**/*.m', + 'Source/**/*.mm', + 'Source/Base/*.m' + ]), + configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])), + preprocessor_flags = COMMON_PREPROCESSOR_FLAGS, + lang_preprocessor_flags = COMMON_LANG_PREPROCESSOR_FLAGS, + linker_flags = COMMON_LINKER_FLAGS, + modular = True, + compiler_flags = ['-w'], + visibility = ["PUBLIC"], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + '$SDKROOT/System/Library/Frameworks/QuartzCore.framework', + '$SDKROOT/System/Library/Frameworks/CoreMedia.framework', + '$SDKROOT/System/Library/Frameworks/CoreText.framework', + '$SDKROOT/System/Library/Frameworks/CoreGraphics.framework', + ] +) diff --git a/submodules/AsyncDisplayKit/CHANGELOG.md b/submodules/AsyncDisplayKit/CHANGELOG.md new file mode 100644 index 0000000000..0cddbfab45 --- /dev/null +++ b/submodules/AsyncDisplayKit/CHANGELOG.md @@ -0,0 +1,685 @@ +# Change Log + +## [2.8](https://github.com/TextureGroup/Texture/tree/2.8) (2019-02-12) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.7...2.8) + +**Merged pull requests:** + +- Remove duplicate definition of category "YogaDebugging" [\#1331](https://github.com/TextureGroup/Texture/pull/1331) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add Yoga layout to ASDKGram Texture cells [\#1315](https://github.com/TextureGroup/Texture/pull/1315) ([maicki](https://github.com/maicki)) +- Remove let and var macros now that we're all-C++ [\#1312](https://github.com/TextureGroup/Texture/pull/1312) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add experiments to skip waiting for updates of collection and table views [\#1311](https://github.com/TextureGroup/Texture/pull/1311) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASCollectionView\] Supplementary nodes should not enter ASHierarchyStateRangeManaged. [\#1310](https://github.com/TextureGroup/Texture/pull/1310) ([appleguy](https://github.com/appleguy)) +- Fix deprecated implementations warning [\#1306](https://github.com/TextureGroup/Texture/pull/1306) ([maicki](https://github.com/maicki)) +- Improve separation of code for layout method types [\#1305](https://github.com/TextureGroup/Texture/pull/1305) ([maicki](https://github.com/maicki)) +- Fix loading items in ASDKGram IGListKit tab [\#1300](https://github.com/TextureGroup/Texture/pull/1300) ([maicki](https://github.com/maicki)) +- performance spell correction [\#1298](https://github.com/TextureGroup/Texture/pull/1298) ([wxyong](https://github.com/wxyong)) +- Add some snapshot tests for ASTextNode2 truncation modes. [\#1296](https://github.com/TextureGroup/Texture/pull/1296) ([wiseoldduck](https://github.com/wiseoldduck)) +- Reduce startup time. [\#1294](https://github.com/TextureGroup/Texture/pull/1294) ([dmaclach](https://github.com/dmaclach)) +- Reduce startup time. [\#1293](https://github.com/TextureGroup/Texture/pull/1293) ([dmaclach](https://github.com/dmaclach)) +- Reduce startup time. [\#1292](https://github.com/TextureGroup/Texture/pull/1292) ([dmaclach](https://github.com/dmaclach)) +- Reduce startup time. [\#1291](https://github.com/TextureGroup/Texture/pull/1291) ([dmaclach](https://github.com/dmaclach)) +- Reduce startup time. [\#1288](https://github.com/TextureGroup/Texture/pull/1288) ([dmaclach](https://github.com/dmaclach)) +- Add a way to opt out of always-clear-data behavior in ASCollectionView and ASTableView [\#1284](https://github.com/TextureGroup/Texture/pull/1284) ([nguyenhuy](https://github.com/nguyenhuy)) +- Copy yogaChildren in accessor method. Avoid using accessor method internally [\#1283](https://github.com/TextureGroup/Texture/pull/1283) ([maicki](https://github.com/maicki)) +- Use cell mode while wrapping supplementary nodes [\#1282](https://github.com/TextureGroup/Texture/pull/1282) ([maicki](https://github.com/maicki)) +- Access thread safe property to avoid assertion [\#1281](https://github.com/TextureGroup/Texture/pull/1281) ([wiseoldduck](https://github.com/wiseoldduck)) +- Match AS\_USE\_VIDEO usage in tests to definitions [\#1280](https://github.com/TextureGroup/Texture/pull/1280) ([wiseoldduck](https://github.com/wiseoldduck)) +- Update test imports to use framework import [\#1279](https://github.com/TextureGroup/Texture/pull/1279) ([maicki](https://github.com/maicki)) +- Set automaticallyAdjustsContentOffset to ASTableView when view is load [\#1278](https://github.com/TextureGroup/Texture/pull/1278) ([strangeliu](https://github.com/strangeliu)) +- Remove UIKit header import in AsyncTransaction file [\#1275](https://github.com/TextureGroup/Texture/pull/1275) ([zhongwuzw](https://github.com/zhongwuzw)) +- Disable a11y cache [\#1274](https://github.com/TextureGroup/Texture/pull/1274) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Introduce ASCellLayoutMode [\#1273](https://github.com/TextureGroup/Texture/pull/1273) ([maicki](https://github.com/maicki)) +- During yoga layout, escalate directly to yoga root rather than walking up [\#1269](https://github.com/TextureGroup/Texture/pull/1269) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Forward hitTest:withEvent and piontInside:withEvent: to node within \_ASCollectionViewCell [\#1268](https://github.com/TextureGroup/Texture/pull/1268) ([maicki](https://github.com/maicki)) +- Wrap supplementary node blocks to enable resizing them. [\#1265](https://github.com/TextureGroup/Texture/pull/1265) ([wiseoldduck](https://github.com/wiseoldduck)) +- Move Bluebird to new row. [\#1264](https://github.com/TextureGroup/Texture/pull/1264) ([ay8s](https://github.com/ay8s)) +- Added Bluebird [\#1263](https://github.com/TextureGroup/Texture/pull/1263) ([ShihabM](https://github.com/ShihabM)) +- Move assertions so they are valid. [\#1261](https://github.com/TextureGroup/Texture/pull/1261) ([wiseoldduck](https://github.com/wiseoldduck)) +- Fix isTruncated logic in ASTextNode2 [\#1259](https://github.com/TextureGroup/Texture/pull/1259) ([maicki](https://github.com/maicki)) +- Documentation typo, "trying" written two times [\#1258](https://github.com/TextureGroup/Texture/pull/1258) ([tataevr](https://github.com/tataevr)) +- \[ASPrimitiveTraitCollection\] Fix ASPrimitiveTraitCollectionMakeDefault and implement containerSize [\#1256](https://github.com/TextureGroup/Texture/pull/1256) ([rcancro](https://github.com/rcancro)) +- Yoga debug info [\#1253](https://github.com/TextureGroup/Texture/pull/1253) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Avoid using global Mutex variables [\#1252](https://github.com/TextureGroup/Texture/pull/1252) ([nguyenhuy](https://github.com/nguyenhuy)) +- Allow setting build.sh SDK and platform w/ env variables [\#1249](https://github.com/TextureGroup/Texture/pull/1249) ([wiseoldduck](https://github.com/wiseoldduck)) +- add more delegate methods for monitoring network image node progress [\#1247](https://github.com/TextureGroup/Texture/pull/1247) ([ernestmama](https://github.com/ernestmama)) +- Start a thrash test suite for the collection node [\#1246](https://github.com/TextureGroup/Texture/pull/1246) ([mikezucc](https://github.com/mikezucc)) +- Add development docs structure [\#1245](https://github.com/TextureGroup/Texture/pull/1245) ([garrettmoon](https://github.com/garrettmoon)) +- Convert YGUndefined back to CGFLOAT\_MAX for Texture layout [\#1244](https://github.com/TextureGroup/Texture/pull/1244) ([tnorman42](https://github.com/tnorman42)) +- Add way to compile out ASTextNode + TextKit dependencies [\#1242](https://github.com/TextureGroup/Texture/pull/1242) ([maicki](https://github.com/maicki)) +- Add AS\_USE\_VIDEO flag and subspec for Video [\#1240](https://github.com/TextureGroup/Texture/pull/1240) ([maicki](https://github.com/maicki)) +- Releases/p6.78 [\#1236](https://github.com/TextureGroup/Texture/pull/1236) ([ernestmama](https://github.com/ernestmama)) +- \[ASDisplayNode\] Propagate traits before loading a subnode [\#1234](https://github.com/TextureGroup/Texture/pull/1234) ([rcancro](https://github.com/rcancro)) +- Correct some block self references to strongSelf [\#1231](https://github.com/TextureGroup/Texture/pull/1231) ([wiseoldduck](https://github.com/wiseoldduck)) +- Update image-node.md [\#1230](https://github.com/TextureGroup/Texture/pull/1230) ([orkhan-huseynov](https://github.com/orkhan-huseynov)) +- Have node and controller share lock [\#1227](https://github.com/TextureGroup/Texture/pull/1227) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Initialize mutex assertion variables [\#1226](https://github.com/TextureGroup/Texture/pull/1226) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove CHECK\_LOCKING\_SAFETY check [\#1225](https://github.com/TextureGroup/Texture/pull/1225) ([maicki](https://github.com/maicki)) +- Clean up our mutex, fix try\_lock not hooking into assert mechanism [\#1219](https://github.com/TextureGroup/Texture/pull/1219) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix warning using \_\_builtin\_popcount [\#1218](https://github.com/TextureGroup/Texture/pull/1218) ([maicki](https://github.com/maicki)) +- Fix A11Y for horizontal collection nodes in Texture [\#1217](https://github.com/TextureGroup/Texture/pull/1217) ([maicki](https://github.com/maicki)) +- ASCATransactionQueue interface trashing improvements [\#1216](https://github.com/TextureGroup/Texture/pull/1216) ([maicki](https://github.com/maicki)) +- Fix shouldTruncateForConstrainedSize in ASTextNode2 [\#1214](https://github.com/TextureGroup/Texture/pull/1214) ([maicki](https://github.com/maicki)) +- ASThread: Remove Locker, Unlocker, and SharedMutex [\#1213](https://github.com/TextureGroup/Texture/pull/1213) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Cleanup Dangerfile [\#1212](https://github.com/TextureGroup/Texture/pull/1212) ([nguyenhuy](https://github.com/nguyenhuy)) +- Rework ASTraitCollection to Fix Warnings and Remove Boilerplate [\#1211](https://github.com/TextureGroup/Texture/pull/1211) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add -Wno-implicit-retain-self to podspec + smaller cleanups \#trivial [\#1209](https://github.com/TextureGroup/Texture/pull/1209) ([maicki](https://github.com/maicki)) +- Address compiler warnings \#trivial [\#1207](https://github.com/TextureGroup/Texture/pull/1207) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Convert the codebase to Objective-C++ [\#1206](https://github.com/TextureGroup/Texture/pull/1206) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add tests for accessibility [\#1205](https://github.com/TextureGroup/Texture/pull/1205) ([wiseoldduck](https://github.com/wiseoldduck)) +- Revert \#1023 \#trivial [\#1204](https://github.com/TextureGroup/Texture/pull/1204) ([maicki](https://github.com/maicki)) +- Follow up cleanup \#trivial [\#1203](https://github.com/TextureGroup/Texture/pull/1203) ([maicki](https://github.com/maicki)) +- Add experiment flag to skip layoutIfNeeded in enterPreloadState for ASM nodes \#trivial [\#1201](https://github.com/TextureGroup/Texture/pull/1201) ([maicki](https://github.com/maicki)) +- Fix logic cleaning data if delegate / dataSource changes and bring over logic to ASTableView [\#1200](https://github.com/TextureGroup/Texture/pull/1200) ([maicki](https://github.com/maicki)) +- Tweak a11y label aggregation behavior to enable container label overrides [\#1199](https://github.com/TextureGroup/Texture/pull/1199) ([maicki](https://github.com/maicki)) +- Fix shadowed var warning \(and add clarity\) \#trivial [\#1198](https://github.com/TextureGroup/Texture/pull/1198) ([wiseoldduck](https://github.com/wiseoldduck)) +- Allow configuring imageCache when initializing ASPINRemoteImageDownloader. [\#1197](https://github.com/TextureGroup/Texture/pull/1197) ([wiseoldduck](https://github.com/wiseoldduck)) +- ASTextNode2 to consider both width and height when determining if it is calculating an intrinsic size [\#1196](https://github.com/TextureGroup/Texture/pull/1196) ([ernestmama](https://github.com/ernestmama)) +- Remove extraneous ";" \#trivial [\#1194](https://github.com/TextureGroup/Texture/pull/1194) ([wiseoldduck](https://github.com/wiseoldduck)) +- Newline character support and truncated line sizing improvement. [\#1193](https://github.com/TextureGroup/Texture/pull/1193) ([wiseoldduck](https://github.com/wiseoldduck)) +- Correct linePositionModifier behavior [\#1192](https://github.com/TextureGroup/Texture/pull/1192) ([maicki](https://github.com/maicki)) +- Move AS\_TEXT\_ALERT\_UNIMPLEMENTED\_FEATURE into ASTextNodeCommon \#trivial [\#1191](https://github.com/TextureGroup/Texture/pull/1191) ([maicki](https://github.com/maicki)) +- A11y for scrollnode [\#1188](https://github.com/TextureGroup/Texture/pull/1188) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Yoga integration improvements [\#1187](https://github.com/TextureGroup/Texture/pull/1187) ([maicki](https://github.com/maicki)) +- Remove unnecessary ASWeakProxy import \#trivial [\#1186](https://github.com/TextureGroup/Texture/pull/1186) ([maicki](https://github.com/maicki)) +- Directly use \_\_instanceLock\_\_ to lock / unlock without having to create and destroy a MutextUnlocker \#trivial [\#1185](https://github.com/TextureGroup/Texture/pull/1185) ([maicki](https://github.com/maicki)) +- Don’t handle touches on additional attributed message if passthrough is enabled [\#1184](https://github.com/TextureGroup/Texture/pull/1184) ([maicki](https://github.com/maicki)) +- Set the default values for showsVerticalScrollIndicator and showsHorizontalScrollIndicator \#trivial [\#1181](https://github.com/TextureGroup/Texture/pull/1181) ([maicki](https://github.com/maicki)) +- Move import of stdatomic to ASRecursiveUnfairLock implementation file \#trivial [\#1180](https://github.com/TextureGroup/Texture/pull/1180) ([maicki](https://github.com/maicki)) +- Add NSLocking conformance to ASNodeController [\#1179](https://github.com/TextureGroup/Texture/pull/1179) ([maicki](https://github.com/maicki)) +- Only initialize framework once, avoid multiple across tests \#trivial [\#1178](https://github.com/TextureGroup/Texture/pull/1178) ([maicki](https://github.com/maicki)) +- Expose a way to determine if a text node will truncate for a given constrained size \#trivial [\#1177](https://github.com/TextureGroup/Texture/pull/1177) ([maicki](https://github.com/maicki)) +- Fix define spaces \#trivial [\#1176](https://github.com/TextureGroup/Texture/pull/1176) ([maicki](https://github.com/maicki)) +- Expose test\_resetWithConfiguration: for testing \#trivial [\#1175](https://github.com/TextureGroup/Texture/pull/1175) ([maicki](https://github.com/maicki)) +- Add way to suppress invalid CollectionUpdateExceptions \#trivial [\#1173](https://github.com/TextureGroup/Texture/pull/1173) ([maicki](https://github.com/maicki)) +- Use interface state to manage image loading \#trivial [\#1172](https://github.com/TextureGroup/Texture/pull/1172) ([maicki](https://github.com/maicki)) +- ASTableNode init method match checks from ASCollectionNode [\#1171](https://github.com/TextureGroup/Texture/pull/1171) ([maicki](https://github.com/maicki)) +- \[ASDisplayNode\] Expose default Texture-set accessibility values as properties [\#1170](https://github.com/TextureGroup/Texture/pull/1170) ([jiawernlim](https://github.com/jiawernlim)) +- Fix mismatch in UIAccessibilityAction selector method [\#1169](https://github.com/TextureGroup/Texture/pull/1169) ([maicki](https://github.com/maicki)) +- Small fix in ASTextKitRenderer \#trivial [\#1167](https://github.com/TextureGroup/Texture/pull/1167) ([nguyenhuy](https://github.com/nguyenhuy)) +- ASTextNode2 to ignore certain text alignments while calculating intrinsic size [\#1166](https://github.com/TextureGroup/Texture/pull/1166) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update Jekyll to 3.6.3 [\#1165](https://github.com/TextureGroup/Texture/pull/1165) ([nguyenhuy](https://github.com/nguyenhuy)) +- Migrate placeholder example project from 1.0 to 2.x [\#1164](https://github.com/TextureGroup/Texture/pull/1164) ([ay8s](https://github.com/ay8s)) +- Update documentation of ASNetworkImageNodeDelegate \#trivial [\#1163](https://github.com/TextureGroup/Texture/pull/1163) ([nguyenhuy](https://github.com/nguyenhuy)) +- Make ASEditableTextNode accessible to VoiceOver [\#1162](https://github.com/TextureGroup/Texture/pull/1162) ([ay8s](https://github.com/ay8s)) +- Mismatch name experimental features [\#1159](https://github.com/TextureGroup/Texture/pull/1159) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Set default tuning params [\#1158](https://github.com/TextureGroup/Texture/pull/1158) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Clean up timing of layout tree flattening/ copying of unflattened tree for Weaver [\#1157](https://github.com/TextureGroup/Texture/pull/1157) ([mikezucc](https://github.com/mikezucc)) +- Only clear ASCollectionView's data during deallocation [\#1154](https://github.com/TextureGroup/Texture/pull/1154) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASTextNode2\] Add improved support for all line-break modes in experimental text node. [\#1150](https://github.com/TextureGroup/Texture/pull/1150) ([wiseoldduck](https://github.com/wiseoldduck)) +- \[ASImageNode\] Fix a threading issue which can cause a display completion block to never be executed [\#1148](https://github.com/TextureGroup/Texture/pull/1148) ([nguyenhuy](https://github.com/nguyenhuy)) +- Guard photo library with macro for tests [\#1147](https://github.com/TextureGroup/Texture/pull/1147) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Rollout ASDeallocQueueV2 \#trivial [\#1143](https://github.com/TextureGroup/Texture/pull/1143) ([ernestmama](https://github.com/ernestmama)) +- Fix crash setting attributed text on multiple threads [\#1141](https://github.com/TextureGroup/Texture/pull/1141) ([maicki](https://github.com/maicki)) +- Add missing NS\_NOESCAPE attributes in overwritten methods \#trivial [\#1139](https://github.com/TextureGroup/Texture/pull/1139) ([ejensen](https://github.com/ejensen)) +- Add missing comma in ASExperimentalFeatures \#trivial [\#1137](https://github.com/TextureGroup/Texture/pull/1137) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add ASExperimentalSkipClearData \#trivial [\#1136](https://github.com/TextureGroup/Texture/pull/1136) ([maicki](https://github.com/maicki)) +- Fix RemoteImageDownloader name mismatch \#trivial [\#1134](https://github.com/TextureGroup/Texture/pull/1134) ([ernestmama](https://github.com/ernestmama)) +- Fix compilation warnings \#trivial [\#1132](https://github.com/TextureGroup/Texture/pull/1132) ([ejensen](https://github.com/ejensen)) +- Remove reliance on shared\_ptr for ASDisplayNodeLayouts [\#1131](https://github.com/TextureGroup/Texture/pull/1131) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Make yoga & layout specs faster by eliminating some copies [\#1128](https://github.com/TextureGroup/Texture/pull/1128) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove ASRectMap, which is not worth its own weight [\#1127](https://github.com/TextureGroup/Texture/pull/1127) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASPINRemoteImageDownloader\] Fix +setSharedPreconfiguredRemoteImageManager:'s doc \#trivial [\#1126](https://github.com/TextureGroup/Texture/pull/1126) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add a method for setting preconfigured PINRemoteImageManager [\#1124](https://github.com/TextureGroup/Texture/pull/1124) ([ernestmama](https://github.com/ernestmama)) +- Don't copy onDidLoadBlocks \#trivial [\#1123](https://github.com/TextureGroup/Texture/pull/1123) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove use of NSHashTable for interface state delegates \#trivial [\#1122](https://github.com/TextureGroup/Texture/pull/1122) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix typos and minor code cleanups \#trivial [\#1120](https://github.com/TextureGroup/Texture/pull/1120) ([nguyenhuy](https://github.com/nguyenhuy)) +- Don't setNeedsDisplay on text node 2 measure \#trivial [\#1116](https://github.com/TextureGroup/Texture/pull/1116) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Don't copy container during ASTextNode2 measure [\#1115](https://github.com/TextureGroup/Texture/pull/1115) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Make interface state delegate non optional [\#1112](https://github.com/TextureGroup/Texture/pull/1112) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Interface state not update correctly during layer thrash. [\#1111](https://github.com/TextureGroup/Texture/pull/1111) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Fix layer backed nodes not update properly [\#1110](https://github.com/TextureGroup/Texture/pull/1110) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- changelog fix: let / var macros did not make it to 2.7 [\#1109](https://github.com/TextureGroup/Texture/pull/1109) ([jozsefmihalicza](https://github.com/jozsefmihalicza)) +- Improve locking around clearContents [\#1107](https://github.com/TextureGroup/Texture/pull/1107) ([maicki](https://github.com/maicki)) +- Add missing argument for calling image download completion block \#trivial [\#1106](https://github.com/TextureGroup/Texture/pull/1106) ([maicki](https://github.com/maicki)) +- Fix URL for blog about Pinterest [\#1105](https://github.com/TextureGroup/Texture/pull/1105) ([muukii](https://github.com/muukii)) +- Remove necessity to use view to access rangeController in ASTableNode, ASCollectionNode [\#1103](https://github.com/TextureGroup/Texture/pull/1103) ([maicki](https://github.com/maicki)) +- Add a -textureDidInitialize delegate callback [\#1100](https://github.com/TextureGroup/Texture/pull/1100) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Reuse interface state delegates when calling out \#trivial [\#1099](https://github.com/TextureGroup/Texture/pull/1099) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add an explicit cast to satisfy strict compilers \#trivial [\#1098](https://github.com/TextureGroup/Texture/pull/1098) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix a couple typos. [\#1092](https://github.com/TextureGroup/Texture/pull/1092) ([jtbthethird](https://github.com/jtbthethird)) +- \#trivial Shouldn't hold the lock while adding subnodes [\#1091](https://github.com/TextureGroup/Texture/pull/1091) ([garrettmoon](https://github.com/garrettmoon)) +- Allow to add interface state delegate in background. [\#1090](https://github.com/TextureGroup/Texture/pull/1090) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Fix Typo [\#1089](https://github.com/TextureGroup/Texture/pull/1089) ([jtbthethird](https://github.com/jtbthethird)) +- Add subnode should not be called with the lock held. \#trivial [\#1088](https://github.com/TextureGroup/Texture/pull/1088) ([garrettmoon](https://github.com/garrettmoon)) +- Unlock before cleanup and calling out to subclass hooks for animated images. [\#1087](https://github.com/TextureGroup/Texture/pull/1087) ([maicki](https://github.com/maicki)) +- Fix collection editing [\#1081](https://github.com/TextureGroup/Texture/pull/1081) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Fix compiler error in ASLocking \#trivial [\#1079](https://github.com/TextureGroup/Texture/pull/1079) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update showcase to add Wishpoke [\#1078](https://github.com/TextureGroup/Texture/pull/1078) ([dhatuna](https://github.com/dhatuna)) +- \[License\] Simplify the Texture license to be pure Apache 2 \(removing ASDK-Licenses\). [\#1077](https://github.com/TextureGroup/Texture/pull/1077) ([appleguy](https://github.com/appleguy)) +- Fix multiple documentation issues \#trivial [\#1073](https://github.com/TextureGroup/Texture/pull/1073) ([maicki](https://github.com/maicki)) +- Refactored `accessibleElements` to `accessibilityElements` [\#1069](https://github.com/TextureGroup/Texture/pull/1069) ([jiawernlim](https://github.com/jiawernlim)) +- Readability improvements in ASDataController \#trivial [\#1067](https://github.com/TextureGroup/Texture/pull/1067) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove direct ivar access on non-self object to fix mocking case \#trivial [\#1066](https://github.com/TextureGroup/Texture/pull/1066) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Reduce copying in ASTextNode2 stack [\#1065](https://github.com/TextureGroup/Texture/pull/1065) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add an experimental framesetter cache in ASTextNode2 [\#1063](https://github.com/TextureGroup/Texture/pull/1063) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove extra string/attributed string creation in accessibility props [\#1062](https://github.com/TextureGroup/Texture/pull/1062) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove objc association & weak proxy from node -\> controller pointer [\#1061](https://github.com/TextureGroup/Texture/pull/1061) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove CATransaction signposts [\#1060](https://github.com/TextureGroup/Texture/pull/1060) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextNode2\] Simplify allocWithZone: + initialize implementation \#trivial [\#1059](https://github.com/TextureGroup/Texture/pull/1059) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextNode\] Fixes in ASTextKitFontSizeAdjuster [\#1056](https://github.com/TextureGroup/Texture/pull/1056) ([ejensen](https://github.com/ejensen)) +- Revert "Optimize drawing code + add examples how to round corners \(\#996\) [\#1055](https://github.com/TextureGroup/Texture/pull/1055) ([maicki](https://github.com/maicki)) +- Add NS\_DESIGNATED\_INITIALIZER to ASViewController initWithNode: [\#1054](https://github.com/TextureGroup/Texture/pull/1054) ([maicki](https://github.com/maicki)) +- Fix headers in markdown [\#1053](https://github.com/TextureGroup/Texture/pull/1053) ([Un3qual](https://github.com/Un3qual)) +- Avoid setting frame on a node's backing store while holding its lock [\#1048](https://github.com/TextureGroup/Texture/pull/1048) ([nguyenhuy](https://github.com/nguyenhuy)) +- \#trivial Add a comment about tiling mode and issue \#1046 [\#1047](https://github.com/TextureGroup/Texture/pull/1047) ([wiseoldduck](https://github.com/wiseoldduck)) +- Add documentation for rounding corners within Texture \#trivial [\#1044](https://github.com/TextureGroup/Texture/pull/1044) ([maicki](https://github.com/maicki)) +- Improve locking situation in ASVideoPlayerNode [\#1042](https://github.com/TextureGroup/Texture/pull/1042) ([maicki](https://github.com/maicki)) +- Revert unreleased layout debug method name change from \#1030 \#trivial [\#1039](https://github.com/TextureGroup/Texture/pull/1039) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Pin OCMock version to 3.4.1 because 3.4.2 has issues [\#1038](https://github.com/TextureGroup/Texture/pull/1038) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix & update ASCollectionNode constrained size doc. \#trivial [\#1037](https://github.com/TextureGroup/Texture/pull/1037) ([ay8s](https://github.com/ay8s)) +- Fix warning for ASLayout method override for the designated initializer of the superclass '-init' not found \#trivial [\#1036](https://github.com/TextureGroup/Texture/pull/1036) ([maicki](https://github.com/maicki)) +- Fix the bug I introduced in \#1030 \#trivial [\#1035](https://github.com/TextureGroup/Texture/pull/1035) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Turn off exceptions to reduce binary size \(-600KB for arm64\) [\#1033](https://github.com/TextureGroup/Texture/pull/1033) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Turn lock-checking on only when assertions are enabled \#trivial [\#1032](https://github.com/TextureGroup/Texture/pull/1032) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove NSMutableArray for retaining sublayout elements [\#1030](https://github.com/TextureGroup/Texture/pull/1030) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Create and set delegate for clip corner layers within ASDisplayNode [\#1029](https://github.com/TextureGroup/Texture/pull/1029) ([maicki](https://github.com/maicki)) +- Split framework dependencies into separate subspecs [\#1028](https://github.com/TextureGroup/Texture/pull/1028) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove misleading comment and add assertion \#trivial [\#1027](https://github.com/TextureGroup/Texture/pull/1027) ([wiseoldduck](https://github.com/wiseoldduck)) +- Address warnings in Xcode \>= 9.3 about using %zd for NSInteger \#trivial [\#1026](https://github.com/TextureGroup/Texture/pull/1026) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix 32-bit simulator build on Xcode \>= 9.3 [\#1025](https://github.com/TextureGroup/Texture/pull/1025) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Stricter locking assertions [\#1024](https://github.com/TextureGroup/Texture/pull/1024) ([nguyenhuy](https://github.com/nguyenhuy)) +- Make sure -\_completePendingLayoutTransition is called without the node's instance lock \#trivial [\#1023](https://github.com/TextureGroup/Texture/pull/1023) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fix misleading/scary stack trace shown when an assertion occurs during node measurement [\#1022](https://github.com/TextureGroup/Texture/pull/1022) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add an introduction for ASCornerLayoutSpec in layout2-layoutspec-types.md \#trivial [\#1021](https://github.com/TextureGroup/Texture/pull/1021) ([huang-kun](https://github.com/huang-kun)) +- Add showsHorizontal\(Vertical\)ScrollIndicator property applying from pending state \#trivial [\#1016](https://github.com/TextureGroup/Texture/pull/1016) ([maicki](https://github.com/maicki)) +- \[IGListKit\] Adds missing UIScrollViewDelegate method to DataSource proxy [\#1015](https://github.com/TextureGroup/Texture/pull/1015) ([wannabehero](https://github.com/wannabehero)) +- Introduce let / var macros and some further cleanup [\#1012](https://github.com/TextureGroup/Texture/pull/1012) ([maicki](https://github.com/maicki)) +- Properly consider node for responder methods [\#1008](https://github.com/TextureGroup/Texture/pull/1008) ([maicki](https://github.com/maicki)) +- Background image load api [\#1007](https://github.com/TextureGroup/Texture/pull/1007) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Add move detection and support to ASLayoutTransition [\#1006](https://github.com/TextureGroup/Texture/pull/1006) ([wiseoldduck](https://github.com/wiseoldduck)) +- Fix warnings and a memory leak \#trivial [\#1003](https://github.com/TextureGroup/Texture/pull/1003) ([maicki](https://github.com/maicki)) +- Rewrite Swift Example [\#1002](https://github.com/TextureGroup/Texture/pull/1002) ([maicki](https://github.com/maicki)) +- Remove yoga layout spec, which has been superseded [\#999](https://github.com/TextureGroup/Texture/pull/999) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Optimize drawing code + add examples how to round corners [\#996](https://github.com/TextureGroup/Texture/pull/996) ([maicki](https://github.com/maicki)) +- Fix typo in containers-asviewcontroller.md [\#989](https://github.com/TextureGroup/Texture/pull/989) ([muukii](https://github.com/muukii)) +- Create transfer-array method and use it [\#987](https://github.com/TextureGroup/Texture/pull/987) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add missing instance variables in ASTextNode and warnings cleanup \#trivial [\#984](https://github.com/TextureGroup/Texture/pull/984) ([maicki](https://github.com/maicki)) +- Optimize layout flattening [\#982](https://github.com/TextureGroup/Texture/pull/982) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Changed lost images to existing one. \#trivial [\#981](https://github.com/TextureGroup/Texture/pull/981) ([tataevr](https://github.com/tataevr)) +- \[texturegroup.org\] Use valid link for Upgrade to 2.0 beta 1 page \#trivial [\#980](https://github.com/TextureGroup/Texture/pull/980) ([mikezucc](https://github.com/mikezucc)) +- Adds support for having multiple interface state delegates. [\#979](https://github.com/TextureGroup/Texture/pull/979) ([garrettmoon](https://github.com/garrettmoon)) +- Create an experiment to remove extra collection teardown step [\#975](https://github.com/TextureGroup/Texture/pull/975) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove unused/unneeded header macros [\#973](https://github.com/TextureGroup/Texture/pull/973) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Standardize "extern" decls on AS\_EXTERN [\#972](https://github.com/TextureGroup/Texture/pull/972) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- ASConfiguration version check only when have json dict [\#971](https://github.com/TextureGroup/Texture/pull/971) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Pointer check [\#970](https://github.com/TextureGroup/Texture/pull/970) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Reduce usage of autorelease pools [\#968](https://github.com/TextureGroup/Texture/pull/968) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update showcase to include Apollo for Reddit [\#967](https://github.com/TextureGroup/Texture/pull/967) ([christianselig](https://github.com/christianselig)) +- Fix crash when call needsMainThreadDeallocation on NSProxy instances \#trivial [\#965](https://github.com/TextureGroup/Texture/pull/965) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fix name typo \#trivial [\#963](https://github.com/TextureGroup/Texture/pull/963) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Generalize the main thread ivar deallocation system [\#959](https://github.com/TextureGroup/Texture/pull/959) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add support for acquiring multiple locks at once [\#958](https://github.com/TextureGroup/Texture/pull/958) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Clean up async transaction system a bit [\#955](https://github.com/TextureGroup/Texture/pull/955) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Added 'Waplog' to showcase. [\#953](https://github.com/TextureGroup/Texture/pull/953) ([malikkuru](https://github.com/malikkuru)) +- Make ASPerformMainThreadDeallocation visible in C [\#952](https://github.com/TextureGroup/Texture/pull/952) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Cut 2.7 release [\#949](https://github.com/TextureGroup/Texture/pull/949) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fixed removing node from supernode after layout transition [\#937](https://github.com/TextureGroup/Texture/pull/937) ([atitovdev](https://github.com/atitovdev)) +- add ASTextNode2 snapshot test [\#935](https://github.com/TextureGroup/Texture/pull/935) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[ASTextNode\] One more check variables before calling delegate method \#trivial [\#922](https://github.com/TextureGroup/Texture/pull/922) ([Flatout73](https://github.com/Flatout73)) +- Assert node did load before did enter visible way 1 [\#886](https://github.com/TextureGroup/Texture/pull/886) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Renew supplementary node on relayout [\#842](https://github.com/TextureGroup/Texture/pull/842) ([wsdwsd0829](https://github.com/wsdwsd0829)) + +## [2.7](https://github.com/TextureGroup/Texture/tree/2.7) (2018-05-29) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.6...2.7) + +**Merged pull requests:** + +- Update AppIcon in showcase [\#946](https://github.com/TextureGroup/Texture/pull/946) ([muukii](https://github.com/muukii)) +- Update tip-1-nodeBlocks.md [\#943](https://github.com/TextureGroup/Texture/pull/943) ([sagarbhosale](https://github.com/sagarbhosale)) +- \[ASTableView\] Generate a new cell layout if existing ones are invalid [\#942](https://github.com/TextureGroup/Texture/pull/942) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update to unsplash [\#938](https://github.com/TextureGroup/Texture/pull/938) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASTextNode2\] Simplify compare-assign check & lock \_pointScaleFactors accessor \#trivial [\#934](https://github.com/TextureGroup/Texture/pull/934) ([appleguy](https://github.com/appleguy)) +- Create a new dealloc queue that is more efficient [\#931](https://github.com/TextureGroup/Texture/pull/931) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASImageNode+AnimatedImage\] Fix early return when animatedImage is nil in setAnimatedImage \#trivial [\#925](https://github.com/TextureGroup/Texture/pull/925) ([flovouin](https://github.com/flovouin)) +- Remove assert. fix \#878 \#914 [\#924](https://github.com/TextureGroup/Texture/pull/924) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Always call out to delegate for experiments, whether enabled or not [\#923](https://github.com/TextureGroup/Texture/pull/923) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextNode2\] Upgrade lock safety by protecting all ivars \(including rarely-changed ones\). [\#918](https://github.com/TextureGroup/Texture/pull/918) ([appleguy](https://github.com/appleguy)) +- \[ASCollectionNode/ASTableNode\] Fix a crash occurs while remeasuring cell nodes [\#917](https://github.com/TextureGroup/Texture/pull/917) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDisplayNode\] Improve thread-safety of didExitHierarchy \#trivial [\#916](https://github.com/TextureGroup/Texture/pull/916) ([nguyenhuy](https://github.com/nguyenhuy)) +- Prevent UITextView from updating contentOffset while deallocating [\#915](https://github.com/TextureGroup/Texture/pull/915) ([maicki](https://github.com/maicki)) +- Fix ASDKgram-Swift to avoid 'error parsing JSON within PhotoModel Init' [\#913](https://github.com/TextureGroup/Texture/pull/913) ([kenstir](https://github.com/kenstir)) +- \#trivial Add forgotten experiment into Schemas/configuration.json [\#912](https://github.com/TextureGroup/Texture/pull/912) ([garrettmoon](https://github.com/garrettmoon)) +- \#trivial Fix the C++ assertion [\#911](https://github.com/TextureGroup/Texture/pull/911) ([garrettmoon](https://github.com/garrettmoon)) +- Add 'iDiva - Beauty & Wedding tips' to Showcase [\#909](https://github.com/TextureGroup/Texture/pull/909) ([sudhanshutil](https://github.com/sudhanshutil)) +- Issue ASNetworkImageNode callbacks off main thread [\#908](https://github.com/TextureGroup/Texture/pull/908) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASTextNode\] Fix a deadlock that could occur when enabling experimental ASTextNode2 via ASConfiguration [\#903](https://github.com/TextureGroup/Texture/pull/903) ([appleguy](https://github.com/appleguy)) +- \[Docs\] Add new lightning talk from Buffer \#trivial [\#902](https://github.com/TextureGroup/Texture/pull/902) ([ay8s](https://github.com/ay8s)) +- Request std=c++11 dialect again, and add warning [\#900](https://github.com/TextureGroup/Texture/pull/900) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextNode\] Check variables before calling delegate method \#trivial [\#898](https://github.com/TextureGroup/Texture/pull/898) ([Jauzee](https://github.com/Jauzee)) +- ASDKFastImageNamed UIImage initializer nullability \#trivial [\#897](https://github.com/TextureGroup/Texture/pull/897) ([alexhillc](https://github.com/alexhillc)) +- \#trivial Fixes an issue where playback may not start [\#896](https://github.com/TextureGroup/Texture/pull/896) ([garrettmoon](https://github.com/garrettmoon)) +- Update configuration schema \#trivial [\#893](https://github.com/TextureGroup/Texture/pull/893) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- replace ` with code in containers-overview.md [\#884](https://github.com/TextureGroup/Texture/pull/884) ([everettjf](https://github.com/everettjf)) +- \[Docs\] Fix typos in layout specs section \#trivial [\#883](https://github.com/TextureGroup/Texture/pull/883) ([morozkin](https://github.com/morozkin)) +- Match interfacestate update sequence to uikit [\#882](https://github.com/TextureGroup/Texture/pull/882) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Add experiment to skip creating UIViews altogether for constants [\#881](https://github.com/TextureGroup/Texture/pull/881) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix ASDISPLAYNODE\_ASSERTIONS\_ENABLED and ASDefaultPlaybackButton warnings \#trivial [\#880](https://github.com/TextureGroup/Texture/pull/880) ([maicki](https://github.com/maicki)) +- Fix macro definition for AS\_KDEBUG\_ENABLE producing warning \#trivial [\#879](https://github.com/TextureGroup/Texture/pull/879) ([andrewrohn](https://github.com/andrewrohn)) +- Fix pager node for interface coalescing [\#877](https://github.com/TextureGroup/Texture/pull/877) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Standardize Property Declaration Style in Core Classes [\#870](https://github.com/TextureGroup/Texture/pull/870) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[NoCopyRendering\] In non-VM case, use calloc to get a zerod buffer \#trivial [\#869](https://github.com/TextureGroup/Texture/pull/869) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Check in Xcode 9.3 "workspace checks" file [\#868](https://github.com/TextureGroup/Texture/pull/868) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove Redundant Atomic Store from Recursive Unfair Lock in Recursive Case \#trivial [\#867](https://github.com/TextureGroup/Texture/pull/867) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update Podspec [\#866](https://github.com/TextureGroup/Texture/pull/866) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Issue 838\] Update ASCeilPixelValue and ASRoundPixelValue [\#864](https://github.com/TextureGroup/Texture/pull/864) ([rcancro](https://github.com/rcancro)) +- Disable interface coalescing [\#862](https://github.com/TextureGroup/Texture/pull/862) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Introduce ASRecursiveUnfairLock and tests [\#858](https://github.com/TextureGroup/Texture/pull/858) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Make NSIndexSet+ASHelpers.h reference local \#trivial [\#857](https://github.com/TextureGroup/Texture/pull/857) ([dmaclach](https://github.com/dmaclach)) +- Make ASBatchContext lock-free \#trivial [\#854](https://github.com/TextureGroup/Texture/pull/854) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASNetworkImageNode\] Replace NSUUID sentinel with integer \#trivial [\#852](https://github.com/TextureGroup/Texture/pull/852) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Make objects conform to NSLocking [\#851](https://github.com/TextureGroup/Texture/pull/851) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Make cache support animated image [\#850](https://github.com/TextureGroup/Texture/pull/850) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[bugfix\] Align timing of interface coalescing and range update. \#trivial [\#847](https://github.com/TextureGroup/Texture/pull/847) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Update layout2-layout-element-properties.md [\#844](https://github.com/TextureGroup/Texture/pull/844) ([arielelkin](https://github.com/arielelkin)) +- Use NS\_RETURNS\_RETAINED macro to save time [\#843](https://github.com/TextureGroup/Texture/pull/843) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Handle nil backgroundColor in ASTextNode2 \#trivial [\#841](https://github.com/TextureGroup/Texture/pull/841) ([maicki](https://github.com/maicki)) +- Put back VM flag in ASCGImageBuffer [\#839](https://github.com/TextureGroup/Texture/pull/839) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Order items in XCode project navigator by name [\#835](https://github.com/TextureGroup/Texture/pull/835) ([OleksiyA](https://github.com/OleksiyA)) +- \[NoCopyRendering\] Use vm instead of malloc [\#833](https://github.com/TextureGroup/Texture/pull/833) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextNode2\] Fix background color drawing [\#831](https://github.com/TextureGroup/Texture/pull/831) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix Text Node Thread Sanitizer Warning [\#830](https://github.com/TextureGroup/Texture/pull/830) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- access view first before checking canBecome/Resign responder in becomeResponder methods [\#829](https://github.com/TextureGroup/Texture/pull/829) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[\#trivial\] fixes rendered image quality on networked image nodes whic… [\#826](https://github.com/TextureGroup/Texture/pull/826) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASTextNode\] Fix locking, add test for issue \#trivial [\#825](https://github.com/TextureGroup/Texture/pull/825) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[\#trivial\] I don't think we need this extra locked method. [\#824](https://github.com/TextureGroup/Texture/pull/824) ([garrettmoon](https://github.com/garrettmoon)) +- \#trivial Hopefully made this a bit more readable. [\#823](https://github.com/TextureGroup/Texture/pull/823) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASTextNode\] Avoid acquiring instance lock multiple times \#trivial [\#820](https://github.com/TextureGroup/Texture/pull/820) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Showcase\] Fix mensXP showcase and attach Vingle-Tech-Talk Medium [\#818](https://github.com/TextureGroup/Texture/pull/818) ([GeekTree0101](https://github.com/GeekTree0101)) +- \[ASDisplayNode\] Add unit tests for layout z-order changes \(with an open issue to fix\). [\#816](https://github.com/TextureGroup/Texture/pull/816) ([appleguy](https://github.com/appleguy)) +- \[ASDKGram Example\] image\_url has been changed from URL string to Array by 5… [\#813](https://github.com/TextureGroup/Texture/pull/813) ([kaar3k](https://github.com/kaar3k)) +- Replace pthread specifics with C11 thread-local variables [\#811](https://github.com/TextureGroup/Texture/pull/811) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Upgrade dangerfile [\#810](https://github.com/TextureGroup/Texture/pull/810) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASDisplayNode\] Fix an issue that causes a node to sometimes return an outdated calculated size or size range [\#808](https://github.com/TextureGroup/Texture/pull/808) ([nguyenhuy](https://github.com/nguyenhuy)) +- Avoid triggering main thread assertions in collection/table dealloc \#trivial [\#803](https://github.com/TextureGroup/Texture/pull/803) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update IGListKit dependency to allow for updated versions [\#802](https://github.com/TextureGroup/Texture/pull/802) ([johntmcintosh](https://github.com/johntmcintosh)) +- \[ASDisplayNode\] Consolidate main thread initialization and allow apps to invoke it manually instead of +load. [\#798](https://github.com/TextureGroup/Texture/pull/798) ([appleguy](https://github.com/appleguy)) +- \[ASWrapperCellNode\] Introduce a new class allowing more control of UIKit passthrough cells. [\#797](https://github.com/TextureGroup/Texture/pull/797) ([appleguy](https://github.com/appleguy)) +- Add missing scrollViewWillEndDragging passthrough delegate [\#796](https://github.com/TextureGroup/Texture/pull/796) ([xezero](https://github.com/xezero)) +- Fix ASTextNode2 is accessing backgroundColor off main while sizing / layout is happening [\#794](https://github.com/TextureGroup/Texture/pull/794) ([maicki](https://github.com/maicki)) +- \[ASTableNode & ASCollectionNode\] Keepalive reference for node if their view is necessarily alive \(has a superview\). [\#793](https://github.com/TextureGroup/Texture/pull/793) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[ASDisplayNode layout\] Fix an issue that sometimes causes a node's pending layout to not be applied [\#792](https://github.com/TextureGroup/Texture/pull/792) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASRangeController\] Fix stability of "minimum" rangeMode if the app has more than one layout before scrolling. [\#790](https://github.com/TextureGroup/Texture/pull/790) ([appleguy](https://github.com/appleguy)) +- Fix UIResponder handling with view backing ASDisplayNode [\#789](https://github.com/TextureGroup/Texture/pull/789) ([maicki](https://github.com/maicki)) +- New runloop queue to coalesce Interface state update calls. [\#788](https://github.com/TextureGroup/Texture/pull/788) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[Graphics contexts\] Retain the reference color space \#trivial [\#784](https://github.com/TextureGroup/Texture/pull/784) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Get CatDealsCollectionView example running again \#trivial [\#783](https://github.com/TextureGroup/Texture/pull/783) ([maicki](https://github.com/maicki)) +- Improve nullable annotations for \_ASDisplayLayer and \_ASDisplayView \#trivial [\#780](https://github.com/TextureGroup/Texture/pull/780) ([maicki](https://github.com/maicki)) +- \[ASDisplayNode\] Force a layout pass on a visible node as soon as it enters preload state [\#779](https://github.com/TextureGroup/Texture/pull/779) ([nguyenhuy](https://github.com/nguyenhuy)) +- Improve ASNetworkImageNode delegate callout behavior [\#778](https://github.com/TextureGroup/Texture/pull/778) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix capturing self in the block while loading image in ASNetworkImageNode [\#777](https://github.com/TextureGroup/Texture/pull/777) ([morozkin](https://github.com/morozkin)) +- Fix synchronous state of node if +viewClass or +layerClass is overwritten \#trivial [\#776](https://github.com/TextureGroup/Texture/pull/776) ([maicki](https://github.com/maicki)) +- Add support for providing additional info to network image node delegate [\#775](https://github.com/TextureGroup/Texture/pull/775) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Expose asyncdisplaykit\_node in \_ASDisplayView same as in \_ASDisplayLayer \#trivial [\#773](https://github.com/TextureGroup/Texture/pull/773) ([maicki](https://github.com/maicki)) +- Improve no-copy rendering experiment, remove +load method [\#771](https://github.com/TextureGroup/Texture/pull/771) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix typos in layout2-layoutspec-types.md \#trivial [\#770](https://github.com/TextureGroup/Texture/pull/770) ([morozkin](https://github.com/morozkin)) +- Update PINCache [\#769](https://github.com/TextureGroup/Texture/pull/769) ([justinswart](https://github.com/justinswart)) +- Fix misprint \#trivial [\#768](https://github.com/TextureGroup/Texture/pull/768) ([Flatout73](https://github.com/Flatout73)) +- NoCopyRendering experiment: Fix possible memory leak if image node rendering is canceled \#trivial [\#765](https://github.com/TextureGroup/Texture/pull/765) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Node tint color [\#764](https://github.com/TextureGroup/Texture/pull/764) ([ShogunPhyched](https://github.com/ShogunPhyched)) +- Revert "Faster collection operations" [\#759](https://github.com/TextureGroup/Texture/pull/759) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASPrimitiveTraitCollection\] Always treat preferredContentSize as a potential nil \#trivial [\#757](https://github.com/TextureGroup/Texture/pull/757) ([ypogribnyi](https://github.com/ypogribnyi)) +- Update subclassing.md [\#753](https://github.com/TextureGroup/Texture/pull/753) ([janechoi6](https://github.com/janechoi6)) +- \[ASDisplayNode\] Don't force a layout pass on a visible node that enters preload state [\#751](https://github.com/TextureGroup/Texture/pull/751) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fix the dangerfile [\#750](https://github.com/TextureGroup/Texture/pull/750) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASDisplayNode\] Always return the thread-safe cornerRadius property, even in slow CALayer rounding mode [\#749](https://github.com/TextureGroup/Texture/pull/749) ([nguyenhuy](https://github.com/nguyenhuy)) +- Faster collection operations [\#748](https://github.com/TextureGroup/Texture/pull/748) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Create a centralized configuration API [\#747](https://github.com/TextureGroup/Texture/pull/747) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update dangerfile for 2018 \#trivial [\#746](https://github.com/TextureGroup/Texture/pull/746) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Raise deployment target to iOS 9 [\#743](https://github.com/TextureGroup/Texture/pull/743) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add an experimental "no-copy" renderer [\#741](https://github.com/TextureGroup/Texture/pull/741) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fixed: completeBatchFetching is called on a background thread [\#731](https://github.com/TextureGroup/Texture/pull/731) ([aaronr93](https://github.com/aaronr93)) +- \[tvOS\] Fixes errors when building against tvOS SDK [\#728](https://github.com/TextureGroup/Texture/pull/728) ([alexhillc](https://github.com/alexhillc)) +- \[ASCellNode\] focusStyle mapping [\#727](https://github.com/TextureGroup/Texture/pull/727) ([alexhillc](https://github.com/alexhillc)) +- \[ASDisplayNode\] Provide safeAreaInsets and layoutMargins bridge [\#685](https://github.com/TextureGroup/Texture/pull/685) ([ypogribnyi](https://github.com/ypogribnyi)) +- \[ASTraitCollection\] Add missing properties to ASTraitCollection [\#625](https://github.com/TextureGroup/Texture/pull/625) ([ypogribnyi](https://github.com/ypogribnyi)) + +## [2.6](https://github.com/TextureGroup/Texture/tree/2.6) (2018-01-12) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.5.1...2.6) + +**Merged pull requests:** + +- Add MensXP to Showcase [\#739](https://github.com/TextureGroup/Texture/pull/739) ([sudhanshutil](https://github.com/sudhanshutil)) +- Enable collection node interactive moves [\#735](https://github.com/TextureGroup/Texture/pull/735) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add Blendle to our showcase page [\#721](https://github.com/TextureGroup/Texture/pull/721) ([nguyenhuy](https://github.com/nguyenhuy)) +- \#trivial Fixes image nodes being stuck not being able to download image [\#720](https://github.com/TextureGroup/Texture/pull/720) ([garrettmoon](https://github.com/garrettmoon)) +- Reimplement ASRectTable using unordered\_map to avoid obscure NSMapTable exception. [\#719](https://github.com/TextureGroup/Texture/pull/719) ([appleguy](https://github.com/appleguy)) +- Add missing flags for ASCollectionDelegate [\#718](https://github.com/TextureGroup/Texture/pull/718) ([ilyailya](https://github.com/ilyailya)) +- Add support for toggling logs off and back on at runtime [\#714](https://github.com/TextureGroup/Texture/pull/714) ([johntmcintosh](https://github.com/johntmcintosh)) +- \[Update Showcase\] Update Showcase, add Vingle very community [\#711](https://github.com/TextureGroup/Texture/pull/711) ([GeekTree0101](https://github.com/GeekTree0101)) +- \[ASCollectionElement\] Check for nil elements on ASTableView as well. [\#710](https://github.com/TextureGroup/Texture/pull/710) ([cesteban](https://github.com/cesteban)) +- Ensure an ASM enabled node applies its pending layout when enters preload state [\#706](https://github.com/TextureGroup/Texture/pull/706) ([nguyenhuy](https://github.com/nguyenhuy)) +- The ASDKgram example doesn't compile. [\#700](https://github.com/TextureGroup/Texture/pull/700) ([onato](https://github.com/onato)) +- Revert Adds support for specifying a quality indexed array of URLs [\#699](https://github.com/TextureGroup/Texture/pull/699) ([garrettmoon](https://github.com/garrettmoon)) +- Correct Synchronous Concurrency Talk Link [\#698](https://github.com/TextureGroup/Texture/pull/698) ([ay8s](https://github.com/ay8s)) +- \[ASDisplayNode+Layout\] Ensure a pending layout is applied once [\#695](https://github.com/TextureGroup/Texture/pull/695) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add missing \ tags in Layout API Sizing docs [\#691](https://github.com/TextureGroup/Texture/pull/691) ([richardhenry](https://github.com/richardhenry)) +- Fix bug that breaks ASNodeController docs page [\#690](https://github.com/TextureGroup/Texture/pull/690) ([richardhenry](https://github.com/richardhenry)) +- Add a recent talk by @smeis at CocoaHeadsNL [\#687](https://github.com/TextureGroup/Texture/pull/687) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update subtree-rasterization.md [\#679](https://github.com/TextureGroup/Texture/pull/679) ([WymzeeLabs](https://github.com/WymzeeLabs)) +- Update layer-backing.md [\#678](https://github.com/TextureGroup/Texture/pull/678) ([WymzeeLabs](https://github.com/WymzeeLabs)) +- \[iOS11\] Update project settings and fix errors [\#676](https://github.com/TextureGroup/Texture/pull/676) ([Eke](https://github.com/Eke)) +- Fix swift sample. [\#669](https://github.com/TextureGroup/Texture/pull/669) ([rwinzhang](https://github.com/rwinzhang)) +- Bugfix/fix yoga logging aligning api changes [\#668](https://github.com/TextureGroup/Texture/pull/668) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- Make it possible to map between sections even if they're empty [\#660](https://github.com/TextureGroup/Texture/pull/660) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASCornerLayoutSpec\] New layout spec class for declarative corner element layout. [\#657](https://github.com/TextureGroup/Texture/pull/657) ([huang-kun](https://github.com/huang-kun)) +- Update layout2-layoutspec-types.md [\#655](https://github.com/TextureGroup/Texture/pull/655) ([TBXark](https://github.com/TBXark)) +- \[Minor Breaking API\] Make deallocation queues more reliable [\#651](https://github.com/TextureGroup/Texture/pull/651) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Make the framework backwards compatible with Xcode 8 [\#650](https://github.com/TextureGroup/Texture/pull/650) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Disable this test for now, it's too flakey and no one has time to inv… [\#649](https://github.com/TextureGroup/Texture/pull/649) ([garrettmoon](https://github.com/garrettmoon)) +- \[Documentation\] Update Inversion Docs [\#647](https://github.com/TextureGroup/Texture/pull/647) ([GeekTree0101](https://github.com/GeekTree0101)) +- Have ASNetworkImageNode report whether images were cached or not [\#639](https://github.com/TextureGroup/Texture/pull/639) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix a layout deadlock caused by holding the lock and going up the tree. [\#638](https://github.com/TextureGroup/Texture/pull/638) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASScrollNode\] Fix small bugs and add unit tests [\#637](https://github.com/TextureGroup/Texture/pull/637) ([nguyenhuy](https://github.com/nguyenhuy)) +- A couple performance tweaks for animated images \#trivial [\#634](https://github.com/TextureGroup/Texture/pull/634) ([garrettmoon](https://github.com/garrettmoon)) +- \[Documentation\] Update "Getting Started" page [\#633](https://github.com/TextureGroup/Texture/pull/633) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Tests\] Add test scrollToPageAtIndex ASPagerNode [\#629](https://github.com/TextureGroup/Texture/pull/629) ([remirobert](https://github.com/remirobert)) +- \[Tests\] Introducing tests for the ASTabBarController [\#628](https://github.com/TextureGroup/Texture/pull/628) ([remirobert](https://github.com/remirobert)) +- \[Tests\] Introducing tests for the ASNavigationController [\#627](https://github.com/TextureGroup/Texture/pull/627) ([remirobert](https://github.com/remirobert)) +- \[ASCollectionView\] Call -invalidateFlowLayoutDelegateMetrics when rotating. \#trivial [\#616](https://github.com/TextureGroup/Texture/pull/616) ([appleguy](https://github.com/appleguy)) +- Add unit tests for the layout engine [\#424](https://github.com/TextureGroup/Texture/pull/424) ([Adlai-Holler](https://github.com/Adlai-Holler)) + +## [2.5.1](https://github.com/TextureGroup/Texture/tree/2.5.1) (2017-10-24) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.5...2.5.1) + +**Merged pull requests:** + +- Dispatch batch update to main \#trivial [\#626](https://github.com/TextureGroup/Texture/pull/626) ([garrettmoon](https://github.com/garrettmoon)) +- Check if we need to do a batch update [\#624](https://github.com/TextureGroup/Texture/pull/624) ([garrettmoon](https://github.com/garrettmoon)) +- Fix naming conflict with YYText \#trivial [\#623](https://github.com/TextureGroup/Texture/pull/623) ([maicki](https://github.com/maicki)) +- Fix "This block and function declaration is not a prototype" warning. [\#619](https://github.com/TextureGroup/Texture/pull/619) ([mbesnili](https://github.com/mbesnili)) +- update Pinterest CDN URL in example code [\#613](https://github.com/TextureGroup/Texture/pull/613) ([derekargueta](https://github.com/derekargueta)) +- \[ASTextKitComponents\] Make sure Main Thread Checker isn't triggered during background calculations \#trivial [\#612](https://github.com/TextureGroup/Texture/pull/612) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASTextKitComponents\] Temporary components can be deallocated off main \#trivial [\#610](https://github.com/TextureGroup/Texture/pull/610) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update layout2-layoutspec-types.md [\#608](https://github.com/TextureGroup/Texture/pull/608) ([olcayertas](https://github.com/olcayertas)) +- Don't set download results if no longer in preload range. [\#606](https://github.com/TextureGroup/Texture/pull/606) ([garrettmoon](https://github.com/garrettmoon)) +- Animated WebP support [\#605](https://github.com/TextureGroup/Texture/pull/605) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASVideoNode\] Time observer fix [\#604](https://github.com/TextureGroup/Texture/pull/604) ([flovouin](https://github.com/flovouin)) +- Add assertion in dealloc that it is on main in ASTextKitComponents \#trivial [\#603](https://github.com/TextureGroup/Texture/pull/603) ([maicki](https://github.com/maicki)) +- ASTextKitComponents needs to be deallocated on main [\#598](https://github.com/TextureGroup/Texture/pull/598) ([maicki](https://github.com/maicki)) +- update faq toc links to match the generated html id \#trivial [\#597](https://github.com/TextureGroup/Texture/pull/597) ([romankl](https://github.com/romankl)) +- \[PINCache\] Set a default .byteLimit to reduce disk usage & startup time. [\#595](https://github.com/TextureGroup/Texture/pull/595) ([appleguy](https://github.com/appleguy)) +- Move clearing out of ASTextKitComponents property delegates into ASTextKitComponents dealloc \#trivial [\#591](https://github.com/TextureGroup/Texture/pull/591) ([maicki](https://github.com/maicki)) +- Clear ivar after scheduling for main thread deallocation \#trivial [\#590](https://github.com/TextureGroup/Texture/pull/590) ([maicki](https://github.com/maicki)) +- Use Nil for "no class" instead of nil \#trivial [\#589](https://github.com/TextureGroup/Texture/pull/589) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update showcase.md [\#587](https://github.com/TextureGroup/Texture/pull/587) ([hannahmbanana](https://github.com/hannahmbanana)) +- Rolling back CI to known version for now [\#585](https://github.com/TextureGroup/Texture/pull/585) ([garrettmoon](https://github.com/garrettmoon)) +- Use node lock instead of separate one to avoid deadlocks. [\#582](https://github.com/TextureGroup/Texture/pull/582) ([garrettmoon](https://github.com/garrettmoon)) +- \[\_ASPendingState\] Make sure accessibility strings are not nil before allocating attributed strings for them \#trivial [\#581](https://github.com/TextureGroup/Texture/pull/581) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASTextNode\] Implement an example comparing ASTextNode 1 & 2 behavior. [\#570](https://github.com/TextureGroup/Texture/pull/570) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[ASCollectionView\] Fix index space translation of Flow Layout Delegate methods. [\#467](https://github.com/TextureGroup/Texture/pull/467) ([appleguy](https://github.com/appleguy)) +- \[ASCollectionView\] Improve performance and behavior of rotation / bounds changes. [\#431](https://github.com/TextureGroup/Texture/pull/431) ([appleguy](https://github.com/appleguy)) + +## [2.5](https://github.com/TextureGroup/Texture/tree/2.5) (2017-09-26) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/v2.5...2.5) + +**Merged pull requests:** + +- Fix crashes caused by failing to unlock or destroy a static mutex while the app is being terminated [\#577](https://github.com/TextureGroup/Texture/pull/577) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update yoga version [\#569](https://github.com/TextureGroup/Texture/pull/569) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[ASDKgram Example\] fix crash on startup [\#566](https://github.com/TextureGroup/Texture/pull/566) ([hannahmbanana](https://github.com/hannahmbanana)) +- Added attributed versions of accessibilityLabel, accessibilityHint, accessibilityValue [\#554](https://github.com/TextureGroup/Texture/pull/554) ([fruitcoder](https://github.com/fruitcoder)) +- \[Yoga\] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [\#469](https://github.com/TextureGroup/Texture/pull/469) ([appleguy](https://github.com/appleguy)) +- \[ASCornerRounding\] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [\#465](https://github.com/TextureGroup/Texture/pull/465) ([appleguy](https://github.com/appleguy)) +- \[ASElementMap\] Fix indexPath's section or item is actually negative \#trivial [\#457](https://github.com/TextureGroup/Texture/pull/457) ([Anyewuya](https://github.com/Anyewuya)) + +## [v2.5](https://github.com/TextureGroup/Texture/tree/v2.5) (2017-09-14) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.4...v2.5) + +**Merged pull requests:** + +- Fix -\[ASPagerNode view\] triggering pendingState + nodeLoaded assert \#trivial [\#564](https://github.com/TextureGroup/Texture/pull/564) ([samhsiung](https://github.com/samhsiung)) +- \[ASCollectionLayout\] Exclude content inset on scrollable directions from viewport size [\#562](https://github.com/TextureGroup/Texture/pull/562) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASImageNode\] Always dealloc images in a background queue [\#561](https://github.com/TextureGroup/Texture/pull/561) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASCollectionNode\]\[ASTableNode\] Add content inset bridging property [\#560](https://github.com/TextureGroup/Texture/pull/560) ([nguyenhuy](https://github.com/nguyenhuy)) +- Mark ASRunLoopQueue as drained if it contains only NULLs [\#558](https://github.com/TextureGroup/Texture/pull/558) ([cesteban](https://github.com/cesteban)) +- Adds support for specifying a quality indexed array of URLs [\#557](https://github.com/TextureGroup/Texture/pull/557) ([garrettmoon](https://github.com/garrettmoon)) +- Make ASWeakMapEntry Value Atomic [\#555](https://github.com/TextureGroup/Texture/pull/555) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASDisplayNode\] Deprecate -displayWillStart in favor of -displayWillStartAsynchronously: [\#536](https://github.com/TextureGroup/Texture/pull/536) ([nguyenhuy](https://github.com/nguyenhuy)) +- SEP-491 prerequisite: add textViewShouldBeginEditing: to ASEditableTextNodeDelegate [\#535](https://github.com/TextureGroup/Texture/pull/535) ([yans](https://github.com/yans)) +- \[Gallery layout\] Include the caller in properties providing methods [\#533](https://github.com/TextureGroup/Texture/pull/533) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDisplayNode\] Notify rasterized subnodes that render pass has completed [\#532](https://github.com/TextureGroup/Texture/pull/532) ([smeis](https://github.com/smeis)) +- \[Cleanup\] Remove deprecated APIs [\#529](https://github.com/TextureGroup/Texture/pull/529) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add a function to disable all logging at runtime [\#528](https://github.com/TextureGroup/Texture/pull/528) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Table and collection views\] Consider content inset when calculating \(default\) element size range [\#525](https://github.com/TextureGroup/Texture/pull/525) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASCollectionNode\] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. [\#522](https://github.com/TextureGroup/Texture/pull/522) ([appleguy](https://github.com/appleguy)) +- ASImageNode+AnimatedImage playbackReadyCallback retain cycle [\#520](https://github.com/TextureGroup/Texture/pull/520) ([plarson](https://github.com/plarson)) +- \[CI\] BuildKite to ignore all markdown files [\#517](https://github.com/TextureGroup/Texture/pull/517) ([nguyenhuy](https://github.com/nguyenhuy)) +- ASCollectionLayout improvements [\#513](https://github.com/TextureGroup/Texture/pull/513) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update changelog and podspec for 2.4 [\#512](https://github.com/TextureGroup/Texture/pull/512) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- ASCollectionLayout to return a zero content size if its state is unavailable [\#509](https://github.com/TextureGroup/Texture/pull/509) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update corner-rounding.md [\#482](https://github.com/TextureGroup/Texture/pull/482) ([oferRounds](https://github.com/oferRounds)) +- \[Accessibility\] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. [\#468](https://github.com/TextureGroup/Texture/pull/468) ([appleguy](https://github.com/appleguy)) +- \[ASImageNode\] Enable .clipsToBounds by default \(fix .cornerRadius, GIFs overflow\). [\#466](https://github.com/TextureGroup/Texture/pull/466) ([appleguy](https://github.com/appleguy)) + +## [2.4](https://github.com/TextureGroup/Texture/tree/2.4) (2017-08-15) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.3.4...2.4) + +**Merged pull requests:** + +- Avoid re-entrant call to self.view when applying initial pending state [\#510](https://github.com/TextureGroup/Texture/pull/510) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[examples/ASCollectionView\] Register supplementary kinds \#trivial [\#508](https://github.com/TextureGroup/Texture/pull/508) ([nguyenhuy](https://github.com/nguyenhuy)) +- Rename the field again to nodeModel [\#504](https://github.com/TextureGroup/Texture/pull/504) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Rename -\[ASCellNode viewModel\] to -\[ASCellNode nodeViewModel\] to avoid collisions [\#499](https://github.com/TextureGroup/Texture/pull/499) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fixed typo `UIKIt` [\#497](https://github.com/TextureGroup/Texture/pull/497) ([nixzhu](https://github.com/nixzhu)) +- Improvements in ASCollectionGalleryLayoutDelegate [\#496](https://github.com/TextureGroup/Texture/pull/496) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Showcase\] Update showcase - add blog post link to ClassDojo icon \#trivial [\#493](https://github.com/TextureGroup/Texture/pull/493) ([Kaspik](https://github.com/Kaspik)) +- \[ASCoreAnimationExtras\] Update documentation for resizbale images \#trivial [\#492](https://github.com/TextureGroup/Texture/pull/492) ([Kaspik](https://github.com/Kaspik)) +- \[ASStackLayoutSpec\] Fix interitem spacing not being reset on new lines and add snapshot tests \#trivial [\#491](https://github.com/TextureGroup/Texture/pull/491) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Layout Transition\] Avoid calling didComplete method if pending layout transition is nil [\#490](https://github.com/TextureGroup/Texture/pull/490) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[LayoutTransition\] Call \_locked\_constrainedSizeForLayoutPass with the lock actually held \#trivial [\#488](https://github.com/TextureGroup/Texture/pull/488) ([nguyenhuy](https://github.com/nguyenhuy)) +- iOS 11 UITableView automatic height estimation fix [\#485](https://github.com/TextureGroup/Texture/pull/485) ([christianselig](https://github.com/christianselig)) +- Update scroll-node.md [\#484](https://github.com/TextureGroup/Texture/pull/484) ([oferRounds](https://github.com/oferRounds)) +- Update adoption-guide-2-0-beta1.md [\#483](https://github.com/TextureGroup/Texture/pull/483) ([oferRounds](https://github.com/oferRounds)) +- Update subclassing.md [\#479](https://github.com/TextureGroup/Texture/pull/479) ([oferRounds](https://github.com/oferRounds)) +- Invalidate layouts more aggressively when transitioning with animation [\#476](https://github.com/TextureGroup/Texture/pull/476) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update image-modification-block.md [\#474](https://github.com/TextureGroup/Texture/pull/474) ([oferRounds](https://github.com/oferRounds)) +- \[ASStackLayoutSpec\] Flex wrap fix and lineSpacing property [\#472](https://github.com/TextureGroup/Texture/pull/472) ([flovouin](https://github.com/flovouin)) +- \[ASNodeController\] Add -nodeDidLayout callback. Allow switching retain behavior at runtime. [\#470](https://github.com/TextureGroup/Texture/pull/470) ([appleguy](https://github.com/appleguy)) +- \[Layout transition\] Invalidate calculated layout if transitioning using the same size range [\#464](https://github.com/TextureGroup/Texture/pull/464) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASTableNode\]\[ASCollectionNode\] Add content offset bridging property [\#460](https://github.com/TextureGroup/Texture/pull/460) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDisplayNode\] Fix infinite layout loop [\#455](https://github.com/TextureGroup/Texture/pull/455) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add ASPagerNode+Beta to umbrella header \#trivial [\#454](https://github.com/TextureGroup/Texture/pull/454) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASPagerNode\] Remove unused flow layout reference \#trivial [\#452](https://github.com/TextureGroup/Texture/pull/452) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASCollectionLayout\] Add ASCollectionGalleryLayoutSizeProviding [\#451](https://github.com/TextureGroup/Texture/pull/451) ([nguyenhuy](https://github.com/nguyenhuy)) +- fix SIMULATE\_WEB\_RESPONSE not imported \#449 [\#450](https://github.com/TextureGroup/Texture/pull/450) ([wsdwsd0829](https://github.com/wsdwsd0829)) +- \[ASDataController \] Merge willUpdateWithChangeSet and didUpdateWithChangeSet delegate methods \#trivial [\#445](https://github.com/TextureGroup/Texture/pull/445) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDataController\] Clean up [\#443](https://github.com/TextureGroup/Texture/pull/443) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDataController\] Avoid asking for size ranges of soon-to-be-delete elements during relayouts [\#442](https://github.com/TextureGroup/Texture/pull/442) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASCollectionView\] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [\#440](https://github.com/TextureGroup/Texture/pull/440) ([appleguy](https://github.com/appleguy)) +- \[ASDisplayNode\] Fix some gaps in the bridging of new contents\* properties. [\#435](https://github.com/TextureGroup/Texture/pull/435) ([appleguy](https://github.com/appleguy)) +- \[ASDisplayNode\] Allow setting stretchable contents on nodes; add bridged properties. \#trivial [\#429](https://github.com/TextureGroup/Texture/pull/429) ([appleguy](https://github.com/appleguy)) +- Use a sentinel NSUInteger for node layout data \#trivial [\#428](https://github.com/TextureGroup/Texture/pull/428) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Workaround clang4.0 \ initialization \#trivial [\#426](https://github.com/TextureGroup/Texture/pull/426) ([bkase](https://github.com/bkase)) +- Add missing import in ASDisplayNode+AsyncDisplay \#trivial [\#423](https://github.com/TextureGroup/Texture/pull/423) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASCollectionElement\] Add checks for nil element, prior to other PRs landing. [\#421](https://github.com/TextureGroup/Texture/pull/421) ([appleguy](https://github.com/appleguy)) +- \[ASDataController\] Apply new visible map inside batch updates block [\#420](https://github.com/TextureGroup/Texture/pull/420) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASVideoPlayerNode\] Check that the video player's delegate implements the didTapFullScreenButtonNode method before calling it \#trivial [\#418](https://github.com/TextureGroup/Texture/pull/418) ([tnev](https://github.com/tnev)) +- \[ASDataController\] Fix a crash in table view caused by executing an empty change set during layoutSubviews [\#416](https://github.com/TextureGroup/Texture/pull/416) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDisplayNode+Layout\] In layoutThatFits:, check and use \_pending layout if valid. [\#413](https://github.com/TextureGroup/Texture/pull/413) ([appleguy](https://github.com/appleguy)) +- Integrate Weaver into ASDKGram [\#412](https://github.com/TextureGroup/Texture/pull/412) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDisplayNode\] -didEnterPreloadState does not need to call -layoutIfNeeded \#trivial [\#411](https://github.com/TextureGroup/Texture/pull/411) ([appleguy](https://github.com/appleguy)) +- \[ASTextNode2\] Provide compiler flag to enable ASTextNode2 for all usages. [\#410](https://github.com/TextureGroup/Texture/pull/410) ([appleguy](https://github.com/appleguy)) +- \[Yoga\] Refine the handling of measurement functions when Yoga is used. [\#408](https://github.com/TextureGroup/Texture/pull/408) ([appleguy](https://github.com/appleguy)) +- \[ASCollectionView\] Small improvements [\#407](https://github.com/TextureGroup/Texture/pull/407) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Documentation\] Improve description of synchronous concurrency with screenshot and video link. [\#406](https://github.com/TextureGroup/Texture/pull/406) ([appleguy](https://github.com/appleguy)) +- Introduce ASIntegerMap, improve our changeset handling \#trivial [\#405](https://github.com/TextureGroup/Texture/pull/405) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix issue where supplementary elements don't track section changes [\#404](https://github.com/TextureGroup/Texture/pull/404) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Overhaul our logging, add activity tracing support. [\#399](https://github.com/TextureGroup/Texture/pull/399) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextNode2\] Add initial implementation for link handling. [\#396](https://github.com/TextureGroup/Texture/pull/396) ([appleguy](https://github.com/appleguy)) +- Introduce ASCollectionGalleryLayoutDelegate [\#76](https://github.com/TextureGroup/Texture/pull/76) ([nguyenhuy](https://github.com/nguyenhuy)) + +## [2.3.4](https://github.com/TextureGroup/Texture/tree/2.3.4) (2017-06-30) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.3.3...2.3.4) + +**Merged pull requests:** + +- Update to the latest betas of PINRemoteImage and PINCache [\#403](https://github.com/TextureGroup/Texture/pull/403) ([garrettmoon](https://github.com/garrettmoon)) +- A bit of minor cleanup \#trivial [\#402](https://github.com/TextureGroup/Texture/pull/402) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASLayout\] Revisit the flattening algorithm [\#395](https://github.com/TextureGroup/Texture/pull/395) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASLayout\] If a layout has no sublayouts, don't bother initializing its rect table [\#394](https://github.com/TextureGroup/Texture/pull/394) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASLayout\] Fix documentation of retainSublayoutLayoutElements \#trivial [\#393](https://github.com/TextureGroup/Texture/pull/393) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fix compiling ASDimension if Yoga enabled \#trivial [\#389](https://github.com/TextureGroup/Texture/pull/389) ([maicki](https://github.com/maicki)) +- comments to reflect code \#trivial [\#388](https://github.com/TextureGroup/Texture/pull/388) ([benjamin-chang](https://github.com/benjamin-chang)) +- \[ASCellNode\] Remove unnecessary frame setting \#trivial [\#387](https://github.com/TextureGroup/Texture/pull/387) ([nguyenhuy](https://github.com/nguyenhuy)) +- Horrible spelling mistake \#trivial [\#384](https://github.com/TextureGroup/Texture/pull/384) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fix for Video Table Example Building [\#383](https://github.com/TextureGroup/Texture/pull/383) ([ay8s](https://github.com/ay8s)) +- ASDimensionMake to be more lenient \#trivial [\#382](https://github.com/TextureGroup/Texture/pull/382) ([nguyenhuy](https://github.com/nguyenhuy)) +- Gate orphaned node detector behind YOGA flag \#trivial [\#380](https://github.com/TextureGroup/Texture/pull/380) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Event Log\] Log ASM flag when modify subnodes \#trivial [\#379](https://github.com/TextureGroup/Texture/pull/379) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add new workspaces for tests for different integrations \#trivial [\#377](https://github.com/TextureGroup/Texture/pull/377) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix imageModificationBlock doc \#trivial [\#376](https://github.com/TextureGroup/Texture/pull/376) ([maicki](https://github.com/maicki)) +- Fix double-load issue with ASCollectionNode [\#372](https://github.com/TextureGroup/Texture/pull/372) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- FIXED Typo in Layout Transition Documentation [\#371](https://github.com/TextureGroup/Texture/pull/371) ([martinjkelly](https://github.com/martinjkelly)) +- \[Yoga\] Delete YOGA\_TREE\_CONTIGOUS gating and permanently enable. \#trivial [\#370](https://github.com/TextureGroup/Texture/pull/370) ([appleguy](https://github.com/appleguy)) +- \[Yoga\] Minimize number of nodes that have MeasureFunc set on them. [\#369](https://github.com/TextureGroup/Texture/pull/369) ([appleguy](https://github.com/appleguy)) +- Improve System Trace Implementation \#trivial [\#368](https://github.com/TextureGroup/Texture/pull/368) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Updates ASDKGram to use IGListKit 3.0.0 [\#367](https://github.com/TextureGroup/Texture/pull/367) ([ay8s](https://github.com/ay8s)) +- Update link to AsyncDisplayKit 2.0 Launch Talk [\#363](https://github.com/TextureGroup/Texture/pull/363) ([appleguy](https://github.com/appleguy)) +- \[ASTableView\] Use ASTableView tableNode property instead of calling ASViewToDisplayNode \#trivial [\#361](https://github.com/TextureGroup/Texture/pull/361) ([maicki](https://github.com/maicki)) +- Add section-object support to new tests, improve test confinement. \#trivial [\#360](https://github.com/TextureGroup/Texture/pull/360) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Docs\] Update 'Corner Rounding' document for Texture 2 [\#359](https://github.com/TextureGroup/Texture/pull/359) ([ArchimboldiMao](https://github.com/ArchimboldiMao)) +- Add support for keeping letting cell nodes update to new view models when reloaded. \#trivial [\#357](https://github.com/TextureGroup/Texture/pull/357) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add first-pass view model support to collection node. \#trivial [\#356](https://github.com/TextureGroup/Texture/pull/356) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTraitCollection\] Convert ASPrimitiveTraitCollection from lock to atomic. [\#355](https://github.com/TextureGroup/Texture/pull/355) ([appleguy](https://github.com/appleguy)) +- Improve collection node testing, reveal double-load issue. \#trivial [\#352](https://github.com/TextureGroup/Texture/pull/352) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix title in changelog [\#350](https://github.com/TextureGroup/Texture/pull/350) ([levi](https://github.com/levi)) +- Add a Flag to Disable Main Thread Assertions \#trivial [\#348](https://github.com/TextureGroup/Texture/pull/348) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Migrate to Latest OCMock, Demonstrate Improved Unit Testing [\#347](https://github.com/TextureGroup/Texture/pull/347) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Upgrade ASLayoutElementContext to an Object \#trivial [\#344](https://github.com/TextureGroup/Texture/pull/344) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Yoga\] Rewrite YOGA\_TREE\_CONTIGUOUS mode with improved behavior and cleaner integration [\#343](https://github.com/TextureGroup/Texture/pull/343) ([appleguy](https://github.com/appleguy)) +- Fix internal Linter warnings \#trivial [\#340](https://github.com/TextureGroup/Texture/pull/340) ([maicki](https://github.com/maicki)) +- Small changes required by the coming layout debugger [\#337](https://github.com/TextureGroup/Texture/pull/337) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASDataController\] Add event logging for transaction queue flush duration \#trivial [\#334](https://github.com/TextureGroup/Texture/pull/334) ([hannahmbanana](https://github.com/hannahmbanana)) +- \[ASCollectionView\] synchronous mode [\#332](https://github.com/TextureGroup/Texture/pull/332) ([hannahmbanana](https://github.com/hannahmbanana)) +- \[Performance\] Convert ASLayoutElementSize to atomic \#trivial [\#331](https://github.com/TextureGroup/Texture/pull/331) ([hannahmbanana](https://github.com/hannahmbanana)) +- \[Yoga\] Refer to proper path name and use module import [\#306](https://github.com/TextureGroup/Texture/pull/306) ([weibel](https://github.com/weibel)) +- \[ASImageNode\] Add documentation for image effects \#trivial [\#263](https://github.com/TextureGroup/Texture/pull/263) ([maicki](https://github.com/maicki)) + +## [2.3.3](https://github.com/TextureGroup/Texture/tree/2.3.3) (2017-06-06) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.3.2...2.3.3) + +**Merged pull requests:** + +- Updating to 2.3.3 \#trivial [\#338](https://github.com/TextureGroup/Texture/pull/338) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASDisplayNode+Layout\] Add check for orphaned nodes after layout transition to clean up. [\#336](https://github.com/TextureGroup/Texture/pull/336) ([appleguy](https://github.com/appleguy)) +- Update PINRemoteImage [\#328](https://github.com/TextureGroup/Texture/pull/328) ([garrettmoon](https://github.com/garrettmoon)) +- Fix typo \#trivial [\#327](https://github.com/TextureGroup/Texture/pull/327) ([vitalybaev](https://github.com/vitalybaev)) +- Fixes an issue with GIFs that would always be covered by their placeh… [\#326](https://github.com/TextureGroup/Texture/pull/326) ([garrettmoon](https://github.com/garrettmoon)) +- Replace NSMutableSet with NSHashTable when Appropriate \#trivial [\#321](https://github.com/TextureGroup/Texture/pull/321) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Cleanup\] Small fixes to improve conformance for strict compiler settings \#trivial [\#320](https://github.com/TextureGroup/Texture/pull/320) ([appleguy](https://github.com/appleguy)) +- Rejigger Cell Visibility Tracking [\#317](https://github.com/TextureGroup/Texture/pull/317) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Clean Up ASAsyncTransaction \#trivial [\#316](https://github.com/TextureGroup/Texture/pull/316) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Clean Up ASDisplayLayer \#trivial [\#315](https://github.com/TextureGroup/Texture/pull/315) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASDisplayNode\] Revise assertion to log until Issue \#145 is addressed. \#trivial [\#313](https://github.com/TextureGroup/Texture/pull/313) ([appleguy](https://github.com/appleguy)) +- \[Docs\] Fixed typo in carthage project name \#trivial [\#310](https://github.com/TextureGroup/Texture/pull/310) ([george-gw](https://github.com/george-gw)) +- Fix non layout [\#309](https://github.com/TextureGroup/Texture/pull/309) ([garrettmoon](https://github.com/garrettmoon)) +- Catch Invalid Layer Bounds in a Nonfatal Assertion \#trivial [\#308](https://github.com/TextureGroup/Texture/pull/308) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASCollectionNode\] Fix missing properties and layoutInspector \#trivial [\#305](https://github.com/TextureGroup/Texture/pull/305) ([flovouin](https://github.com/flovouin)) +- \[Examples\] Fixed crash on SocialAppLayout-Inverted + behaviour comments [\#304](https://github.com/TextureGroup/Texture/pull/304) ([dimazen](https://github.com/dimazen)) +- IGListKit related headers need to be in the module all time now \#trivial [\#300](https://github.com/TextureGroup/Texture/pull/300) ([maicki](https://github.com/maicki)) +- \[ASDisplayNode\] Remove assertion in calculateSizeThatFits: and log an event \#trivial [\#299](https://github.com/TextureGroup/Texture/pull/299) ([maicki](https://github.com/maicki)) +- ASBatchFetching to not round scroll velocity \#trivial [\#294](https://github.com/TextureGroup/Texture/pull/294) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[ASVideoNodeDelegate\] fix for \#291 crash [\#292](https://github.com/TextureGroup/Texture/pull/292) ([SergeyPetrachkov](https://github.com/SergeyPetrachkov)) +- Fix Alignment of Hashed Structs [\#287](https://github.com/TextureGroup/Texture/pull/287) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[IGListKit\] Add IGListKit headers to public section of Xcode project [\#286](https://github.com/TextureGroup/Texture/pull/286) ([maicki](https://github.com/maicki)) +- Only call -layout and -layoutDidFinish if the node is already loaded [\#285](https://github.com/TextureGroup/Texture/pull/285) ([nguyenhuy](https://github.com/nguyenhuy)) +- \[Batch Fetching\] Add ASBatchFetchingDelegate [\#281](https://github.com/TextureGroup/Texture/pull/281) ([nguyenhuy](https://github.com/nguyenhuy)) +- Ignore Relayout Requests for Deleted Cell Nodes [\#279](https://github.com/TextureGroup/Texture/pull/279) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Remove Unused Node Code \#trivial [\#278](https://github.com/TextureGroup/Texture/pull/278) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix Documentation Warnings \#trivial [\#276](https://github.com/TextureGroup/Texture/pull/276) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Examples\] Fix LayoutSpecExamples and LayoutSpecExamples-Swift: image URLs were still pointing to asyncdisplaykit.org [\#275](https://github.com/TextureGroup/Texture/pull/275) ([cesteban](https://github.com/cesteban)) +- \[Layout\] Extract layout implementation code into it's own subcategories [\#272](https://github.com/TextureGroup/Texture/pull/272) ([maicki](https://github.com/maicki)) +- Fix Release Builds \#trivial [\#271](https://github.com/TextureGroup/Texture/pull/271) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Yoga\] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga. [\#270](https://github.com/TextureGroup/Texture/pull/270) ([appleguy](https://github.com/appleguy)) +- Simplify Layout Transition State \#trivial [\#269](https://github.com/TextureGroup/Texture/pull/269) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Store ASLayoutElementContext in Thread-Local Storage \#trivial [\#268](https://github.com/TextureGroup/Texture/pull/268) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Examples\] Fix a couple of examples due to API changes recently \#trivial [\#267](https://github.com/TextureGroup/Texture/pull/267) ([maicki](https://github.com/maicki)) +- Fix Collection Item Index Path Conversion [\#262](https://github.com/TextureGroup/Texture/pull/262) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- added error reporting callback to ASVideoNode [\#260](https://github.com/TextureGroup/Texture/pull/260) ([SergeyPetrachkov](https://github.com/SergeyPetrachkov)) +- Add Experimental Text Node Implementation [\#259](https://github.com/TextureGroup/Texture/pull/259) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add missing import and define in ASLog \#trivial [\#257](https://github.com/TextureGroup/Texture/pull/257) ([nguyenhuy](https://github.com/nguyenhuy)) +- Simplify Override Checking, Only Do It When Assertions Are Enabled \#trivial [\#253](https://github.com/TextureGroup/Texture/pull/253) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASTextKitFontSizeAdjuster\] Replace use of boundingRectWithSize:options:context: with boundingRectForGlyphRange: inTextContainer: [\#251](https://github.com/TextureGroup/Texture/pull/251) ([rcancro](https://github.com/rcancro)) +- Improve Ancestry Handling, Avoid Assertion Failure [\#246](https://github.com/TextureGroup/Texture/pull/246) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[Yoga\] Increment Yoga version to current, 1.5.0. [\#91](https://github.com/TextureGroup/Texture/pull/91) ([appleguy](https://github.com/appleguy)) +- \[example/CustomCollectionView\] Implement MosaicCollectionLayoutDelegate [\#28](https://github.com/TextureGroup/Texture/pull/28) ([nguyenhuy](https://github.com/nguyenhuy)) + +## [2.3.2](https://github.com/TextureGroup/Texture/tree/2.3.2) (2017-05-09) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.3.1...2.3.2) + +**Merged pull requests:** + +- \[ASDisplayNode\] Pass drawParameter in rendering context callbacks [\#248](https://github.com/TextureGroup/Texture/pull/248) ([maicki](https://github.com/maicki)) +- Assert only once we know URL has changed [\#247](https://github.com/TextureGroup/Texture/pull/247) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASImageNode\] Move to class method of displayWithParameters:isCancelled: for drawing [\#244](https://github.com/TextureGroup/Texture/pull/244) ([maicki](https://github.com/maicki)) +- Don't Use Associated Objects for Drawing Priority \#trivial [\#239](https://github.com/TextureGroup/Texture/pull/239) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASDimension\] Remove warning about float precision using CGFloat and … \#trivial [\#237](https://github.com/TextureGroup/Texture/pull/237) ([amegias](https://github.com/amegias)) +- \[ASImageNode\] Move debug label and will- / didDisplayNodeContentWithRenderingContext out of drawing method \#trivial [\#235](https://github.com/TextureGroup/Texture/pull/235) ([maicki](https://github.com/maicki)) +- Fixes assertion on startup in social app layout example [\#233](https://github.com/TextureGroup/Texture/pull/233) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASTextNode\] Move to class method of drawRect:withParameters:isCancelled:isRasterizing: for drawing [\#232](https://github.com/TextureGroup/Texture/pull/232) ([maicki](https://github.com/maicki)) +- \[ASImageNode\] Remove unneeded pointer star \#trivial [\#231](https://github.com/TextureGroup/Texture/pull/231) ([maicki](https://github.com/maicki)) +- \[ASTwoDimensionalArrayUtils\] Fix extern C function definition to fix compiler issue. \#trivial [\#229](https://github.com/TextureGroup/Texture/pull/229) ([appleguy](https://github.com/appleguy)) +- Move Last Few Properties from ASTableView,ASCollectionView to Node [\#225](https://github.com/TextureGroup/Texture/pull/225) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix Issues in the Project File \#trivial [\#224](https://github.com/TextureGroup/Texture/pull/224) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Improve Our Handling of Subnodes [\#223](https://github.com/TextureGroup/Texture/pull/223) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Extract ASLayoutElement and ASLayoutElementStylability into categories \#trivial [\#131](https://github.com/TextureGroup/Texture/pull/131) ([maicki](https://github.com/maicki)) +- \[Layout\] Remove finalLayoutElement [\#96](https://github.com/TextureGroup/Texture/pull/96) ([maicki](https://github.com/maicki)) +- \[Docs\] Add workaround for setting a custom lineSpacing and maxNumberOfLines to ASTextNode docs [\#92](https://github.com/TextureGroup/Texture/pull/92) ([maicki](https://github.com/maicki)) +- \[ASDisplayNode\] Implement a std::atomic-based flag system for superb performance [\#89](https://github.com/TextureGroup/Texture/pull/89) ([appleguy](https://github.com/appleguy)) +- \[Yoga\] Ensure that calculated layout is nil'd in invalidate\*Layout [\#87](https://github.com/TextureGroup/Texture/pull/87) ([appleguy](https://github.com/appleguy)) +- Simplify Hashing Code [\#86](https://github.com/TextureGroup/Texture/pull/86) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Fix site header [\#84](https://github.com/TextureGroup/Texture/pull/84) ([levi](https://github.com/levi)) +- Tighten Rasterization API, Undeprecate It [\#82](https://github.com/TextureGroup/Texture/pull/82) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Implement ASPageTable [\#81](https://github.com/TextureGroup/Texture/pull/81) ([nguyenhuy](https://github.com/nguyenhuy)) +- Make Cell Node Properties Atomic [\#74](https://github.com/TextureGroup/Texture/pull/74) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- \[ASNodeController+Beta\] Provide an option to allow nodes to own their controllers. [\#61](https://github.com/TextureGroup/Texture/pull/61) ([appleguy](https://github.com/appleguy)) +- \[Yoga Beta\] Improvements to the experimental support for Yoga layout. [\#59](https://github.com/TextureGroup/Texture/pull/59) ([appleguy](https://github.com/appleguy)) +- Fix issue with swipe to delete cell gesture. \#trivial [\#46](https://github.com/TextureGroup/Texture/pull/46) ([rewcraig](https://github.com/rewcraig)) +- Fix CustomCollectionView-Swift sample [\#22](https://github.com/TextureGroup/Texture/pull/22) ([george-gw](https://github.com/george-gw)) +- Automatically resume ASVideoNode after returning from background [\#13](https://github.com/TextureGroup/Texture/pull/13) ([plarson](https://github.com/plarson)) + +## [2.3.1](https://github.com/TextureGroup/Texture/tree/2.3.1) (2017-04-27) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.2.1...2.3.1) + +**Merged pull requests:** + +- Don't run tests for the docs directory. [\#79](https://github.com/TextureGroup/Texture/pull/79) ([garrettmoon](https://github.com/garrettmoon)) +- Fix SCSS build [\#78](https://github.com/TextureGroup/Texture/pull/78) ([levi](https://github.com/levi)) +- Fix documentation warning on ASCollectionLayoutState.h \#trivial [\#77](https://github.com/TextureGroup/Texture/pull/77) ([garrettmoon](https://github.com/garrettmoon)) +- ASLayoutSpec to use more default implementations \#trivial [\#73](https://github.com/TextureGroup/Texture/pull/73) ([nguyenhuy](https://github.com/nguyenhuy)) +- Update the CI to the new ruby version [\#71](https://github.com/TextureGroup/Texture/pull/71) ([garrettmoon](https://github.com/garrettmoon)) +- Missing a word [\#68](https://github.com/TextureGroup/Texture/pull/68) ([djblake](https://github.com/djblake)) +- Update license v2 [\#67](https://github.com/TextureGroup/Texture/pull/67) ([garrettmoon](https://github.com/garrettmoon)) +- \[ASCollectionView\] Prevent prefetching from being enabled to eliminate overhead. [\#65](https://github.com/TextureGroup/Texture/pull/65) ([appleguy](https://github.com/appleguy)) +- \[CGPointNull\] Rename globally exported C function to avoid collisions \#trivial [\#62](https://github.com/TextureGroup/Texture/pull/62) ([appleguy](https://github.com/appleguy)) +- \[RTL\] Bridge the UISemanticContentAttribute property for more convenient RTL support. [\#60](https://github.com/TextureGroup/Texture/pull/60) ([appleguy](https://github.com/appleguy)) +- Fixes a potential deadlock; it's not safe to message likely super nod… [\#56](https://github.com/TextureGroup/Texture/pull/56) ([garrettmoon](https://github.com/garrettmoon)) +- Fix \_\_has\_include check in ASLog.h \#trivial [\#55](https://github.com/TextureGroup/Texture/pull/55) ([fsmorygo](https://github.com/fsmorygo)) +- GLKit workaround \#trivial [\#54](https://github.com/TextureGroup/Texture/pull/54) ([stephenkopylov](https://github.com/stephenkopylov)) +- Layout debugger proposal [\#52](https://github.com/TextureGroup/Texture/pull/52) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fixes header check to accept the 'new' header for modified files. [\#50](https://github.com/TextureGroup/Texture/pull/50) ([garrettmoon](https://github.com/garrettmoon)) +- Remove References to IGListSectionType, Now that It's Gone [\#49](https://github.com/TextureGroup/Texture/pull/49) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Move doc stylesheets to sass [\#47](https://github.com/TextureGroup/Texture/pull/47) ([levi](https://github.com/levi)) +- Fixes for Dangerfile header checks [\#45](https://github.com/TextureGroup/Texture/pull/45) ([garrettmoon](https://github.com/garrettmoon)) +- Enforce header file changes [\#44](https://github.com/TextureGroup/Texture/pull/44) ([garrettmoon](https://github.com/garrettmoon)) +- Use \_ASCollectionReusableView inside ASIGListSupplementaryViewSourceMethods [\#40](https://github.com/TextureGroup/Texture/pull/40) ([plarson](https://github.com/plarson)) +- Bump Cartfile versions to match podspec [\#37](https://github.com/TextureGroup/Texture/pull/37) ([dymv](https://github.com/dymv)) +- \[Site\] Remove hero drop shadow [\#35](https://github.com/TextureGroup/Texture/pull/35) ([levi](https://github.com/levi)) +- Add announcement banner to documentation site [\#31](https://github.com/TextureGroup/Texture/pull/31) ([levi](https://github.com/levi)) +- \[ASCollectionLayout\] Manually set size to measured cells [\#24](https://github.com/TextureGroup/Texture/pull/24) ([nguyenhuy](https://github.com/nguyenhuy)) +- Create a Pluggable "Tips" System to Help in Development [\#19](https://github.com/TextureGroup/Texture/pull/19) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Add danger [\#18](https://github.com/TextureGroup/Texture/pull/18) ([garrettmoon](https://github.com/garrettmoon)) +- Fix Case where Network Image Node Stays Locked [\#17](https://github.com/TextureGroup/Texture/pull/17) ([Adlai-Holler](https://github.com/Adlai-Holler)) +- Update the homepage URL [\#10](https://github.com/TextureGroup/Texture/pull/10) ([garrettmoon](https://github.com/garrettmoon)) + +## [2.2.1](https://github.com/TextureGroup/Texture/tree/2.2.1) (2017-04-14) +[Full Changelog](https://github.com/TextureGroup/Texture/compare/2.3...2.2.1) + +**Merged pull requests:** + +- Add blog post [\#9](https://github.com/TextureGroup/Texture/pull/9) ([garrettmoon](https://github.com/garrettmoon)) + + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/submodules/AsyncDisplayKit/CI/build.sh b/submodules/AsyncDisplayKit/CI/build.sh new file mode 100755 index 0000000000..058cef0542 --- /dev/null +++ b/submodules/AsyncDisplayKit/CI/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eo pipefail + +./build.sh all \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/CI/exclude-from-build.json b/submodules/AsyncDisplayKit/CI/exclude-from-build.json new file mode 100644 index 0000000000..999e736a52 --- /dev/null +++ b/submodules/AsyncDisplayKit/CI/exclude-from-build.json @@ -0,0 +1,6 @@ +[ + "^plans/", + "^docs/", + "^CI/exclude-from-build.json$", + "^**/*.md$" +] diff --git a/submodules/AsyncDisplayKit/CONTRIBUTING.md b/submodules/AsyncDisplayKit/CONTRIBUTING.md new file mode 100644 index 0000000000..22aded2d6e --- /dev/null +++ b/submodules/AsyncDisplayKit/CONTRIBUTING.md @@ -0,0 +1,261 @@ +# Contribution Guidelines +Texture is the most actively developed open source project at [Pinterest](https://www.pinterest.com) and is used extensively to deliver a world-class Pinner experience. This document is setup to ensure contributing to the project is easy and transparent. + +# Questions + +If you are having difficulties using Texture or have a question about usage, please ask a +question in our [Slack channel](http://texturegroup.org/slack.html). **Please do not ask for help by filing Github issues.** + +# Core Team + +The Core Team reviews and helps iterate on the RFC Issues from the community at large and acting as the approver of these RFCs. Team members help drive Texture forward in a coherent direction consistent with the goal of creating the best possible general purpose UI framework for iOS. Team members will have merge permissions on the repository. + +Members of the core team are appointed based on their technical expertise and proven contribution to the community. The current core team members are: + +- Adlai Holler ([@](http://github.com/adlai-holler)[adlai-holler](http://github.com/adlai-holler)) +- Garrett Moon [(](https://github.com/garrettmoon)[@](https://github.com/garrettmoon)[garrett](https://github.com/garrettmoon)[moon](https://github.com/garrettmoon)) +- Huy Nguyen ([@](https://github.com/nguyenhuy)[nguyenhuy](https://github.com/nguyenhuy)) +- Michael Schneider ([@maicki](https://github.com/maicki)) +- Scott Goodson ([@appleguy](https://github.com/appleguy)) + +Over time, exceptional community members from a much more diverse background will be appointed based on their record of community involvement and contributions. + +# Issues + +Think you've found a bug or have a new feature to suggest? [Let us know](https://github.com/TextureGroup/Texture/issues/new)! + +## Where to Find Known Issues + +We use [GitHub Issues](https://github.com/texturegroup/texture/issues) for all bug tracking. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +## Reporting New Issues +1. Update to the most recent master release if possible. We may have already fixed your bug. +2. Search for similar [issues](https://github.com/TextureGroup/Texture/issues). It's possible somebody has encountered this bug already. +3. Provide a reduced test case that specifically shows the problem. This demo should be fully operational with the exception of the bug you want to demonstrate. The more pared down, the better. If it is not possible to produce a test case, please make sure you provide very specific steps to reproduce the error. If we cannot reproduce it, and there is no other evidence to help us figure out a fix we will close the issue. +4. Your issue will be verified. The provided example will be tested for correctness. The Texture team will work with you until your issue can be verified. +5. Keep up to date with feedback from the Texture team on your issue. Your issue may be closed if it becomes stale. +6. If possible, submit a Pull Request with a failing test. Better yet, take a stab at fixing the bug yourself if you can! + +The more information you provide, the easier it is for us to validate that there is a bug and the faster we'll be able to take action. + +## Issues Triaging +- You might be requested to provide a reproduction or extra information. In that case, the issue will be labeled as **N****eeds More Info**. If we did not get any response after fourteen days, we will ping you to remind you about it. We might close the issue if we do not hear from you after two weeks since the original notice. +- If you submit a feature request as a GitHub issue, you will be invited to follow the instructions in this section otherwise the issue will be closed. +- Issues that become inactive will be labelled accordingly to inform the original poster and Texture contributors that the issue should be closed since the issue is no longer actionable. The issue can be reopened at a later time if needed, e.g. becomes actionable again. +- If possible, issues will be labeled to indicate the status or priority. For example, labels may have a prefix for Status: X, or Priority: X. Statuses may include: In Progress, On Hold. Priorities may include: P1, P2 or P3 (high to low priority). +# Requesting a Feature + +If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend filing an RFC [issue](https://github.com/TextureGroup/Texture/issues/new) outlined below. This lets us reach an agreement on your proposal before you put significant effort into implementing it. + +If you're only fixing a bug, it's fine to submit a pull request right away, but we still recommend to file an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue. + +## RFC Issue process +1. Texture has an RFC process for feature requests. To begin the discussion either gather feedback on the Texture Slack channel or draft an Texture RFC as a Github Issue. +2. The title of the GitHub RFC Issue should have `[RFC]` as prefix: `[RFC] Add new cool feature` +3. Provide a clear and detailed explanation of the feature you want and why it's important to add. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on library for Texture. +4. If the feature is complex, consider writing an Texture RFC issue. Even if you can’t implement the feature yourself, consider writing an RFC issue. If we do end up accepting the feature, the issue provides the needed documentation for contributors to develop the feature according the specification accepted by the core team. We will tag accepted RFC issues with **Needs Volunteer**. +5. After discussing the feature you may choose to attempt a Pull Request. If you're at all able, start writing some code. We always have more work to do than time to do it. If you can write some code then that will speed the process along. + +In short, if you have an idea that would be nice to have, create an issue on the [TextureGroup](https://github.com/TextureGroup/Texture)[/](https://github.com/TextureGroup/Texture)[Texture](https://github.com/TextureGroup/Texture) repo. If you have a question about requesting a feature, start a discussion in our [Slack channel](http://texturegroup.org/slack.html). + +# Our Development Process + +All work on Texture happens directly on [GitHub](https://github.com/TextureGroup/Texture). Both core team members and external contributors send pull requests which go through the same review process. + +## `master` is under active development + +We will do our best to keep master in good shape, with tests passing at all times. But in order to move fast, we will make API changes that your application might not be compatible with. We will do our best to communicate these changes and version appropriately so you can lock into a specific version if need be. + +## Pull Requests + +If you send a pull request, please do it against the master branch. We maintain stable branches for major versions separately but we don't accept pull requests to them directly. Instead, we cherry-pick non-breaking changes from master to the latest stable major version. + +Before submitting a pull request, please make sure the following is done… + +1. Search GitHub for an open or closed [pull request](https://github.com/TextureGroup/Texture/pulls?utf8=✓&q=is%3Apr) that relates to your submission. You don't want to duplicate effort. +2. Fork the [repo](https://github.com/TextureGroup/Texture) and create your branch from master: + git checkout -b my-fix-branch master +3. Create your patch, including appropriate test cases. Please follow our Coding Guidelines. +4. Please make sure every commit message are meaningful so it that makes it clearer for people to review and easier to understand your intention +5. Ensure tests pass CI on GitHub for your Pull Request. +6. If you haven't already, sign the CLA. + +**Copyright Notice for files** +Copy and paste this to the top of your new file(s): +```objc +// +// ASDisplayNode.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +``` + +If you’ve modified an existing file, change the header to: +```objc +// +// ASDisplayNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +``` + +# Semantic Versioning + +Texture follows semantic versioning. We release patch versions for bug fixes, minor versions for new features (and rarely, clear and easy to fix breaking changes), and major versions for any major breaking changes. When we make breaking changes, we also introduce deprecation warnings in a minor version so that our users learn about the upcoming changes and migrate their code in advance. + +We tag every pull request with a label marking whether the change should go in the next patch, minor, or a major version. We release new versions pretty regularly usually every few weeks. The version will be a patch or minor version if it does not contain major new features or breaking API changes and a major version if it does. + +# Coding Guidelines +- Indent using 2 spaces (this conserves space in print and makes line wrapping less likely). Never indent with tabs. Be sure to set this preference in Xcode. +- Do your best to keep it around 120 characters line length +- End files with a newline. +- Don’t leave trailing whitespace. +- Space after `@property` declarations and conform to ordering of attributes +```objc +@property (nonatomic, readonly, assign, getter=isTracking) BOOL tracking; +@property (nonatomic, readwrite, strong, nullable) NSAttributedString *attributedText; +``` +- In method signatures, there should be a space after the method type (-/+ symbol). There should be a space between the method segments (matching Apple's style). Always include a keyword and be descriptive with the word before the argument which describes the argument. +```objc +@interface SomeClass +- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height; +- (void)setExampleText:(NSString *)text image:(UIImage *)image; +- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag; +- (id)viewWithTag:(NSInteger)tag; +@end +``` +- Internal methods should be prefixed with a `_` +```objc +- (void)_internalMethodWithParameter:(id)param; +``` +- Method braces and other braces (if/else/switch/while etc.) always open on the same line as the statement but close on a new line. +```objc +if (foo == bar) { + //.. +} else { + //.. +} +``` +- Method, `@interface` , and `@implementation` brackets on the following line +```objc +@implementation SomeClass +- (void)someMethod +{ + // Implementation +} +@end +``` +- Function brackets on the same line +```objc +static void someFunction() { + // Implementation +} +``` +- Operator goes with the variable name +```objc + NSAttributedString *attributedText = self.textNode.attributedText; +``` +- Locking + - Add a `_locked_` in front of the method name that needs to be called with a lock held +```objc +- (void)_locked_needsToBeCalledWithLock {} +``` + - Locking safety: + - It is fine for a `_locked_` method to call other `_locked_` methods. + - On the other hand, the following should not be done: + - Calling normal, unlocked methods inside a `_locked_` method + - Calling subclass hooks that are meant to be overridden by developers inside a `_locked_` method. + - Subclass hooks + - that are meant to be overwritten by users should not be called with a lock held. + - that are used internally the same conventions as above apply. +- There are multiple ways to acquire a lock: + 1. Explicitly call `.lock()` and `.unlock()` : +```objc +- (void)setContentSpacing:(CGFloat)contentSpacing +{ + __instanceLock__.lock(); + BOOL needsUpdate = (contentSpacing != _contentSpacing); + if (needsUpdate) { + _contentSpacing = contentSpacing; + } + __instanceLock__.unlock(); + + if (needsUpdate) { + [self setNeedsLayout]; + } +} + +- (CGFloat)contentSpacing +{ + CGFloat contentSpacing = 0.0; + __instanceLock__.lock(); + contentSpacing = _contentSpacing; + __instanceLock__.unlock(); + return contentSpacing; +} +``` + 2. Create an `ASDN::MutexLocker` : +```objc +- (void)setContentSpacing:(CGFloat)contentSpacing +{ + { + ASDN::MutexLocker l(__instanceLock__); + if (contentSpacing == _contentSpacing) { + return; + } + + _contentSpacing = contentSpacing; + } + + [self setNeedsLayout]; +} + +- (CGFloat)contentSpacing +{ + ASDN::MutexLocker l(__instanceLock__); + return _contentSpacing; +} +``` +- Nullability + - The adoption of annotations is straightforward. The standard we adopt is using the `NS_ASSUME_NONNULL_BEGIN` and `NS_ASSUME_NONNULL_END` on all headers. Then indicate nullability for the pointers that can be so. + - There is mostly no sense using nullability annotations outside of interface declarations. +```objc +// Properties +// Never include: `atomic`, `readwrite`, `strong`, `assign`. +// Only specify nullability if it isn't assumed from NS_ASSUME. +// (nullability, atomicity, storage class, writability, custom getter, custom setter) +@property (nullable, copy) NSNumber *status + +// Methods +- (nullable NSNumber *)doSomethingWithString:(nullable NSString *)str; + +// Functions +NSString * _Nullable ASStringWithQuotesIfMultiword(NSString * _Nullable string); + +// Typedefs +typedef void (^RemoteCallback)(id _Nullable result, NSError * _Nullable error); + +// Block as parameter +- (void)reloadDataWithCompletion:(void (^ _Nullable)())completion; + +// Block as parameter with parameter and return value +- (void)convertObject:(id _Nonnull (^ _Nullable)(id _Nullable input))handler; + +// More complex pointer types +- (void)allElementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode displaySet:(NSSet *__autoreleasing _Nullable *)displaySet preloadSet:(NSSet *__autoreleasing _Nullable *)preloadSet map:(ASElementMap *)map; +``` + +# Contributor License Agreement (CLA) + +Please sign our Contributor License Agreement (CLA) before sending pull requests. For any code changes to be accepted, the CLA must be signed. + +Complete your CLA [here](https://cla-assistant.io/TextureGroup/Texture) + +# License + +By contributing to Texture, you agree that your contributions will be licensed under its Apache 2 license. diff --git a/submodules/AsyncDisplayKit/Cartfile b/submodules/AsyncDisplayKit/Cartfile new file mode 100644 index 0000000000..ea145be2cb --- /dev/null +++ b/submodules/AsyncDisplayKit/Cartfile @@ -0,0 +1,2 @@ +github "pinterest/PINRemoteImage" "3.0.0-beta.14" +github "pinterest/PINCache" "3.0.1-beta.7" diff --git a/submodules/AsyncDisplayKit/Dangerfile b/submodules/AsyncDisplayKit/Dangerfile new file mode 100644 index 0000000000..147d389069 --- /dev/null +++ b/submodules/AsyncDisplayKit/Dangerfile @@ -0,0 +1,83 @@ +require 'open-uri' + +source_pattern = /(\.m|\.mm|\.h)$/ + +modified_source_files = git.modified_files.grep(source_pattern) +has_modified_source_files = !modified_source_files.empty? +added_source_files = git.added_files.grep(source_pattern) +has_added_source_files = !added_source_files.empty? + +# Make it more obvious that a PR is a work in progress and shouldn't be merged yet +warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" + +# Warn when there is a big PR +warn("This is a big PR, please consider splitting it up to ease code review.") if git.lines_of_code > 500 + +# Modifying the changelog will probably get overwritten. +if git.modified_files.include?("CHANGELOG.md") && !github.pr_title.include?("#changelog") + warn("PR modifies CHANGELOG.md, which is a generated file. Add #changelog to the title to suppress this warning.") +end + +def full_license(partial_license, filename) + license_header = <<-HEREDOC +// + HEREDOC + license_header += "// " + filename + "\n" + license_header += <<-HEREDOC +// Texture +// + HEREDOC + license_header += partial_license + return license_header +end + +def check_file_header(files_to_check, licenses) + repo_name = github.pr_json["base"]["repo"]["full_name"] + pr_number = github.pr_json["number"] + files = github.api.pull_request_files(repo_name, pr_number) + files.each do |file| + if files_to_check.include?(file["filename"]) + filename = File.basename(file["filename"]) + + data = "" + contents = github.api.get file["contents_url"] + open(contents["download_url"]) { |io| + data += io.read + } + + correct_license = false + licenses.each do |license| + license_header = full_license(license, filename) + if data.include? "Pinterest, Inc." + correct_license = true + end + end + + if correct_license == false + warn ("Please ensure license is correct for #{filename}: \n```\n" + full_license(licenses[0], filename) + "```") + end + + end + end +end + +# Ensure new files have proper header +new_source_license_header = <<-HEREDOC +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +HEREDOC + +if has_added_source_files + check_file_header(added_source_files, [new_source_license_header]) +end + +# Ensure modified files have proper header +modified_source_license_header = <<-HEREDOC +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +HEREDOC + +if has_modified_source_files + check_file_header(modified_source_files, [modified_source_license_header, new_source_license_header]) +end diff --git a/submodules/AsyncDisplayKit/Gemfile b/submodules/AsyncDisplayKit/Gemfile new file mode 100644 index 0000000000..38bfbc8e08 --- /dev/null +++ b/submodules/AsyncDisplayKit/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'danger' +gem 'danger-slack' \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/InstrumentsTemplates/SystemTrace.tracetemplate b/submodules/AsyncDisplayKit/InstrumentsTemplates/SystemTrace.tracetemplate new file mode 100644 index 0000000000..48cc3e6aef Binary files /dev/null and b/submodules/AsyncDisplayKit/InstrumentsTemplates/SystemTrace.tracetemplate differ diff --git a/submodules/AsyncDisplayKit/LICENSE b/submodules/AsyncDisplayKit/LICENSE new file mode 100644 index 0000000000..c63a7c0a29 --- /dev/null +++ b/submodules/AsyncDisplayKit/LICENSE @@ -0,0 +1,181 @@ +The Texture project was created by Pinterest as a continuation, under a different +name and license, of the AsyncDisplayKit codebase originally developed by Facebook. + +All code in Texture is covered by the Apache License, Version 2.0. + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/submodules/AsyncDisplayKit/Podfile b/submodules/AsyncDisplayKit/Podfile new file mode 100644 index 0000000000..98479737ec --- /dev/null +++ b/submodules/AsyncDisplayKit/Podfile @@ -0,0 +1,31 @@ +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '9.0' + +target :'AsyncDisplayKitTests' do + pod 'OCMock', '=3.4.1' # 3.4.2 currently has issues. + pod 'FBSnapshotTestCase/Core', '~> 2.1' + pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' + + # Only for buck build + pod 'PINRemoteImage', '3.0.0-beta.14' +end + +#TODO CocoaPods plugin instead? +post_install do |installer| + require 'fileutils' + + # Assuming we're at the root dir + buck_files_dir = 'buck-files' + if File.directory?(buck_files_dir) + installer.pod_targets.flat_map do |pod_target| + pod_name = pod_target.pod_name + # Copy the file at buck-files/BUCK_pod_name to Pods/pod_name/BUCK, + # override existing file if needed + buck_file = buck_files_dir + '/BUCK_' + pod_name + if File.file?(buck_file) + FileUtils.cp(buck_file, 'Pods/' + pod_name + '/BUCK', :preserve => false) + end + end + end +end diff --git a/submodules/AsyncDisplayKit/Podfile.lock b/submodules/AsyncDisplayKit/Podfile.lock new file mode 100644 index 0000000000..df3c6fcb88 --- /dev/null +++ b/submodules/AsyncDisplayKit/Podfile.lock @@ -0,0 +1,55 @@ +PODS: + - FBSnapshotTestCase/Core (2.1.4) + - JGMethodSwizzler (2.0.1) + - OCMock (3.4.1) + - PINCache (3.0.1-beta.7): + - PINCache/Arc-exception-safe (= 3.0.1-beta.7) + - PINCache/Core (= 3.0.1-beta.7) + - PINCache/Arc-exception-safe (3.0.1-beta.7): + - PINCache/Core + - PINCache/Core (3.0.1-beta.7): + - PINOperation (~> 1.1.1) + - PINOperation (1.1.1) + - PINRemoteImage (3.0.0-beta.14): + - PINRemoteImage/PINCache (= 3.0.0-beta.14) + - PINRemoteImage/Core (3.0.0-beta.14): + - PINOperation + - PINRemoteImage/PINCache (3.0.0-beta.14): + - PINCache (= 3.0.1-beta.7) + - PINRemoteImage/Core + +DEPENDENCIES: + - FBSnapshotTestCase/Core (~> 2.1) + - JGMethodSwizzler (from `https://github.com/JonasGessner/JGMethodSwizzler`, branch `master`) + - OCMock (= 3.4.1) + - PINRemoteImage (= 3.0.0-beta.14) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - FBSnapshotTestCase + - OCMock + - PINCache + - PINOperation + - PINRemoteImage + +EXTERNAL SOURCES: + JGMethodSwizzler: + :branch: master + :git: https://github.com/JonasGessner/JGMethodSwizzler + +CHECKOUT OPTIONS: + JGMethodSwizzler: + :commit: 8791eccc5342224bd293b5867348657e3a240c7f + :git: https://github.com/JonasGessner/JGMethodSwizzler + +SPEC CHECKSUMS: + FBSnapshotTestCase: 094f9f314decbabe373b87cc339bea235a63e07a + JGMethodSwizzler: 7328146117fffa8a4038c42eb7cd3d4c75006f97 + OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3 + PINCache: 7cb9ae068c8f655717f7c644ef1dff9fd573e979 + PINOperation: a6219e6fc9db9c269eb7a7b871ac193bcf400aac + PINRemoteImage: 81bbff853acc71c6de9e106e9e489a791b8bbb08 + +PODFILE CHECKSUM: 445046ac151568c694ff286684322273f0b597d6 + +COCOAPODS: 1.6.0 diff --git a/submodules/AsyncDisplayKit/README.md b/submodules/AsyncDisplayKit/README.md new file mode 100644 index 0000000000..4b939fac5a --- /dev/null +++ b/submodules/AsyncDisplayKit/README.md @@ -0,0 +1,51 @@ +## Coming from AsyncDisplayKit? Learn more [here](https://medium.com/@Pinterest_Engineering/introducing-texture-a-new-home-for-asyncdisplaykit-e7c003308f50) + +![Texture](https://github.com/texturegroup/texture/raw/master/docs/static/images/logo.png) + +[![Apps Using](https://img.shields.io/cocoapods/at/Texture.svg?label=Apps%20Using%20Texture&colorB=28B9FE)](http://cocoapods.org/pods/Texture) +[![Downloads](https://img.shields.io/cocoapods/dt/Texture.svg?label=Total%20Downloads&colorB=28B9FE)](http://cocoapods.org/pods/Texture) + +[![Platform](https://img.shields.io/badge/platforms-iOS%20%7C%20tvOS-orange.svg)](http://texturegroup.org) +[![Languages](https://img.shields.io/badge/languages-ObjC%20%7C%20Swift-orange.svg)](http://texturegroup.org) + +[![Version](https://img.shields.io/cocoapods/v/Texture.svg)](http://cocoapods.org/pods/Texture) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-59C939.svg?style=flat)](https://github.com/Carthage/Carthage) +[![License](https://img.shields.io/cocoapods/l/Texture.svg)](https://github.com/texturegroup/texture/blob/master/LICENSE) + +## Installation + +Texture is available via CocoaPods or Carthage. See our [Installation](http://texturegroup.org/docs/installation.html) guide for instructions. + +## Performance Gains + +Texture's basic unit is the `node`. An ASDisplayNode is an abstraction over `UIView`, which in turn is an abstraction over `CALayer`. Unlike views, which can only be used on the main thread, nodes are thread-safe: you can instantiate and configure entire hierarchies of them in parallel on background threads. + +To keep its user interface smooth and responsive, your app should render at 60 frames per second — the gold standard on iOS. This means the main thread has one-sixtieth of a second to push each frame. That's 16 milliseconds to execute all layout and drawing code! And because of system overhead, your code usually has less than ten milliseconds to run before it causes a frame drop. + +Texture lets you move image decoding, text sizing and rendering, layout, and other expensive UI operations off the main thread, to keep the main thread available to respond to user interaction. + +## Advanced Developer Features + +As the framework has grown, many features have been added that can save developers tons of time by eliminating common boilerplate style structures common in modern iOS apps. If you've ever dealt with cell reuse bugs, tried to performantly preload data for a page or scroll style interface or even just tried to keep your app from dropping too many frames you can benefit from integrating Texture. + +## Learn More + +* Read the our [Getting Started](http://texturegroup.org/docs/getting-started.html) guide +* Get the [sample projects](https://github.com/texturegroup/texture/tree/master/examples) +* Browse the [API reference](http://texturegroup.org/appledocs.html) + +## Getting Help + +We use Slack for real-time debugging, community updates, and general talk about Texture. [Signup](http://asdk-slack-auto-invite.herokuapp.com) yourself or email textureframework@gmail.com to get an invite. + +## Release process + +For the release process see the [RELEASE] (https://github.com/texturegroup/texture/blob/master/RELEASE.md) file. + +## Contributing + +We welcome any contributions. See the [CONTRIBUTING](https://github.com/texturegroup/texture/blob/master/CONTRIBUTING.md) file for how to get involved. + +## License + +The Texture project is available for free use, as described by the [LICENSE](https://github.com/texturegroup/texture/blob/master/LICENSE) (Apache 2.0). diff --git a/submodules/AsyncDisplayKit/RELEASE.md b/submodules/AsyncDisplayKit/RELEASE.md new file mode 100644 index 0000000000..b675fa4922 --- /dev/null +++ b/submodules/AsyncDisplayKit/RELEASE.md @@ -0,0 +1,15 @@ +# Release Process +This document describes the process for a public Texture release. + +### Preparation +- Install [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator): `sudo gem install github_changelog_generator` +- Generate a GitHub Personal Access Token to prevent running into public GitHub API rate limits: https://github.com/github-changelog-generator/github-changelog-generator#github-token + +### Process +- Run `github_changelog_generator` in Texture project directory: `github_changelog_generator --token TextureGroup/Texture`. To avoid hitting rate limit, the generator will replace the entire file with just the changes from this version – revert that giant deletion to get the entire new changelog. +- Update `spec.version` within `Texture.podspec` and the `since-tag` and `future-release` fields in `.github_changelog_generator`. +- Create a new PR with the updated `Texture.podspec` and the newly generated changelog, add `#changelog` to the PR message so the CI will not prevent merging it. +- After merging in the PR, [create a new GitHub release](https://github.com/TextureGroup/Texture/releases/new). Use the generated changelog for the new release. + +### Problems +- Sometimes we will still run into GitHub rate limit issues although using a personal token to generate the changelog. For now there is no solution for this. The issue to track is: [Triggering Github Rate limits #656](https://github.com/github-changelog-generator/github-changelog-generator/issues/656) \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Schemas/configuration.json b/submodules/AsyncDisplayKit/Schemas/configuration.json new file mode 100644 index 0000000000..12957a12b5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Schemas/configuration.json @@ -0,0 +1,33 @@ +{ + "id": "configuration.json", + "title": "configuration", + "description" : "Schema definition of a Texture Configuration", + "$schema": "http://json-schema.org/schema#", + "type": "object", + "properties": { + "version" : { + "type" : "number" + }, + "experimental_features": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "exp_graphics_contexts", + "exp_text_node", + "exp_interface_state_coalesce", + "exp_unfair_lock", + "exp_infer_layer_defaults", + "exp_collection_teardown", + "exp_framesetter_cache", + "exp_skip_clear_data", + "exp_did_enter_preload_skip_asm_layout", + "exp_disable_a11y_cache", + "exp_dispatch_apply", + "exp_image_downloader_priority", + "exp_text_drawing" + ] + } + } + } +} diff --git a/submodules/AsyncDisplayKit/Source/ASBlockTypes.h b/submodules/AsyncDisplayKit/Source/ASBlockTypes.h new file mode 100644 index 0000000000..e1c7456019 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASBlockTypes.h @@ -0,0 +1,20 @@ +// +// ASBlockTypes.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASCellNode; + +/** + * ASCellNode creation block. Used to lazily create the ASCellNode instance for a specified indexPath. + */ +typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(void); + +// Type for the cancellation checker block passed into the async display blocks. YES means the operation has been cancelled, NO means continue. +typedef BOOL(^asdisplaynode_iscancelled_block_t)(void); diff --git a/submodules/AsyncDisplayKit/Source/ASButtonNode+Private.h b/submodules/AsyncDisplayKit/Source/ASButtonNode+Private.h new file mode 100644 index 0000000000..2ecc894463 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASButtonNode+Private.h @@ -0,0 +1,44 @@ +// +// ASButtonNode+Private.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@interface ASButtonNode() { + NSAttributedString *_normalAttributedTitle; + NSAttributedString *_highlightedAttributedTitle; + NSAttributedString *_selectedAttributedTitle; + NSAttributedString *_selectedHighlightedAttributedTitle; + NSAttributedString *_disabledAttributedTitle; + + UIImage *_normalImage; + UIImage *_highlightedImage; + UIImage *_selectedImage; + UIImage *_selectedHighlightedImage; + UIImage *_disabledImage; + + UIImage *_normalBackgroundImage; + UIImage *_highlightedBackgroundImage; + UIImage *_selectedBackgroundImage; + UIImage *_selectedHighlightedBackgroundImage; + UIImage *_disabledBackgroundImage; + + CGFloat _contentSpacing; + BOOL _laysOutHorizontally; + ASVerticalAlignment _contentVerticalAlignment; + ASHorizontalAlignment _contentHorizontalAlignment; + UIEdgeInsets _contentEdgeInsets; + ASButtonNodeImageAlignment _imageAlignment; + ASTextNode *_titleNode; + ASImageNode *_imageNode; + ASImageNode *_backgroundImageNode; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASButtonNode+Yoga.h b/submodules/AsyncDisplayKit/Source/ASButtonNode+Yoga.h new file mode 100644 index 0000000000..4fddc9df3a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASButtonNode+Yoga.h @@ -0,0 +1,20 @@ +// +// ASButtonNode+Yoga.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASButtonNode (Yoga) + +- (void)updateYogaLayoutIfNeeded; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASButtonNode+Yoga.mm b/submodules/AsyncDisplayKit/Source/ASButtonNode+Yoga.mm new file mode 100644 index 0000000000..4666475560 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASButtonNode+Yoga.mm @@ -0,0 +1,106 @@ +// +// ASButtonNode+Yoga.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASButtonNode+Yoga.h" +#import +#import +#import +#import + +#if YOGA +static void ASButtonNodeResolveHorizontalAlignmentForStyle(ASLayoutElementStyle *style, ASStackLayoutDirection _direction, ASHorizontalAlignment _horizontalAlignment, ASStackLayoutJustifyContent _justifyContent, ASStackLayoutAlignItems _alignItems) { + if (_direction == ASStackLayoutDirectionHorizontal) { + style.justifyContent = justifyContent(_horizontalAlignment, _justifyContent); + } else { + style.alignItems = alignment(_horizontalAlignment, _alignItems); + } +} + +static void ASButtonNodeResolveVerticalAlignmentForStyle(ASLayoutElementStyle *style, ASStackLayoutDirection _direction, ASVerticalAlignment _verticalAlignment, ASStackLayoutJustifyContent _justifyContent, ASStackLayoutAlignItems _alignItems) { + if (_direction == ASStackLayoutDirectionHorizontal) { + style.alignItems = alignment(_verticalAlignment, _alignItems); + } else { + style.justifyContent = justifyContent(_verticalAlignment, _justifyContent); + } +} + +@implementation ASButtonNode (Yoga) + +- (void)updateYogaLayoutIfNeeded +{ + NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:2]; + { + ASLockScopeSelf(); + + // Build up yoga children for button node again + unowned ASLayoutElementStyle *style = [self _locked_style]; + [style yogaNodeCreateIfNeeded]; + + // Setup stack layout values + style.flexDirection = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; + + // Resolve horizontal and vertical alignment + ASButtonNodeResolveHorizontalAlignmentForStyle(style, style.flexDirection, _contentHorizontalAlignment, style.justifyContent, style.alignItems); + ASButtonNodeResolveVerticalAlignmentForStyle(style, style.flexDirection, _contentVerticalAlignment, style.justifyContent, style.alignItems); + + // Setup new yoga children + if (_imageNode.image != nil) { + [_imageNode.style yogaNodeCreateIfNeeded]; + [children addObject:_imageNode]; + } + + if (_titleNode.attributedText.length > 0) { + [_titleNode.style yogaNodeCreateIfNeeded]; + if (_imageAlignment == ASButtonNodeImageAlignmentBeginning) { + [children addObject:_titleNode]; + } else { + [children insertObject:_titleNode atIndex:0]; + } + } + + // Add spacing between title and button + if (children.count == 2) { + unowned ASLayoutElementStyle *firstChildStyle = children.firstObject.style; + if (_laysOutHorizontally) { + firstChildStyle.margin = ASEdgeInsetsMake(UIEdgeInsetsMake(0, 0, 0, _contentSpacing)); + } else { + firstChildStyle.margin = ASEdgeInsetsMake(UIEdgeInsetsMake(0, 0, _contentSpacing, 0)); + } + } + + // Add padding to button + if (UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, _contentEdgeInsets) == NO) { + style.padding = ASEdgeInsetsMake(_contentEdgeInsets); + } + + // Add background node + if (_backgroundImageNode.image) { + [_backgroundImageNode.style yogaNodeCreateIfNeeded]; + [children insertObject:_backgroundImageNode atIndex:0]; + + _backgroundImageNode.style.positionType = YGPositionTypeAbsolute; + _backgroundImageNode.style.position = ASEdgeInsetsMake(UIEdgeInsetsZero); + } + } + + // Update new children + [self setYogaChildren:children]; +} + +@end + +#else + +@implementation ASButtonNode (Yoga) + +- (void)updateYogaLayoutIfNeeded {} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASButtonNode.h b/submodules/AsyncDisplayKit/Source/ASButtonNode.h new file mode 100644 index 0000000000..f43a544df1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASButtonNode.h @@ -0,0 +1,130 @@ +// +// ASButtonNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASImageNode, ASTextNode; + +/** + Image alignment defines where the image will be placed relative to the text. + */ +typedef NS_ENUM(NSInteger, ASButtonNodeImageAlignment) { + /** Places the image before the text. */ + ASButtonNodeImageAlignmentBeginning, + /** Places the image after the text. */ + ASButtonNodeImageAlignmentEnd +}; + +@interface ASButtonNode : ASControlNode + +@property (readonly) ASTextNode * titleNode; +@property (readonly) ASImageNode * imageNode; +@property (readonly) ASImageNode * backgroundImageNode; + +/** + Spacing between image and title. Defaults to 8.0. + */ +@property CGFloat contentSpacing; + +/** + Whether button should be laid out vertically (image on top of text) or horizontally (image to the left of text). + ASButton node does not yet support RTL but it should be fairly easy to implement. + Defaults to YES. + */ +@property BOOL laysOutHorizontally; + +/** Horizontally align content (text or image). + Defaults to ASHorizontalAlignmentMiddle. + */ +@property ASHorizontalAlignment contentHorizontalAlignment; + +/** Vertically align content (text or image). + Defaults to ASVerticalAlignmentCenter. + */ +@property ASVerticalAlignment contentVerticalAlignment; + +/** + * @discussion The insets used around the title and image node + */ +@property UIEdgeInsets contentEdgeInsets; + +/** + * @discusstion Whether the image should be aligned at the beginning or at the end of node. Default is `ASButtonNodeImageAlignmentBeginning`. + */ +@property ASButtonNodeImageAlignment imageAlignment; + +/** + * Returns the styled title associated with the specified state. + * + * @param state The control state that uses the styled title. + * + * @return The title for the specified state. + */ +- (nullable NSAttributedString *)attributedTitleForState:(UIControlState)state AS_WARN_UNUSED_RESULT; + +/** + * Sets the styled title to use for the specified state. This will reset styled title previously set with -setTitle:withFont:withColor:forState. + * + * @param title The styled text string to use for the title. + * @param state The control state that uses the specified title. + */ +- (void)setAttributedTitle:(nullable NSAttributedString *)title forState:(UIControlState)state; + +#if TARGET_OS_IOS +/** + * Sets the title to use for the specified state. This will reset styled title previously set with -setAttributedTitle:forState. + * + * @param title The styled text string to use for the title. + * @param font The font to use for the title. + * @param color The color to use for the title. + * @param state The control state that uses the specified title. + */ +- (void)setTitle:(NSString *)title withFont:(nullable UIFont *)font withColor:(nullable UIColor *)color forState:(UIControlState)state; +#endif +/** + * Returns the image used for a button state. + * + * @param state The control state that uses the image. + * + * @return The image used for the specified state. + */ +- (nullable UIImage *)imageForState:(UIControlState)state AS_WARN_UNUSED_RESULT; + +/** + * Sets the image to use for the specified state. + * + * @param image The image to use for the specified state. + * @param state The control state that uses the specified title. + */ +- (void)setImage:(nullable UIImage *)image forState:(UIControlState)state; + +/** + * Sets the background image to use for the specified state. + * + * @param image The image to use for the specified state. + * @param state The control state that uses the specified title. + */ +- (void)setBackgroundImage:(nullable UIImage *)image forState:(UIControlState)state; + + +/** + * Returns the background image used for a button state. + * + * @param state The control state that uses the image. + * + * @return The background image used for the specified state. + */ +- (nullable UIImage *)backgroundImageForState:(UIControlState)state AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASButtonNode.mm b/submodules/AsyncDisplayKit/Source/ASButtonNode.mm new file mode 100644 index 0000000000..f91b16c2bd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASButtonNode.mm @@ -0,0 +1,557 @@ +// +// ASButtonNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +@implementation ASButtonNode + +@synthesize contentSpacing = _contentSpacing; +@synthesize laysOutHorizontally = _laysOutHorizontally; +@synthesize contentVerticalAlignment = _contentVerticalAlignment; +@synthesize contentHorizontalAlignment = _contentHorizontalAlignment; +@synthesize contentEdgeInsets = _contentEdgeInsets; +@synthesize imageAlignment = _imageAlignment; +@synthesize titleNode = _titleNode; +@synthesize imageNode = _imageNode; +@synthesize backgroundImageNode = _backgroundImageNode; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (self = [super init]) { + self.automaticallyManagesSubnodes = YES; + + _contentSpacing = 8.0; + _laysOutHorizontally = YES; + _contentHorizontalAlignment = ASHorizontalAlignmentMiddle; + _contentVerticalAlignment = ASVerticalAlignmentCenter; + _contentEdgeInsets = UIEdgeInsetsZero; + _imageAlignment = ASButtonNodeImageAlignmentBeginning; + self.accessibilityTraits = self.defaultAccessibilityTraits; + + [self updateYogaLayoutIfNeeded]; + } + return self; +} + +- (ASTextNode *)titleNode +{ + ASLockScopeSelf(); + if (!_titleNode) { + _titleNode = [[ASTextNode alloc] init]; +#if TARGET_OS_IOS + // tvOS needs access to the underlying view + // of the button node to add a touch handler. + [_titleNode setLayerBacked:YES]; +#endif + _titleNode.style.flexShrink = 1.0; + } + return _titleNode; +} + +#pragma mark - Public Getter + +- (ASImageNode *)imageNode +{ + ASLockScopeSelf(); + if (!_imageNode) { + _imageNode = [[ASImageNode alloc] init]; + [_imageNode setLayerBacked:YES]; + } + return _imageNode; +} + +- (ASImageNode *)backgroundImageNode +{ + ASLockScopeSelf(); + if (!_backgroundImageNode) { + _backgroundImageNode = [[ASImageNode alloc] init]; + [_backgroundImageNode setLayerBacked:YES]; + [_backgroundImageNode setContentMode:UIViewContentModeScaleToFill]; + } + return _backgroundImageNode; +} + +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!layerBacked, @"ASButtonNode must not be layer backed!"); + [super setLayerBacked:layerBacked]; +} + +- (void)setEnabled:(BOOL)enabled +{ + if (self.enabled != enabled) { + [super setEnabled:enabled]; + self.accessibilityTraits = self.defaultAccessibilityTraits; + [self updateButtonContent]; + } +} + +- (void)setHighlighted:(BOOL)highlighted +{ + if (self.highlighted != highlighted) { + [super setHighlighted:highlighted]; + [self updateButtonContent]; + } +} + +- (void)setSelected:(BOOL)selected +{ + if (self.selected != selected) { + [super setSelected:selected]; + [self updateButtonContent]; + } +} + +- (void)updateButtonContent +{ + [self updateBackgroundImage]; + [self updateImage]; + [self updateTitle]; +} + +- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously +{ + [super setDisplaysAsynchronously:displaysAsynchronously]; + [self.backgroundImageNode setDisplaysAsynchronously:displaysAsynchronously]; + [self.imageNode setDisplaysAsynchronously:displaysAsynchronously]; + [self.titleNode setDisplaysAsynchronously:displaysAsynchronously]; +} + +- (void)updateImage +{ + [self lock]; + + UIImage *newImage; + if (self.enabled == NO && _disabledImage) { + newImage = _disabledImage; + } else if (self.highlighted && self.selected && _selectedHighlightedImage) { + newImage = _selectedHighlightedImage; + } else if (self.highlighted && _highlightedImage) { + newImage = _highlightedImage; + } else if (self.selected && _selectedImage) { + newImage = _selectedImage; + } else { + newImage = _normalImage; + } + + if ((_imageNode != nil || newImage != nil) && newImage != self.imageNode.image) { + _imageNode.image = newImage; + [self unlock]; + + [self updateYogaLayoutIfNeeded]; + [self setNeedsLayout]; + return; + } + + [self unlock]; +} + +- (void)updateTitle +{ + [self lock]; + + NSAttributedString *newTitle; + if (self.enabled == NO && _disabledAttributedTitle) { + newTitle = _disabledAttributedTitle; + } else if (self.highlighted && self.selected && _selectedHighlightedAttributedTitle) { + newTitle = _selectedHighlightedAttributedTitle; + } else if (self.highlighted && _highlightedAttributedTitle) { + newTitle = _highlightedAttributedTitle; + } else if (self.selected && _selectedAttributedTitle) { + newTitle = _selectedAttributedTitle; + } else { + newTitle = _normalAttributedTitle; + } + + // Calling self.titleNode is essential here because _titleNode is lazily created by the getter. + if ((_titleNode != nil || newTitle.length > 0) && [self.titleNode.attributedText isEqualToAttributedString:newTitle] == NO) { + _titleNode.attributedText = newTitle; + [self unlock]; + + self.accessibilityLabel = self.defaultAccessibilityLabel; + [self updateYogaLayoutIfNeeded]; + [self setNeedsLayout]; + return; + } + + [self unlock]; +} + +- (void)updateBackgroundImage +{ + [self lock]; + + UIImage *newImage; + if (self.enabled == NO && _disabledBackgroundImage) { + newImage = _disabledBackgroundImage; + } else if (self.highlighted && self.selected && _selectedHighlightedBackgroundImage) { + newImage = _selectedHighlightedBackgroundImage; + } else if (self.highlighted && _highlightedBackgroundImage) { + newImage = _highlightedBackgroundImage; + } else if (self.selected && _selectedBackgroundImage) { + newImage = _selectedBackgroundImage; + } else { + newImage = _normalBackgroundImage; + } + + if ((_backgroundImageNode != nil || newImage != nil) && newImage != self.backgroundImageNode.image) { + _backgroundImageNode.image = newImage; + [self unlock]; + + [self updateYogaLayoutIfNeeded]; + [self setNeedsLayout]; + return; + } + + [self unlock]; +} + +- (CGFloat)contentSpacing +{ + ASLockScopeSelf(); + return _contentSpacing; +} + +- (void)setContentSpacing:(CGFloat)contentSpacing +{ + if (ASLockedSelfCompareAssign(_contentSpacing, contentSpacing)) { + [self updateYogaLayoutIfNeeded]; + [self setNeedsLayout]; + } +} + +- (BOOL)laysOutHorizontally +{ + ASLockScopeSelf(); + return _laysOutHorizontally; +} + +- (void)setLaysOutHorizontally:(BOOL)laysOutHorizontally +{ + if (ASLockedSelfCompareAssign(_laysOutHorizontally, laysOutHorizontally)) { + [self updateYogaLayoutIfNeeded]; + [self setNeedsLayout]; + } +} + +- (ASVerticalAlignment)contentVerticalAlignment +{ + ASLockScopeSelf(); + return _contentVerticalAlignment; +} + +- (void)setContentVerticalAlignment:(ASVerticalAlignment)contentVerticalAlignment +{ + ASLockScopeSelf(); + _contentVerticalAlignment = contentVerticalAlignment; +} + +- (ASHorizontalAlignment)contentHorizontalAlignment +{ + ASLockScopeSelf(); + return _contentHorizontalAlignment; +} + +- (void)setContentHorizontalAlignment:(ASHorizontalAlignment)contentHorizontalAlignment +{ + ASLockScopeSelf(); + _contentHorizontalAlignment = contentHorizontalAlignment; +} + +- (UIEdgeInsets)contentEdgeInsets +{ + ASLockScopeSelf(); + return _contentEdgeInsets; +} + +- (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets +{ + ASLockScopeSelf(); + _contentEdgeInsets = contentEdgeInsets; +} + +- (ASButtonNodeImageAlignment)imageAlignment +{ + ASLockScopeSelf(); + return _imageAlignment; +} + +- (void)setImageAlignment:(ASButtonNodeImageAlignment)imageAlignment +{ + ASLockScopeSelf(); + _imageAlignment = imageAlignment; +} + + +#if TARGET_OS_IOS +- (void)setTitle:(NSString *)title withFont:(UIFont *)font withColor:(UIColor *)color forState:(UIControlState)state +{ + NSDictionary *attributes = @{ + NSFontAttributeName: font ? : [UIFont systemFontOfSize:[UIFont buttonFontSize]], + NSForegroundColorAttributeName : color ? : [UIColor blackColor] + }; + + NSAttributedString *string = [[NSAttributedString alloc] initWithString:title attributes:attributes]; + [self setAttributedTitle:string forState:state]; +} +#endif + +- (NSAttributedString *)attributedTitleForState:(UIControlState)state +{ + ASLockScopeSelf(); + switch (state) { + case UIControlStateNormal: + return _normalAttributedTitle; + + case UIControlStateHighlighted: + return _highlightedAttributedTitle; + + case UIControlStateSelected: + return _selectedAttributedTitle; + + case UIControlStateSelected | UIControlStateHighlighted: + return _selectedHighlightedAttributedTitle; + + case UIControlStateDisabled: + return _disabledAttributedTitle; + + default: + return _normalAttributedTitle; + } +} + +- (void)setAttributedTitle:(NSAttributedString *)title forState:(UIControlState)state +{ + { + ASLockScopeSelf(); + switch (state) { + case UIControlStateNormal: + _normalAttributedTitle = [title copy]; + break; + + case UIControlStateHighlighted: + _highlightedAttributedTitle = [title copy]; + break; + + case UIControlStateSelected: + _selectedAttributedTitle = [title copy]; + break; + + case UIControlStateSelected | UIControlStateHighlighted: + _selectedHighlightedAttributedTitle = [title copy]; + break; + + case UIControlStateDisabled: + _disabledAttributedTitle = [title copy]; + break; + + default: + break; + } + } + + [self updateTitle]; +} + +- (UIImage *)imageForState:(UIControlState)state +{ + ASLockScopeSelf(); + switch (state) { + case UIControlStateNormal: + return _normalImage; + + case UIControlStateHighlighted: + return _highlightedImage; + + case UIControlStateSelected: + return _selectedImage; + + case UIControlStateSelected | UIControlStateHighlighted: + return _selectedHighlightedImage; + + case UIControlStateDisabled: + return _disabledImage; + + default: + return _normalImage; + } +} + +- (void)setImage:(UIImage *)image forState:(UIControlState)state +{ + { + ASLockScopeSelf(); + switch (state) { + case UIControlStateNormal: + _normalImage = image; + break; + + case UIControlStateHighlighted: + _highlightedImage = image; + break; + + case UIControlStateSelected: + _selectedImage = image; + break; + + case UIControlStateSelected | UIControlStateHighlighted: + _selectedHighlightedImage = image; + break; + + case UIControlStateDisabled: + _disabledImage = image; + break; + + default: + break; + } + } + + [self updateImage]; +} + +- (UIImage *)backgroundImageForState:(UIControlState)state +{ + ASLockScopeSelf(); + switch (state) { + case UIControlStateNormal: + return _normalBackgroundImage; + + case UIControlStateHighlighted: + return _highlightedBackgroundImage; + + case UIControlStateSelected: + return _selectedBackgroundImage; + + case UIControlStateSelected | UIControlStateHighlighted: + return _selectedHighlightedBackgroundImage; + + case UIControlStateDisabled: + return _disabledBackgroundImage; + + default: + return _normalBackgroundImage; + } +} + +- (void)setBackgroundImage:(UIImage *)image forState:(UIControlState)state +{ + { + ASLockScopeSelf(); + switch (state) { + case UIControlStateNormal: + _normalBackgroundImage = image; + break; + + case UIControlStateHighlighted: + _highlightedBackgroundImage = image; + break; + + case UIControlStateSelected: + _selectedBackgroundImage = image; + break; + + case UIControlStateSelected | UIControlStateHighlighted: + _selectedHighlightedBackgroundImage = image; + break; + + case UIControlStateDisabled: + _disabledBackgroundImage = image; + break; + + default: + break; + } + } + + [self updateBackgroundImage]; +} + + +- (NSString *)defaultAccessibilityLabel +{ + ASLockScopeSelf(); + return _titleNode.defaultAccessibilityLabel; +} + +- (UIAccessibilityTraits)defaultAccessibilityTraits +{ + return self.enabled ? UIAccessibilityTraitButton + : (UIAccessibilityTraitButton | UIAccessibilityTraitNotEnabled); +} + +#pragma mark - Layout + +#if !YOGA +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + UIEdgeInsets contentEdgeInsets; + ASButtonNodeImageAlignment imageAlignment; + ASLayoutSpec *spec; + ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init]; + { + ASLockScopeSelf(); + stack.direction = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; + stack.spacing = _contentSpacing; + stack.horizontalAlignment = _contentHorizontalAlignment; + stack.verticalAlignment = _contentVerticalAlignment; + + contentEdgeInsets = _contentEdgeInsets; + imageAlignment = _imageAlignment; + } + + NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:2]; + if (_imageNode.image) { + [children addObject:_imageNode]; + } + + if (_titleNode.attributedText.length > 0) { + if (imageAlignment == ASButtonNodeImageAlignmentBeginning) { + [children addObject:_titleNode]; + } else { + [children insertObject:_titleNode atIndex:0]; + } + } + + stack.children = children; + + spec = stack; + + if (UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, contentEdgeInsets) == NO) { + spec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:contentEdgeInsets child:spec]; + } + + if (_backgroundImageNode.image) { + spec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:spec background:_backgroundImageNode]; + } + + return spec; +} +#endif + +- (void)layout +{ + [super layout]; + + _backgroundImageNode.hidden = (_backgroundImageNode.image == nil); + _imageNode.hidden = (_imageNode.image == nil); + _titleNode.hidden = (_titleNode.attributedText.length == 0); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h new file mode 100644 index 0000000000..a77452622b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h @@ -0,0 +1,28 @@ +// +// ASCGImageBuffer.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASCGImageBuffer : NSObject + +/// Init a zero-filled buffer with the given length. +- (instancetype)initWithLength:(NSUInteger)length; + +@property (readonly) void *mutableBytes NS_RETURNS_INNER_POINTER; + +/// Don't do any drawing or call any methods after calling this. +- (CGDataProviderRef)createDataProviderAndInvalidate CF_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm new file mode 100644 index 0000000000..6f05300e23 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm @@ -0,0 +1,88 @@ +// +// ASCGImageBuffer.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASCGImageBuffer.h" + +#import +#import +#import +#import + +/** + * The behavior of this class is modeled on the private function + * _CGDataProviderCreateWithCopyOfData, which is the function used + * by CGBitmapContextCreateImage. + * + * If the buffer is larger than a page, we use mmap and mark it as + * read-only when they are finished drawing. Then we wrap the VM + * in an NSData + */ +@implementation ASCGImageBuffer { + BOOL _createdData; + BOOL _isVM; + NSUInteger _length; +} + +- (instancetype)initWithLength:(NSUInteger)length +{ + if (self = [super init]) { + _length = length; + _isVM = (length >= vm_page_size); + if (_isVM) { + _mutableBytes = mmap(NULL, length, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, VM_MAKE_TAG(VM_MEMORY_COREGRAPHICS_DATA), 0); + if (_mutableBytes == MAP_FAILED) { + NSAssert(NO, @"Failed to map for CG image data."); + _isVM = NO; + } + } + + // Check the VM flag again because we may have failed above. + if (!_isVM) { + _mutableBytes = calloc(1, length); + } + } + return self; +} + +- (void)dealloc +{ + if (!_createdData) { + [ASCGImageBuffer deallocateBuffer:_mutableBytes length:_length isVM:_isVM]; + } +} + +- (CGDataProviderRef)createDataProviderAndInvalidate +{ + NSAssert(!_createdData, @"Should not create data provider from buffer multiple times."); + _createdData = YES; + + // Mark the pages as read-only. + if (_isVM) { + __unused kern_return_t result = vm_protect(mach_task_self(), (vm_address_t)_mutableBytes, _length, true, VM_PROT_READ); + NSAssert(result == noErr, @"Error marking buffer as read-only: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]); + } + + // Wrap in an NSData + BOOL isVM = _isVM; + NSData *d = [[NSData alloc] initWithBytesNoCopy:_mutableBytes length:_length deallocator:^(void * _Nonnull bytes, NSUInteger length) { + [ASCGImageBuffer deallocateBuffer:bytes length:length isVM:isVM]; + }]; + return CGDataProviderCreateWithCFData((__bridge CFDataRef)d); +} + ++ (void)deallocateBuffer:(void *)buf length:(NSUInteger)length isVM:(BOOL)isVM +{ + if (isVM) { + __unused kern_return_t result = vm_deallocate(mach_task_self(), (vm_address_t)buf, length); + NSAssert(result == noErr, @"Failed to unmap cg image buffer: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]); + } else { + free(buf); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASCellNode.h b/submodules/AsyncDisplayKit/Source/ASCellNode.h new file mode 100644 index 0000000000..35d836ce5f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCellNode.h @@ -0,0 +1,263 @@ +// +// ASCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCellNode, ASTextNode; +@protocol ASRangeManagingNode; + +typedef NSUInteger ASCellNodeAnimation; + +typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { + /** + * Indicates a cell has just became visible + */ + ASCellNodeVisibilityEventVisible, + /** + * Its position (determined by scrollView.contentOffset) has changed while at least 1px remains visible. + * It is possible that 100% of the cell is visible both before and after and only its position has changed, + * or that the position change has resulted in more or less of the cell being visible. + * Use CGRectIntersect between cellFrame and scrollView.bounds to get this rectangle + */ + ASCellNodeVisibilityEventVisibleRectChanged, + /** + * Indicates a cell is no longer visible + */ + ASCellNodeVisibilityEventInvisible, + /** + * Indicates user has started dragging the visible cell + */ + ASCellNodeVisibilityEventWillBeginDragging, + /** + * Indicates user has ended dragging the visible cell + */ + ASCellNodeVisibilityEventDidEndDragging, +}; + +/** + * Generic cell node. Subclass this instead of `ASDisplayNode` to use with `ASTableView` and `ASCollectionView`. + + * @note When a cell node is contained inside a collection view (or table view), + * calling `-setNeedsLayout` will also notify the collection on the main thread + * so that the collection can update its item layout if the cell's size changed. + */ +@interface ASCellNode : ASDisplayNode + +/** + * @abstract When enabled, ensures that the cell is completely displayed before allowed onscreen. + * + * @default NO + * @discussion Normally, ASCellNodes are preloaded and have finished display before they are onscreen. + * However, if the Table or Collection's rangeTuningParameters are set to small values (or 0), + * or if the user is scrolling rapidly on a slow device, it is possible for a cell's display to + * be incomplete when it becomes visible. + * + * In this case, normally placeholder states are shown and scrolling continues uninterrupted. + * The finished, drawn content is then shown as soon as it is ready. + * + * With this property set to YES, the main thread will be blocked until display is complete for + * the cell. This is more similar to UIKit, and in fact makes AsyncDisplayKit scrolling visually + * indistinguishable from UIKit's, except being faster. + * + * Using this option does not eliminate all of the performance advantages of AsyncDisplayKit. + * Normally, a cell has been preloading and is almost done when it reaches the screen, so the + * blocking time is very short. If the rangeTuningParameters are set to 0, still this option + * outperforms UIKit: while the main thread is waiting, subnode display executes concurrently. + */ +@property BOOL neverShowPlaceholders; + +/* + * The kind of supplementary element this node represents, if any. + * + * @return The supplementary element kind, or @c nil if this node does not represent a supplementary element. + */ +@property (nullable, copy, readonly) NSString *supplementaryElementKind; + +/* + * The layout attributes currently assigned to this node, if any. + * + * @discussion This property is useful because it is set before @c collectionView:willDisplayNode:forItemAtIndexPath: + * is called, when the node is not yet in the hierarchy and its frame cannot be converted to/from other nodes. Instead + * you can use the layout attributes object to learn where and how the cell will be displayed. + */ +@property (nullable, copy, readonly) UICollectionViewLayoutAttributes *layoutAttributes; + +/** + * A Boolean value that is synchronized with the underlying collection or tableView cell property. + * Setting this value is equivalent to calling selectItem / deselectItem on the collection or table. + */ +@property (getter=isSelected) BOOL selected; + +/** + * A Boolean value that is synchronized with the underlying collection or tableView cell property. + * Setting this value is equivalent to calling highlightItem / unHighlightItem on the collection or table. + */ +@property (getter=isHighlighted) BOOL highlighted; + +/** + * The current index path of this cell node, or @c nil if this node is + * not a valid item inside a table node or collection node. + */ +@property (nullable, copy, readonly) NSIndexPath *indexPath; + +/** + * BETA: API is under development. We will attempt to provide an easy migration pathway for any changes. + * + * The view-model currently assigned to this node, if any. + * + * This property may be set off the main thread, but this method will never be invoked concurrently on the + */ +@property (nullable) id nodeModel; + +/** + * Asks the node whether it can be updated to the given node model. + * + * The default implementation returns YES if the class matches that of the current view-model. + */ +- (BOOL)canUpdateToNodeModel:(id)nodeModel; + +/** + * The backing view controller, or @c nil if the node wasn't initialized with backing view controller + * @note This property must be accessed on the main thread. + */ +@property (nullable, nonatomic, readonly) UIViewController *viewController; + + +/** + * The table- or collection-node that this cell is a member of, if any. + */ +@property (nullable, weak, readonly) id owningNode; + +/* + * ASCellNode must forward touch events in order for UITableView and UICollectionView tap handling to work. Overriding + * these methods (e.g. for highlighting) requires the super method be called. + */ +- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; +- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; +- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; +- (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * Called by the system when ASCellNode is used with an ASCollectionNode. It will not be called by ASTableNode. + * When the UICollectionViewLayout object returns a new UICollectionViewLayoutAttributes object, the corresponding ASCellNode will be updated. + * See UICollectionViewCell's applyLayoutAttributes: for a full description. +*/ +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes; + +/** + * @abstract Initializes a cell with a given view controller block. + * + * @param viewControllerBlock The block that will be used to create the backing view controller. + * @param didLoadBlock The block that will be called after the view controller's view is loaded. + * + * @return An ASCellNode created using the root view of the view controller provided by the viewControllerBlock. + * The view controller's root view is resized to match the calculated size produced during layout. + * + */ +- (instancetype)initWithViewControllerBlock:(ASDisplayNodeViewControllerBlock)viewControllerBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock; + +/** + * @abstract Notifies the cell node of certain visibility events, such as changing visible rect. + * + * @warning In cases where an ASCellNode is used as a plain node – i.e. not returned from the + * nodeBlockForItemAtIndexPath/nodeForItemAtIndexPath data source methods – this method will + * deliver only the `Visible` and `Invisible` events, `scrollView` will be nil, and + * `cellFrame` will be the zero rect. + */ +- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(nullable UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame; + +#pragma mark - UITableViewCell specific passthrough properties + +/* @abstract The selection style when a tap on a cell occurs + * @default UITableViewCellSelectionStyleDefault + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + */ +@property UITableViewCellSelectionStyle selectionStyle; + +/* @abstract The focus style when a cell is focused + * @default UITableViewCellFocusStyleDefault + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + */ +@property UITableViewCellFocusStyle focusStyle; + +/* @abstract The view used as the background of the cell when it is selected. + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + * ASCollectionView uses these properties when configuring UICollectionViewCells that host ASCellNodes. + */ +@property (nullable) UIView *selectedBackgroundView; + +/* @abstract The view used as the background of the cell. + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + * ASCollectionView uses these properties when configuring UICollectionViewCells that host ASCellNodes. + */ +@property (nullable) UIView *backgroundView; + +/* @abstract The accessory type view on the right side of the cell. Please take care of your ASLayoutSpec so that doesn't overlay the accessoryView + * @default UITableViewCellAccessoryNone + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + */ +@property UITableViewCellAccessoryType accessoryType; + +/* @abstract The inset of the cell separator line + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + */ +@property UIEdgeInsets separatorInset; + +@end + +@interface ASCellNode (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (void)setLayerBacked:(BOOL)layerBacked AS_UNAVAILABLE("ASCellNode does not support layer-backing, although subnodes may be layer-backed."); + +@end + + +/** + * Simple label-style cell node. Read its source for an example of custom s. + */ +@interface ASTextCellNode : ASCellNode + +/** + * Initializes a text cell with given text attributes and text insets + */ +- (instancetype)initWithAttributes:(NSDictionary *)textAttributes insets:(UIEdgeInsets)textInsets; + +/** + * Text to display. + */ +@property (nullable, copy) NSString *text; + +/** + * A dictionary containing key-value pairs for text attributes. You can specify the font, text color, text shadow color, and text shadow offset using the keys listed in NSString UIKit Additions Reference. + */ +@property (copy) NSDictionary *textAttributes; + +/** + * The text inset or outset for each edge. The default value is 15.0 horizontal and 11.0 vertical padding. + */ +@property UIEdgeInsets textInsets; + +/** + * The text node used by this cell node. + */ +@property (readonly) ASTextNode *textNode; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCellNode.mm b/submodules/AsyncDisplayKit/Source/ASCellNode.mm new file mode 100644 index 0000000000..5793dfabd7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCellNode.mm @@ -0,0 +1,488 @@ +// +// ASCellNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import + +#pragma mark - +#pragma mark ASCellNode + +@interface ASCellNode () +{ + ASDisplayNodeViewControllerBlock _viewControllerBlock; + ASDisplayNodeDidLoadBlock _viewControllerDidLoadBlock; + ASDisplayNode *_viewControllerNode; + UIViewController *_viewController; + BOOL _suspendInteractionDelegate; + BOOL _selected; + BOOL _highlighted; + UICollectionViewLayoutAttributes *_layoutAttributes; +} + +@end + +@implementation ASCellNode +@synthesize interactionDelegate = _interactionDelegate; + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // Use UITableViewCell defaults + _selectionStyle = UITableViewCellSelectionStyleDefault; + _focusStyle = UITableViewCellFocusStyleDefault; + self.clipsToBounds = YES; + + return self; +} + +- (instancetype)initWithViewControllerBlock:(ASDisplayNodeViewControllerBlock)viewControllerBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [super init])) + return nil; + + ASDisplayNodeAssertNotNil(viewControllerBlock, @"should initialize with a valid block that returns a UIViewController"); + _viewControllerBlock = viewControllerBlock; + _viewControllerDidLoadBlock = didLoadBlock; + + return self; +} + +- (void)didLoad +{ + [super didLoad]; + + if (_viewControllerBlock != nil) { + + _viewController = _viewControllerBlock(); + _viewControllerBlock = nil; + + if ([_viewController isKindOfClass:[ASViewController class]]) { + ASViewController *asViewController = (ASViewController *)_viewController; + _viewControllerNode = asViewController.node; + [_viewController loadViewIfNeeded]; + } else { + // Careful to avoid retain cycle + UIViewController *viewController = _viewController; + _viewControllerNode = [[ASDisplayNode alloc] initWithViewBlock:^{ + return viewController.view; + }]; + } + [self addSubnode:_viewControllerNode]; + + // Since we just loaded our node, and added _viewControllerNode as a subnode, + // _viewControllerNode must have just loaded its view, so now is an appropriate + // time to execute our didLoadBlock, if we were given one. + if (_viewControllerDidLoadBlock != nil) { + _viewControllerDidLoadBlock(self); + _viewControllerDidLoadBlock = nil; + } + } +} + +- (void)layout +{ + [super layout]; + + _viewControllerNode.frame = self.bounds; +} + +- (void)_rootNodeDidInvalidateSize +{ + if (_interactionDelegate != nil) { + [_interactionDelegate nodeDidInvalidateSize:self]; + } else { + [super _rootNodeDidInvalidateSize]; + } +} + +- (void)_layoutTransitionMeasurementDidFinish +{ + if (_interactionDelegate != nil) { + [_interactionDelegate nodeDidInvalidateSize:self]; + } else { + [super _layoutTransitionMeasurementDidFinish]; + } +} + +- (BOOL)isSelected +{ + return ASLockedSelf(_selected); +} + +- (void)setSelected:(BOOL)selected +{ + if (ASLockedSelfCompareAssign(_selected, selected)) { + if (!_suspendInteractionDelegate) { + ASPerformBlockOnMainThread(^{ + [_interactionDelegate nodeSelectedStateDidChange:self]; + }); + } + } +} + +- (BOOL)isHighlighted +{ + return ASLockedSelf(_highlighted); +} + +- (void)setHighlighted:(BOOL)highlighted +{ + if (ASLockedSelfCompareAssign(_highlighted, highlighted)) { + if (!_suspendInteractionDelegate) { + ASPerformBlockOnMainThread(^{ + [_interactionDelegate nodeHighlightedStateDidChange:self]; + }); + } + } +} + +- (void)__setSelectedFromUIKit:(BOOL)selected; +{ + // Note: Race condition could mean redundant sets. Risk is low. + if (ASLockedSelf(_selected != selected)) { + _suspendInteractionDelegate = YES; + self.selected = selected; + _suspendInteractionDelegate = NO; + } +} + +- (void)__setHighlightedFromUIKit:(BOOL)highlighted; +{ + // Note: Race condition could mean redundant sets. Risk is low. + if (ASLockedSelf(_highlighted != highlighted)) { + _suspendInteractionDelegate = YES; + self.highlighted = highlighted; + _suspendInteractionDelegate = NO; + } +} + +- (BOOL)canUpdateToNodeModel:(id)nodeModel +{ + return [self.nodeModel class] == [nodeModel class]; +} + +- (NSIndexPath *)indexPath +{ + return [self.owningNode indexPathForNode:self]; +} + +- (UIViewController *)viewController +{ + ASDisplayNodeAssertMainThread(); + // Force the view to load so that we will create the + // view controller if we haven't already. + if (self.isNodeLoaded == NO) { + [self view]; + } + return _viewController; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert([self.view isKindOfClass:_ASDisplayView.class], @"ASCellNode views must be of type _ASDisplayView"); + [(_ASDisplayView *)self.view __forwardTouchesBegan:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert([self.view isKindOfClass:_ASDisplayView.class], @"ASCellNode views must be of type _ASDisplayView"); + [(_ASDisplayView *)self.view __forwardTouchesMoved:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert([self.view isKindOfClass:_ASDisplayView.class], @"ASCellNode views must be of type _ASDisplayView"); + [(_ASDisplayView *)self.view __forwardTouchesEnded:touches withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert([self.view isKindOfClass:_ASDisplayView.class], @"ASCellNode views must be of type _ASDisplayView"); + [(_ASDisplayView *)self.view __forwardTouchesCancelled:touches withEvent:event]; +} + +#pragma clang diagnostic pop + +- (UICollectionViewLayoutAttributes *)layoutAttributes +{ + return ASLockedSelf(_layoutAttributes); +} + +- (void)setLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + ASDisplayNodeAssertMainThread(); + if (ASLockedSelfCompareAssignObjects(_layoutAttributes, layoutAttributes)) { + if (layoutAttributes != nil) { + [self applyLayoutAttributes:layoutAttributes]; + } + } +} + +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + // To be overriden by subclasses +} + +- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame +{ + // To be overriden by subclasses +} + +- (void)didEnterVisibleState +{ + [super didEnterVisibleState]; + if (self.neverShowPlaceholders) { + [self recursivelyEnsureDisplaySynchronously:YES]; + } + [self handleVisibilityChange:YES]; +} + +- (void)didExitVisibleState +{ + [super didExitVisibleState]; + [self handleVisibilityChange:NO]; +} + ++ (BOOL)requestsVisibilityNotifications +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[NSCache alloc] init]; + }); + NSNumber *result = [cache objectForKey:self]; + if (result == nil) { + BOOL overrides = ASSubclassOverridesSelector([ASCellNode class], self, @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:)); + result = overrides ? (NSNumber *)kCFBooleanTrue : (NSNumber *)kCFBooleanFalse; + [cache setObject:result forKey:self]; + } + return (result == (NSNumber *)kCFBooleanTrue); +} + +- (void)handleVisibilityChange:(BOOL)isVisible +{ + if ([self.class requestsVisibilityNotifications] == NO) { + return; // The work below is expensive, and only valuable for subclasses watching visibility events. + } + + // NOTE: This assertion is failing in some apps and will be enabled soon. + // ASDisplayNodeAssert(self.isNodeLoaded, @"Node should be loaded in order for it to become visible or invisible. If not in this situation, we shouldn't trigger creating the view."); + + UIView *view = self.view; + CGRect cellFrame = CGRectZero; + + // Ensure our _scrollView is still valid before converting. It's also possible that we have already been removed from the _scrollView, + // in which case it is not valid to perform a convertRect (this actually crashes on iOS 8). + UIScrollView *scrollView = (_scrollView != nil && view.superview != nil && [view isDescendantOfView:_scrollView]) ? _scrollView : nil; + if (scrollView) { + cellFrame = [view convertRect:view.bounds toView:_scrollView]; + } + + // If we did not convert, we'll pass along CGRectZero and a nil scrollView. The EventInvisible call is thus equivalent to + // didExitVisibileState, but is more convenient for the developer than implementing multiple methods. + [self cellNodeVisibilityEvent:isVisible ? ASCellNodeVisibilityEventVisible + : ASCellNodeVisibilityEventInvisible + inScrollView:scrollView + withCellFrame:cellFrame]; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [super propertiesForDebugDescription]; + + UIScrollView *scrollView = self.scrollView; + + ASDisplayNode *owningNode = scrollView.asyncdisplaykit_node; + if ([owningNode isKindOfClass:[ASCollectionNode class]]) { + NSIndexPath *ip = [(ASCollectionNode *)owningNode indexPathForNode:self]; + if (ip != nil) { + [result addObject:@{ @"indexPath" : ip }]; + } + [result addObject:@{ @"collectionNode" : owningNode }]; + } else if ([owningNode isKindOfClass:[ASTableNode class]]) { + NSIndexPath *ip = [(ASTableNode *)owningNode indexPathForNode:self]; + if (ip != nil) { + [result addObject:@{ @"indexPath" : ip }]; + } + [result addObject:@{ @"tableNode" : owningNode }]; + + } else if ([scrollView isKindOfClass:[ASCollectionView class]]) { + NSIndexPath *ip = [(ASCollectionView *)scrollView indexPathForNode:self]; + if (ip != nil) { + [result addObject:@{ @"indexPath" : ip }]; + } + [result addObject:@{ @"collectionView" : ASObjectDescriptionMakeTiny(scrollView) }]; + + } else if ([scrollView isKindOfClass:[ASTableView class]]) { + NSIndexPath *ip = [(ASTableView *)scrollView indexPathForNode:self]; + if (ip != nil) { + [result addObject:@{ @"indexPath" : ip }]; + } + [result addObject:@{ @"tableView" : ASObjectDescriptionMakeTiny(scrollView) }]; + } + + return result; +} + +- (NSString *)supplementaryElementKind +{ + return self.collectionElement.supplementaryElementKind; +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +- (BOOL)shouldUseUIKitCell +{ + return NO; +} + +@end + + +#pragma mark - +#pragma mark ASWrapperCellNode + +// TODO: Consider if other calls, such as willDisplayCell, should be bridged to this class. +@implementation ASWrapperCellNode : ASCellNode + +- (BOOL)shouldUseUIKitCell +{ + return YES; +} + +@end + + +#pragma mark - +#pragma mark ASTextCellNode + +@implementation ASTextCellNode { + NSDictionary *_textAttributes; + UIEdgeInsets _textInsets; + NSString *_text; +} + +static const CGFloat kASTextCellNodeDefaultFontSize = 18.0f; +static const CGFloat kASTextCellNodeDefaultHorizontalPadding = 15.0f; +static const CGFloat kASTextCellNodeDefaultVerticalPadding = 11.0f; + +- (instancetype)init +{ + return [self initWithAttributes:[ASTextCellNode defaultTextAttributes] insets:[ASTextCellNode defaultTextInsets]]; +} + +- (instancetype)initWithAttributes:(NSDictionary *)textAttributes insets:(UIEdgeInsets)textInsets +{ + self = [super init]; + if (self) { + _textInsets = textInsets; + _textAttributes = [textAttributes copy]; + _textNode = [[ASTextNode alloc] init]; + self.automaticallyManagesSubnodes = YES; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:self.textInsets child:self.textNode]; +} + ++ (NSDictionary *)defaultTextAttributes +{ + return @{NSFontAttributeName : [UIFont systemFontOfSize:kASTextCellNodeDefaultFontSize]}; +} + ++ (UIEdgeInsets)defaultTextInsets +{ + return UIEdgeInsetsMake(kASTextCellNodeDefaultVerticalPadding, kASTextCellNodeDefaultHorizontalPadding, kASTextCellNodeDefaultVerticalPadding, kASTextCellNodeDefaultHorizontalPadding); +} + +- (NSDictionary *)textAttributes +{ + return ASLockedSelf(_textAttributes); +} + +- (void)setTextAttributes:(NSDictionary *)textAttributes +{ + ASDisplayNodeAssertNotNil(textAttributes, @"Invalid text attributes"); + ASLockScopeSelf(); + if (ASCompareAssignCopy(_textAttributes, textAttributes)) { + [self locked_updateAttributedText]; + } +} + +- (UIEdgeInsets)textInsets +{ + return ASLockedSelf(_textInsets); +} + +- (void)setTextInsets:(UIEdgeInsets)textInsets +{ + if (ASLockedSelfCompareAssignCustom(_textInsets, textInsets, UIEdgeInsetsEqualToEdgeInsets)) { + [self setNeedsLayout]; + } +} + +- (NSString *)text +{ + return ASLockedSelf(_text); +} + +- (void)setText:(NSString *)text +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_text, text)) { + [self locked_updateAttributedText]; + } +} + +- (void)locked_updateAttributedText +{ + if (_text == nil) { + _textNode.attributedText = nil; + return; + } + + _textNode.attributedText = [[NSAttributedString alloc] initWithString:_text attributes:_textAttributes]; + [self setNeedsLayout]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionNode+Beta.h b/submodules/AsyncDisplayKit/Source/ASCollectionNode+Beta.h new file mode 100644 index 0000000000..5aefbb8a76 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionNode+Beta.h @@ -0,0 +1,82 @@ +// +// ASCollectionNode+Beta.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +@protocol ASCollectionViewLayoutFacilitatorProtocol, ASCollectionLayoutDelegate, ASBatchFetchingDelegate; +@class ASElementMap; + +NS_ASSUME_NONNULL_BEGIN + +@interface ASCollectionNode (Beta) + +/** + * Allows providing a custom subclass of ASCollectionView to be managed by ASCollectionNode. + * + * @default [ASCollectionView class] is used whenever this property is unset or nil. + */ +@property (nullable, nonatomic) Class collectionViewClass; + +/** + * The elements that are currently displayed. The "UIKit index space". Must be accessed on main thread. + */ +@property (nonatomic, readonly) ASElementMap *visibleElements; + +@property (nullable, readonly) id layoutDelegate; + +@property (nullable, nonatomic, weak) id batchFetchingDelegate; + +/** + * When this mode is enabled, ASCollectionView matches the timing of UICollectionView as closely as + * possible, ensuring that all reload and edit operations are performed on the main thread as + * blocking calls. + * + * This mode is useful for applications that are debugging issues with their collection view + * implementation. In particular, some applications do not properly conform to the API requirement + * of UICollectionView, and these applications may experience difficulties with ASCollectionView. + * Providing this mode allows for developers to work towards resolving technical debt in their + * collection view data source, while ramping up asynchronous collection layout. + * + * NOTE: Because this mode results in expensive operations like cell layout being performed on the + * main thread, it should be used as a tool to resolve data source conformance issues with Apple + * collection view API. + * + * @default defaults to ASCellLayoutModeNone. + */ +@property (nonatomic) ASCellLayoutMode cellLayoutMode; + +/** + * Returns YES if the ASCollectionNode contents are completely synchronized with the underlying collection-view layout. + */ +@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; + +/** + * Schedules a block to be performed (on the main thread) as soon as the completion block is called + * on performBatchUpdates:. + * + * When isSynchronized == YES, the block is run block immediately (before the method returns). + */ +- (void)onDidFinishSynchronizing:(void (^)(void))didFinishSynchronizing; + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(nullable id)layoutFacilitator; + +- (instancetype)initWithLayoutDelegate:(id)layoutDelegate layoutFacilitator:(nullable id)layoutFacilitator; + +- (void)beginUpdates ASDISPLAYNODE_DEPRECATED_MSG("Use -performBatchUpdates:completion: instead."); + +- (void)endUpdatesAnimated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use -performBatchUpdates:completion: instead."); + +- (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion ASDISPLAYNODE_DEPRECATED_MSG("Use -performBatchUpdates:completion: instead."); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionNode.h b/submodules/AsyncDisplayKit/Source/ASCollectionNode.h new file mode 100644 index 0000000000..bbcd8befef --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionNode.h @@ -0,0 +1,954 @@ +// +// ASCollectionNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import +#import +#import + +@protocol ASCollectionViewLayoutFacilitatorProtocol; +@protocol ASCollectionDelegate; +@protocol ASCollectionDataSource; +@class ASCollectionView; + +NS_ASSUME_NONNULL_BEGIN + +/** + * ASCollectionNode is a node based class that wraps an ASCollectionView. It can be used + * as a subnode of another node, and provide room for many (great) features and improvements later on. + */ +@interface ASCollectionNode : ASDisplayNode + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes an ASCollectionNode + * + * @discussion Initializes and returns a newly allocated collection node object with the specified layout. + * + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil. + */ +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout; + +/** + * Initializes an ASCollectionNode + * + * @discussion Initializes and returns a newly allocated collection node object with the specified frame and layout. + * + * @param frame The frame rectangle for the collection view, measured in points. The origin of the frame is relative to the superview in which you plan to add it. This frame is passed to the superclass during initialization. + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil. + */ +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout; + +/** + * Returns the corresponding ASCollectionView + * + * @return view The corresponding ASCollectionView. + */ +@property (readonly) ASCollectionView *view; + +/** + * The object that acts as the asynchronous delegate of the collection view + * + * @discussion The delegate must adopt the ASCollectionDelegate protocol. The collection view maintains a weak reference to the delegate object. + * + * The delegate object is responsible for providing size constraints for nodes and indicating whether batch fetching should begin. + * @note This is a convenience method which sets the asyncDelegate on the collection node's collection view. + */ +@property (nullable, weak) id delegate; + +/** + * The object that acts as the asynchronous data source of the collection view + * + * @discussion The datasource must adopt the ASCollectionDataSource protocol. The collection view maintains a weak reference to the datasource object. + * + * The datasource object is responsible for providing nodes or node creation blocks to the collection view. + * @note This is a convenience method which sets the asyncDatasource on the collection node's collection view. + */ +@property (nullable, weak) id dataSource; + +/** + * The number of screens left to scroll before the delegate -collectionNode:beginBatchFetchingWithContext: is called. + * + * Defaults to two screenfuls. + */ +@property (nonatomic) CGFloat leadingScreensForBatching; + +/* + * A Boolean value that determines whether the collection node will be flipped. + * If the value of this property is YES, the first cell node will be at the bottom of the collection node (as opposed to the top by default). This is useful for chat/messaging apps. The default value is NO. + */ +@property (nonatomic) BOOL inverted; + +/** + * A Boolean value that indicates whether users can select items in the collection node. + * If the value of this property is YES (the default), users can select items. If you want more fine-grained control over the selection of items, you must provide a delegate object and implement the appropriate methods of the UICollectionNodeDelegate protocol. + */ +@property (nonatomic) BOOL allowsSelection; + +/** + * A Boolean value that determines whether users can select more than one item in the collection node. + * This property controls whether multiple items can be selected simultaneously. The default value of this property is NO. + * When the value of this property is YES, tapping a cell adds it to the current selection (assuming the delegate permits the cell to be selected). Tapping the cell again removes it from the selection. + */ +@property (nonatomic) BOOL allowsMultipleSelection; + +/** + * A Boolean value that determines whether bouncing always occurs when vertical scrolling reaches the end of the content. + * The default value of this property is NO. + */ +@property (nonatomic) BOOL alwaysBounceVertical; + +/** + * A Boolean value that determines whether bouncing always occurs when horizontal scrolling reaches the end of the content view. + * The default value of this property is NO. + */ +@property (nonatomic) BOOL alwaysBounceHorizontal; + +/** + * A Boolean value that controls whether the vertical scroll indicator is visible. + * The default value of this property is YES. + */ +@property (nonatomic) BOOL showsVerticalScrollIndicator; + +/** + * A Boolean value that controls whether the horizontal scroll indicator is visible. + * The default value of this property is NO. + */ +@property (nonatomic) BOOL showsHorizontalScrollIndicator; + +/** + * The layout used to organize the node's items. + * + * @discussion Assigning a new layout object to this property causes the new layout to be applied (without animations) to the node’s items. + */ +@property (nonatomic) UICollectionViewLayout *collectionViewLayout; + +/** + * Optional introspection object for the collection node's layout. + * + * @discussion Since supplementary and decoration nodes are controlled by the layout, this object + * is used as a bridge to provide information to the internal data controller about the existence of these views and + * their associated index paths. For collections using `UICollectionViewFlowLayout`, a default inspector + * implementation `ASCollectionViewFlowLayoutInspector` is created and set on this property by default. Custom + * collection layout subclasses will need to provide their own implementation of an inspector object for their + * supplementary elements to be compatible with `ASCollectionNode`'s supplementary node support. + */ +@property (nonatomic, weak) id layoutInspector; + +/** + * The distance that the content view is inset from the collection node edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets contentInset; + +/** + * The offset of the content view's origin from the collection node's origin. Defaults to CGPointZero. + */ +@property (nonatomic) CGPoint contentOffset; + +/** + * Sets the offset from the content node’s origin to the collection node’s origin. + * + * @param contentOffset The offset + * + * @param animated YES to animate to this new offset at a constant velocity, NO to not aniamte and immediately make the transition. + */ +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + +/** + * Tuning parameters for a range type in full mode. + * + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in full mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in full mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType; + +/** + * Tuning parameters for a range type in the specified mode. + * + * @param rangeMode The range mode to get the running parameters for. + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in the given mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in the specified mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeMode The range mode to set the running parameters for. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +/** + * Scrolls the collection to the given item. + * + * @param indexPath The index path of the item. + * @param scrollPosition Where the item should end up after the scroll. + * @param animated Whether the scroll should be animated or not. + * + * This method must be called on the main thread. + */ +- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated; + +/** + * Determines collection node's current scroll direction. Supports 2-axis collection nodes. + * + * @return a bitmask of ASScrollDirection values. + */ +@property (nonatomic, readonly) ASScrollDirection scrollDirection; + +/** + * Determines collection node's scrollable directions. + * + * @return a bitmask of ASScrollDirection values. + */ +@property (nonatomic, readonly) ASScrollDirection scrollableDirections; + +#pragma mark - Editing + +/** + * Registers the given kind of supplementary node for use in creating node-backed supplementary elements. + * + * @param elementKind The kind of supplementary node that will be requested through the data source. + * + * @discussion Use this method to register support for the use of supplementary nodes in place of the default + * `registerClass:forSupplementaryViewOfKind:withReuseIdentifier:` and `registerNib:forSupplementaryViewOfKind:withReuseIdentifier:` + * methods. This method will register an internal backing view that will host the contents of the supplementary nodes + * returned from the data source. + */ +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind; + +/** + * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. + * The data source must be updated to reflect the changes before the update block completes. + * + * @param animated NO to disable animations for this batch + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; + +/** + * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. + * The data source must be updated to reflect the changes before the update block completes. + * + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; + +/** + * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. + * This is typically the concurrent allocation (calling nodeBlocks) and layout of newly inserted + * ASCellNodes. If YES is returned, then calling -waitUntilAllUpdatesAreProcessed may take tens of + * milliseconds to return as it blocks on these concurrent operations. + * + * Returns NO if ASCollectionNode is fully synchronized with the underlying UICollectionView. This + * means that until the next performBatchUpdates: is called, it is safe to compare UIKit values + * (such as from UICollectionViewLayout) with your app's data source. + * + * This method will always return NO if called immediately after -waitUntilAllUpdatesAreProcessed. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; + +/** + * Schedules a block to be performed (on the main thread) after processing of performBatchUpdates: + * is finished (completely synchronized to UIKit). The blocks will be run at the moment that + * -isProcessingUpdates changes from YES to NO; + * + * When isProcessingUpdates == NO, the block is run block immediately (before the method returns). + * + * Blocks scheduled by this mechanism are NOT guaranteed to run in the order they are scheduled. + * They may also be delayed if performBatchUpdates continues to be called; the blocks will wait until + * all running updates are finished. + * + * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. + */ +- (void)onDidFinishProcessingUpdates:(void (^)(void))didFinishProcessingUpdates; + +/** + * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. + */ +- (void)waitUntilAllUpdatesAreProcessed; + +/** + * Inserts one or more sections. + * + * @param sections An index set that specifies the sections to insert. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)insertSections:(NSIndexSet *)sections; + +/** + * Deletes one or more sections. + * + * @param sections An index set that specifies the sections to delete. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteSections:(NSIndexSet *)sections; + +/** + * Reloads the specified sections. + * + * @param sections An index set that specifies the sections to reload. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadSections:(NSIndexSet *)sections; + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; + +/** + * Inserts items at the locations identified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects, each representing an item index and section index that together identify an item. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Deletes the items specified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to delete. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Reloads the specified items. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to reload. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Moves the item at a specified location to a destination location. + * + * @param indexPath The index path identifying the item to move. + * + * @param newIndexPath The index path that is the destination of the move for the item. + * + * @discussion This method must be called from the main thread. The data source must be updated to reflect the changes + * before this method is called. + */ +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @param completion block to run on completion of asynchronous loading or nil. If supplied, the block is run on + * the main thread. + * @warning This method is substantially more expensive than UICollectionView's version. + */ +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion; + + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UICollectionView's version. + */ +- (void)reloadData; + +/** + * Triggers a relayout of all nodes. + * + * @discussion This method invalidates and lays out every cell node in the collection view. + */ +- (void)relayoutItems; + +#pragma mark - Selection + +/** + * The index paths of the selected items, or @c nil if no items are selected. + */ +@property (nullable, nonatomic, copy, readonly) NSArray *indexPathsForSelectedItems; + +/** + * Selects the item at the specified index path and optionally scrolls it into view. + * If the `allowsSelection` property is NO, calling this method has no effect. If there is an existing selection with a different index path and the `allowsMultipleSelection` property is NO, calling this method replaces the previous selection. + * This method does not cause any selection-related delegate methods to be called. + * + * @param indexPath The index path of the item to select. Specifying nil for this parameter clears the current selection. + * + * @param animated Specify YES to animate the change in the selection or NO to make the change without animating it. + * + * @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. For a list of possible values, see `UICollectionViewScrollPosition`. + * + * @discussion This method must be called from the main thread. + */ +- (void)selectItemAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition; + +/** + * Deselects the item at the specified index. + * If the allowsSelection property is NO, calling this method has no effect. + * This method does not cause any selection-related delegate methods to be called. + * + * @param indexPath The index path of the item to select. Specifying nil for this parameter clears the current selection. + * + * @param animated Specify YES to animate the change in the selection or NO to make the change without animating it. + * + * @discussion This method must be called from the main thread. + */ +- (void)deselectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated; + +#pragma mark - Querying Data + +/** + * Retrieves the number of items in the given section. + * + * @param section The section. + * + * @return The number of items. + */ +- (NSInteger)numberOfItemsInSection:(NSInteger)section AS_WARN_UNUSED_RESULT; + +/** + * The number of sections. + */ +@property (nonatomic, readonly) NSInteger numberOfSections; + +/** + * Similar to -visibleCells. + * + * @return an array containing the nodes being displayed on screen. This must be called on the main thread. + */ +@property (nonatomic, readonly) NSArray<__kindof ASCellNode *> *visibleNodes; + +/** + * Retrieves the node for the item at the given index path. + * + * @param indexPath The index path of the requested item. + * + * @return The node for the given item, or @c nil if no item exists at the specified path. + */ +- (nullable __kindof ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Retrieves the node-model for the item at the given index path, if any. + * + * @param indexPath The index path of the requested item. + * + * @return The node-model for the given item, or @c nil if no item exists at the specified path or no node-model was provided. + * + * @warning This API is beta and subject to change. We'll try to provide an easy migration path. + */ +- (nullable id)nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Retrieve the index path for the item with the given node. + * + * @param cellNode A node for an item in the collection node. + * + * @return The indexPath for this item. + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; + +/** + * Retrieve the index paths of all visible items. + * + * @return an array containing the index paths of all visible items. This must be called on the main thread. + */ +@property (nonatomic, readonly) NSArray *indexPathsForVisibleItems; + +/** + * Retrieve the index path of the item at the given point. + * + * @param point The point of the requested item. + * + * @return The indexPath for the item at the given point. This must be called on the main thread. + */ +- (nullable NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point AS_WARN_UNUSED_RESULT; + +/** + * Retrieve the cell at the given index path. + * + * @param indexPath The index path of the requested item. + * + * @return The cell for the given index path. This must be called on the main thread. + */ +- (nullable UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Retrieves the context object for the given section, as provided by the data source in + * the @c collectionNode:contextForSection: method. + * + * @param section The section to get the context for. + * + * @return The context object, or @c nil if no context was provided. + * + * TODO: This method currently accepts @c section in the _view_ index space, but it should + * be in the node index space. To get the context in the view index space (e.g. for subclasses + * of @c UICollectionViewLayout, the user will call the same method on @c ASCollectionView. + */ +- (nullable id)contextForSection:(NSInteger)section AS_WARN_UNUSED_RESULT; + +@end + +@interface ASCollectionNode (Deprecated) + +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -waitUntilAllUpdatesAreProcessed."); + +@end + +/** + * This is a node-based UICollectionViewDataSource. + */ +@protocol ASCollectionDataSource + +@optional + +/** + * Asks the data source for the number of items in the given section of the collection node. + * + * @see @c collectionView:numberOfItemsInSection: + */ +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section; + +/** + * Asks the data source for the number of sections in the collection node. + * + * @see @c numberOfSectionsInCollectionView: + */ +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode; + +/** + * --BETA-- + * Asks the data source for a view-model for the item at the given index path. + * + * @param collectionNode The sender. + * @param indexPath The index path of the item. + * + * @return An object that contains all the data for this item. + */ +- (nullable id)collectionNode:(ASCollectionNode *)collectionNode nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Similar to -collectionNode:nodeForItemAtIndexPath: + * This method takes precedence over collectionNode:nodeForItemAtIndexPath: if implemented. + * + * @param collectionNode The sender. + * @param indexPath The index path of the item. + * + * @return a block that creates the node for display for this item. + * Must be thread-safe (can be called on the main thread or a background + * queue) and should not implement reuse (it will be called once per row). + */ +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Similar to -collectionView:cellForItemAtIndexPath:. + * + * @param collectionNode The sender. + * @param indexPath The index path of the item. + * + * @return A node to display for the given item. This will be called on the main thread and should + * not implement reuse (it will be called once per item). Unlike UICollectionView's version, + * this method is not called when the item is about to display. + */ +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the data source to provide a node-block to display for the given supplementary element in the collection view. + * + * @param collectionNode The sender. + * @param kind The kind of supplementary element. + * @param indexPath The index path of the supplementary element. + */ +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the data source to provide a node to display for the given supplementary element in the collection view. + * + * @param collectionNode The sender. + * @param kind The kind of supplementary element. + * @param indexPath The index path of the supplementary element. + */ +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the data source to provide a context object for the given section. This object + * can later be retrieved by calling @c contextForSection: and is useful when implementing + * custom @c UICollectionViewLayout subclasses. The context object is ret + * + * @param collectionNode The sender. + * @param section The index of the section to provide context for. + * + * @return A context object to assign to the given section, or @c nil. + */ +- (nullable id)collectionNode:(ASCollectionNode *)collectionNode contextForSection:(NSInteger)section; + +/** + * Asks the data source to provide an array of supplementary element kinds that exist in a given section. + * + * @param collectionNode The sender. + * @param section The index of the section to provide supplementary kinds for. + * + * @return The supplementary element kinds that exist in the given section, if any. + */ +- (NSArray *)collectionNode:(ASCollectionNode *)collectionNode supplementaryElementKindsInSection:(NSInteger)section; + +/** + * Asks the data source if it's possible to move the specified item interactively. + * + * See @p -[UICollectionViewDataSource collectionView:canMoveItemAtIndexPath:] @c. + * + * @param collectionNode The sender. + * @param node The display node for the item that may be moved. + * + * @return Whether the item represented by @p node may be moved. + */ +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode canMoveItemWithNode:(ASCellNode *)node; + +/** + * Called when the user has interactively moved an item. The data source + * should update its internal data store to reflect the move. Note that you + * should not call [collectionNode moveItemAtIndexPath:toIndexPath:] – the + * collection node's internal state will be updated automatically. + * + * * See @p -[UICollectionViewDataSource collectionView:moveItemAtIndexPath:toIndexPath:] @c. + * + * @param collectionNode The sender. + * @param sourceIndexPath The original item index path. + * @param destinationIndexPath The new item index path. + */ +- (void)collectionNode:(ASCollectionNode *)collectionNode moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; + +/** + * Generate a unique identifier for an element in a collection. This helps state restoration persist the scroll position + * of a collection view even when the data in that table changes. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param indexPath The index path of the requested node. + * + * @param collectionNode The sender. + * + * @return a unique identifier for the element at the given path. Return nil if the index path does not exist in the collection. + */ +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inNode:(ASCollectionNode *)collectionNode; + +/** + * Similar to -collectionView:cellForItemAtIndexPath:. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param identifier The model identifier of the element, previously generated by a call to modelIdentifierForElementAtIndexPath + * + * @param collectionNode The sender. + * + * @return the index path to the current position of the matching element in the collection. Return nil if the element is not found. + */ +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inNode:(ASCollectionNode *)collectionNode; + +/** + * Similar to -collectionView:cellForItemAtIndexPath:. + * + * @param collectionView The sender. + * + * @param indexPath The index path of the requested node. + * + * @return a node for display at this indexpath. This will be called on the main thread and should + * not implement reuse (it will be called once per row). Unlike UICollectionView's version, + * this method is not called when the row is about to display. + */ +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +/** + * Similar to -collectionView:nodeForItemAtIndexPath: + * This method takes precedence over collectionView:nodeForItemAtIndexPath: if implemented. + * + * @param collectionView The sender. + * + * @param indexPath The index path of the requested node. + * + * @return a block that creates the node for display at this indexpath. + * Must be thread-safe (can be called on the main thread or a background + * queue) and should not implement reuse (it will be called once per row). + */ +- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +/** + * Asks the collection view to provide a supplementary node to display in the collection view. + * + * @param collectionView An object representing the collection view requesting this information. + * @param kind The kind of supplementary node to provide. + * @param indexPath The index path that specifies the location of the new supplementary node. + */ +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +/** + * Indicator to lock the data source for data fetching in async mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception + * due to the data access in async mode. + * + * @param collectionView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. + */ +- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Data source accesses are on the main thread. Method will not be called."); + +/** + * Indicator to unlock the data source for data fetching in async mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception + * due to the data access in async mode. + * + * @param collectionView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. + */ +- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Data source accesses are on the main thread. Method will not be called."); + +@end + +/** + * This is a node-based UICollectionViewDelegate. + */ +@protocol ASCollectionDelegate + +@optional + +/** + * Provides the constrained size range for measuring the given item. + * + * @param collectionNode The sender. + * + * @param indexPath The index path of the item. + * + * @return A constrained size range for layout for the item at this index path. + */ +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node; + +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node; + +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplaySupplementaryElementWithNode:(ASCellNode *)node NS_AVAILABLE_IOS(8_0); +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingSupplementaryElementWithNode:(ASCellNode *)node; + +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionNode:(ASCollectionNode *)collectionNode didHighlightItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionNode:(ASCollectionNode *)collectionNode didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath; +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath; +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionNode:(ASCollectionNode *)collectionNode didDeselectItemAtIndexPath:(NSIndexPath *)indexPath; + +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath; +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath sender:(nullable id)sender; +- (void)collectionNode:(ASCollectionNode *)collectionNode performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath sender:(nullable id)sender; + +/** + * Receive a message that the collection node is near the end of its data set and more data should be fetched if + * necessary. + * + * @param collectionNode The sender. + * @param context A context object that must be notified when the batch fetch is completed. + * + * @discussion You must eventually call -completeBatchFetching: with an argument of YES in order to receive future + * notifications to do batch fetches. This method is called on a background queue. + * + * ASCollectionNode currently only supports batch events for tail loads. If you require a head load, consider + * implementing a UIRefreshControl. + */ +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context; + +/** + * Tell the collection node if batch fetching should begin. + * + * @param collectionNode The sender. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the collection node assumes that it should notify its asyncDelegate when batch fetching + * should occur. + */ +- (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode; + +/** + * Provides the constrained size range for measuring the node at the index path. + * + * @param collectionView The sender. + * + * @param indexPath The index path of the node. + * + * @return A constrained size range for layout the node at this index path. + */ +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's constrainedSizeForItemAtIndexPath: instead. PLEASE NOTE the very subtle method name change."); + +/** + * Informs the delegate that the collection view will add the given node + * at the given index path to the view hierarchy. + * + * @param collectionView The sender. + * @param node The node that will be displayed. + * @param indexPath The index path of the item that will be displayed. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)collectionView:(ASCollectionView *)collectionView willDisplayNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +/** + * Informs the delegate that the collection view did remove the provided node from the view hierarchy. + * This may be caused by the node scrolling out of view, or by deleting the item + * or its containing section with @c deleteItemsAtIndexPaths: or @c deleteSections: . + * + * @param collectionView The sender. + * @param node The node which was removed from the view hierarchy. + * @param indexPath The index path at which the node was located before it was removed. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +- (void)collectionView:(ASCollectionView *)collectionView willBeginBatchFetchWithContext:(ASBatchContext *)context ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +/** + * Tell the collectionView if batch fetching should begin. + * + * @param collectionView The sender. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the collectionView assumes that it should notify its asyncDelegate when batch fetching + * should occur. + */ +- (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +/** + * Informs the delegate that the collection view will add the node + * at the given index path to the view hierarchy. + * + * @param collectionView The sender. + * @param indexPath The index path of the item that will be displayed. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c collectionView:willDisplayNode:forItemAtIndexPath: instead. + */ +- (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +@end + +@protocol ASCollectionDataSourceInterop + +/** + * This method offers compatibility with synchronous, standard UICollectionViewCell objects. + * These cells will **not** have the performance benefits of ASCellNodes (like preloading, async layout, and + * async drawing) - even when mixed within the same ASCollectionNode. + * + * In order to use this method, you must: + * 1. Implement it on your ASCollectionDataSource object. + * 2. Call registerCellClass: on the collectionNode.view (in viewDidLoad, or register an onDidLoad: block). + * 3. Return nil from the nodeBlockForItem...: or nodeForItem...: method. NOTE: it is an error to return + * nil from within a nodeBlock, if you have returned a nodeBlock object. + * 4. Lastly, you must implement a method to provide the size for the cell. There are two ways this is done: + * 4a. UICollectionViewFlowLayout (incl. ASPagerNode). Implement + collectionNode:constrainedSizeForItemAtIndexPath:. + * 4b. Custom collection layouts. Set .layoutInspector and have it implement + collectionView:constrainedSizeForNodeAtIndexPath:. + * + * For an example of using this method with all steps above (including a custom layout, 4b.), + * see the app in examples/CustomCollectionView and enable kShowUICollectionViewCells = YES. + */ +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; + +@optional + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Implement this property and return YES if you want your interop data source to be + * used when dequeuing cells for node-backed items. + * + * If NO (the default), the interop data source will only be consulted in cases + * where no ASCellNode was provided to AsyncDisplayKit. + * + * If YES, the interop data source will always be consulted to dequeue cells, and + * will be expected to return _ASCollectionViewCells in cases where a node was provided. + * + * The default value is NO. + */ +@property (class, nonatomic, readonly) BOOL dequeuesCellsForNodeBackedItems; + +@end + +@protocol ASCollectionDelegateInterop + +@optional + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionNode.mm b/submodules/AsyncDisplayKit/Source/ASCollectionNode.mm new file mode 100644 index 0000000000..6068f1f038 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionNode.mm @@ -0,0 +1,1056 @@ +// +// ASCollectionNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - _ASCollectionPendingState + +@interface _ASCollectionPendingState : NSObject { +@public + std::vector> _tuningParameters; +} +@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) id dataSource; +@property (nonatomic) UICollectionViewLayout *collectionViewLayout; +@property (nonatomic) ASLayoutRangeMode rangeMode; +@property (nonatomic) BOOL allowsSelection; // default is YES +@property (nonatomic) BOOL allowsMultipleSelection; // default is NO +@property (nonatomic) BOOL inverted; //default is NO +@property (nonatomic) ASCellLayoutMode cellLayoutMode; +@property (nonatomic) CGFloat leadingScreensForBatching; +@property (nonatomic, weak) id layoutInspector; +@property (nonatomic) BOOL alwaysBounceVertical; +@property (nonatomic) BOOL alwaysBounceHorizontal; +@property (nonatomic) UIEdgeInsets contentInset; +@property (nonatomic) CGPoint contentOffset; +@property (nonatomic) BOOL animatesContentOffset; +@property (nonatomic) BOOL showsVerticalScrollIndicator; +@property (nonatomic) BOOL showsHorizontalScrollIndicator; +@end + +@implementation _ASCollectionPendingState + +#pragma mark Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self) { + _rangeMode = ASLayoutRangeModeUnspecified; + _tuningParameters = [ASAbstractLayoutController defaultTuningParameters]; + _allowsSelection = YES; + _allowsMultipleSelection = NO; + _inverted = NO; + _contentInset = UIEdgeInsetsZero; + _contentOffset = CGPointZero; + _animatesContentOffset = NO; + _showsVerticalScrollIndicator = YES; + _showsHorizontalScrollIndicator = YES; + } + return self; +} + +#pragma mark Tuning Parameters + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [self tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + return [self setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + ASDisplayNodeAssert(rangeMode < _tuningParameters.size() && rangeType < _tuningParameters[rangeMode].size(), @"Requesting a range that is OOB for the configured tuning parameters"); + return _tuningParameters[rangeMode][rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + ASDisplayNodeAssert(rangeMode < _tuningParameters.size() && rangeType < _tuningParameters[rangeMode].size(), @"Setting a range that is OOB for the configured tuning parameters"); + _tuningParameters[rangeMode][rangeType] = tuningParameters; +} + +@end + +#pragma mark - ASCollectionNode + +@interface ASCollectionNode () +{ + AS::RecursiveMutex _environmentStateLock; + Class _collectionViewClass; + id _batchFetchingDelegate; +} +@property (nonatomic) _ASCollectionPendingState *pendingState; +@property (nonatomic, weak) ASRangeController *rangeController; +@end + +@implementation ASCollectionNode + +#pragma mark Lifecycle + +- (Class)collectionViewClass +{ + return _collectionViewClass ? : [ASCollectionView class]; +} + +- (void)setCollectionViewClass:(Class)collectionViewClass +{ + if (_collectionViewClass != collectionViewClass) { + ASDisplayNodeAssert([collectionViewClass isSubclassOfClass:[ASCollectionView class]] || collectionViewClass == Nil, @"ASCollectionNode requires that .collectionViewClass is an ASCollectionView subclass"); + ASDisplayNodeAssert([self isNodeLoaded] == NO, @"ASCollectionNode's .collectionViewClass cannot be changed after the view is loaded"); + _collectionViewClass = collectionViewClass; + } +} + +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout +{ + return [self initWithFrame:CGRectZero collectionViewLayout:layout layoutFacilitator:nil]; +} + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout +{ + return [self initWithFrame:frame collectionViewLayout:layout layoutFacilitator:nil]; +} + +- (instancetype)initWithLayoutDelegate:(id)layoutDelegate layoutFacilitator:(id)layoutFacilitator +{ + return [self initWithFrame:CGRectZero collectionViewLayout:[[ASCollectionLayout alloc] initWithLayoutDelegate:layoutDelegate] layoutFacilitator:layoutFacilitator]; +} + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(id)layoutFacilitator +{ + if (self = [super init]) { + // Must call the setter here to make sure pendingState is created and the layout is configured. + [self setCollectionViewLayout:layout]; + + __weak __typeof__(self) weakSelf = self; + [self setViewBlock:^{ + __typeof__(self) strongSelf = weakSelf; + return [[[strongSelf collectionViewClass] alloc] _initWithFrame:frame collectionViewLayout:strongSelf->_pendingState.collectionViewLayout layoutFacilitator:layoutFacilitator owningNode:strongSelf eventLog:ASDisplayNodeGetEventLog(strongSelf)]; + }]; + } + return self; +} + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED +- (void)dealloc +{ + if (self.nodeLoaded) { + __weak UIView *view = self.view; + ASPerformBlockOnMainThread(^{ + ASDisplayNodeCAssertNil(view.superview, @"Node's view should be removed from hierarchy."); + }); + } +} +#endif + +#pragma mark ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + ASCollectionView *view = self.view; + view.collectionNode = self; + + _rangeController = view.rangeController; + + if (_pendingState) { + _ASCollectionPendingState *pendingState = _pendingState; + self.pendingState = nil; + view.asyncDelegate = pendingState.delegate; + view.asyncDataSource = pendingState.dataSource; + view.inverted = pendingState.inverted; + view.allowsSelection = pendingState.allowsSelection; + view.allowsMultipleSelection = pendingState.allowsMultipleSelection; + view.cellLayoutMode = pendingState.cellLayoutMode; + view.layoutInspector = pendingState.layoutInspector; + view.showsVerticalScrollIndicator = pendingState.showsVerticalScrollIndicator; + view.showsHorizontalScrollIndicator = pendingState.showsHorizontalScrollIndicator; + + // Only apply these flags if they're enabled; the view might come with them turned on. + if (pendingState.alwaysBounceVertical) { + view.alwaysBounceVertical = YES; + } + if (pendingState.alwaysBounceHorizontal) { + view.alwaysBounceHorizontal = YES; + } + + UIEdgeInsets contentInset = pendingState.contentInset; + if (!UIEdgeInsetsEqualToEdgeInsets(contentInset, UIEdgeInsetsZero)) { + view.contentInset = contentInset; + } + + CGPoint contentOffset = pendingState.contentOffset; + if (!CGPointEqualToPoint(contentOffset, CGPointZero)) { + [view setContentOffset:contentOffset animated:pendingState.animatesContentOffset]; + } + + const auto tuningParametersVector = pendingState->_tuningParameters; + const auto tuningParametersVectorSize = tuningParametersVector.size(); + for (NSInteger rangeMode = 0; rangeMode < tuningParametersVectorSize; rangeMode++) { + const auto tuningparametersRangeModeVector = tuningParametersVector[rangeMode]; + const auto tuningParametersVectorRangeModeSize = tuningparametersRangeModeVector.size(); + for (NSInteger rangeType = 0; rangeType < tuningParametersVectorRangeModeSize; rangeType++) { + ASRangeTuningParameters tuningParameters = tuningparametersRangeModeVector[rangeType]; + [_rangeController setTuningParameters:tuningParameters + forRangeMode:(ASLayoutRangeMode)rangeMode + rangeType:(ASLayoutRangeType)rangeType]; + } + } + + if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { + [_rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; + } + + // Don't need to set collectionViewLayout to the view as the layout was already used to init the view in view block. + } +} + +- (ASCollectionView *)view +{ + return (ASCollectionView *)[super view]; +} + +- (void)clearContents +{ + [super clearContents]; + [self.rangeController clearContents]; +} + +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + [ASRangeController layoutDebugOverlayIfNeeded]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + // ASCollectionNode is often nested inside of other collections. In this case, ASHierarchyState's RangeManaged bit will be set. + // Intentionally allocate the view here and trigger a layout pass on it, which in turn will trigger the intial data load. + // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. + // TODO (ASCL) If this node supports async layout, kick off the initial data load without allocating the view + if (ASHierarchyStateIncludesRangeManaged(self.hierarchyState) && CGRectEqualToRect(self.bounds, CGRectZero) == NO) { + [self.view layoutIfNeeded]; + } +} + +#if ASRangeControllerLoggingEnabled +- (void)didEnterVisibleState +{ + [super didEnterVisibleState]; + NSLog(@"%@ - visible: YES", self); +} + +- (void)didExitVisibleState +{ + [super didExitVisibleState]; + NSLog(@"%@ - visible: NO", self); +} +#endif + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + [self.rangeController clearPreloadedData]; +} + +#pragma mark Setter / Getter + +// TODO: Implement this without the view. Then revisit ASLayoutElementCollectionTableSetTraitCollection +- (ASDataController *)dataController +{ + return self.view.dataController; +} + +- (_ASCollectionPendingState *)pendingState +{ + if (!_pendingState && ![self isNodeLoaded]) { + self.pendingState = [[_ASCollectionPendingState alloc] init]; + } + ASDisplayNodeAssert(![self isNodeLoaded] || !_pendingState, @"ASCollectionNode should not have a pendingState once it is loaded"); + return _pendingState; +} + +- (void)setInverted:(BOOL)inverted +{ + self.transform = inverted ? CATransform3DMakeScale(1, -1, 1) : CATransform3DIdentity; + if ([self pendingState]) { + _pendingState.inverted = inverted; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.inverted = inverted; + } +} + +- (BOOL)inverted +{ + if ([self pendingState]) { + return _pendingState.inverted; + } else { + return self.view.inverted; + } +} + +- (void)setLayoutInspector:(id)layoutInspector +{ + if ([self pendingState]) { + _pendingState.layoutInspector = layoutInspector; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.layoutInspector = layoutInspector; + } +} + +- (id)layoutInspector +{ + if ([self pendingState]) { + return _pendingState.layoutInspector; + } else { + return self.view.layoutInspector; + } +} + +- (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching +{ + if ([self pendingState]) { + _pendingState.leadingScreensForBatching = leadingScreensForBatching; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.leadingScreensForBatching = leadingScreensForBatching; + } +} + +- (CGFloat)leadingScreensForBatching +{ + if ([self pendingState]) { + return _pendingState.leadingScreensForBatching; + } else { + return self.view.leadingScreensForBatching; + } +} + +- (void)setDelegate:(id )delegate +{ + if ([self pendingState]) { + _pendingState.delegate = delegate; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASCollectionView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDelegate = delegate; + }); + } +} + +- (id )delegate +{ + if ([self pendingState]) { + return _pendingState.delegate; + } else { + return self.view.asyncDelegate; + } +} + +- (void)setDataSource:(id )dataSource +{ + if ([self pendingState]) { + _pendingState.dataSource = dataSource; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASCollectionView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDataSource = dataSource; + }); + } +} + +- (id )dataSource +{ + if ([self pendingState]) { + return _pendingState.dataSource; + } else { + return self.view.asyncDataSource; + } +} + +- (void)setAllowsSelection:(BOOL)allowsSelection +{ + if ([self pendingState]) { + _pendingState.allowsSelection = allowsSelection; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.allowsSelection = allowsSelection; + } +} + +- (BOOL)allowsSelection +{ + if ([self pendingState]) { + return _pendingState.allowsSelection; + } else { + return self.view.allowsSelection; + } +} + +- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection +{ + if ([self pendingState]) { + _pendingState.allowsMultipleSelection = allowsMultipleSelection; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.allowsMultipleSelection = allowsMultipleSelection; + } +} + +- (BOOL)allowsMultipleSelection +{ + if ([self pendingState]) { + return _pendingState.allowsMultipleSelection; + } else { + return self.view.allowsMultipleSelection; + } +} + +- (void)setAlwaysBounceVertical:(BOOL)alwaysBounceVertical +{ + if ([self pendingState]) { + _pendingState.alwaysBounceVertical = alwaysBounceVertical; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.alwaysBounceVertical = alwaysBounceVertical; + } +} + +- (BOOL)alwaysBounceVertical +{ + if ([self pendingState]) { + return _pendingState.alwaysBounceVertical; + } else { + return self.view.alwaysBounceVertical; + } +} + +- (void)setAlwaysBounceHorizontal:(BOOL)alwaysBounceHorizontal +{ + if ([self pendingState]) { + _pendingState.alwaysBounceHorizontal = alwaysBounceHorizontal; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.alwaysBounceHorizontal = alwaysBounceHorizontal; + } +} + +- (BOOL)alwaysBounceHorizontal +{ + if ([self pendingState]) { + return _pendingState.alwaysBounceHorizontal; + } else { + return self.view.alwaysBounceHorizontal; + } +} + +- (void)setShowsVerticalScrollIndicator:(BOOL)showsVerticalScrollIndicator +{ + if ([self pendingState]) { + _pendingState.showsVerticalScrollIndicator = showsVerticalScrollIndicator; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.showsVerticalScrollIndicator = showsVerticalScrollIndicator; + } +} + +- (BOOL)showsVerticalScrollIndicator +{ + if ([self pendingState]) { + return _pendingState.showsVerticalScrollIndicator; + } else { + return self.view.showsVerticalScrollIndicator; + } +} + +- (void)setShowsHorizontalScrollIndicator:(BOOL)showsHorizontalScrollIndicator +{ + if ([self pendingState]) { + _pendingState.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator; + } +} + +- (BOOL)showsHorizontalScrollIndicator +{ + if ([self pendingState]) { + return _pendingState.showsHorizontalScrollIndicator; + } else { + return self.view.showsHorizontalScrollIndicator; + } +} + +- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout +{ + if ([self pendingState]) { + [self _configureCollectionViewLayout:layout]; + _pendingState.collectionViewLayout = layout; + } else { + [self _configureCollectionViewLayout:layout]; + self.view.collectionViewLayout = layout; + } +} + +- (UICollectionViewLayout *)collectionViewLayout +{ + if ([self pendingState]) { + return _pendingState.collectionViewLayout; + } else { + return self.view.collectionViewLayout; + } +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + if ([self pendingState]) { + _pendingState.contentInset = contentInset; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.contentInset = contentInset; + } +} + +- (UIEdgeInsets)contentInset +{ + if ([self pendingState]) { + return _pendingState.contentInset; + } else { + return self.view.contentInset; + } +} + +- (void)setContentOffset:(CGPoint)contentOffset +{ + [self setContentOffset:contentOffset animated:NO]; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated +{ + if ([self pendingState]) { + _pendingState.contentOffset = contentOffset; + _pendingState.animatesContentOffset = animated; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + [self.view setContentOffset:contentOffset animated:animated]; + } +} + +- (CGPoint)contentOffset +{ + if ([self pendingState]) { + return _pendingState.contentOffset; + } else { + return self.view.contentOffset; + } +} + +- (ASScrollDirection)scrollDirection +{ + return [self isNodeLoaded] ? self.view.scrollDirection : ASScrollDirectionNone; +} + +- (ASScrollDirection)scrollableDirections +{ + return [self isNodeLoaded] ? self.view.scrollableDirections : ASScrollDirectionNone; +} + +- (ASElementMap *)visibleElements +{ + ASDisplayNodeAssertMainThread(); + // TODO Own the data controller when view is not yet loaded + return self.dataController.visibleMap; +} + +- (id)layoutDelegate +{ + UICollectionViewLayout *layout = self.collectionViewLayout; + if ([layout isKindOfClass:[ASCollectionLayout class]]) { + return ((ASCollectionLayout *)layout).layoutDelegate; + } + return nil; +} + +- (void)setBatchFetchingDelegate:(id)batchFetchingDelegate +{ + _batchFetchingDelegate = batchFetchingDelegate; +} + +- (id)batchFetchingDelegate +{ + return _batchFetchingDelegate; +} + +- (ASCellLayoutMode)cellLayoutMode +{ + if ([self pendingState]) { + return _pendingState.cellLayoutMode; + } else { + return self.view.cellLayoutMode; + } +} + +- (void)setCellLayoutMode:(ASCellLayoutMode)cellLayoutMode +{ + if ([self pendingState]) { + _pendingState.cellLayoutMode = cellLayoutMode; + } else { + self.view.cellLayoutMode = cellLayoutMode; + } +} + +#pragma mark - Range Tuning + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [self tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + [self setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + if ([self pendingState]) { + return [_pendingState tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + } else { + return [self.rangeController tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + } +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + if ([self pendingState]) { + [_pendingState setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; + } else { + return [self.rangeController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; + } +} + +#pragma mark - Selection + +- (NSArray *)indexPathsForSelectedItems +{ + ASDisplayNodeAssertMainThread(); + ASCollectionView *view = self.view; + return [view convertIndexPathsToCollectionNode:view.indexPathsForSelectedItems]; +} + +- (void)selectItemAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition +{ + ASDisplayNodeAssertMainThread(); + ASCollectionView *collectionView = self.view; + + indexPath = [collectionView convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:YES]; + + if (indexPath != nil) { + [collectionView selectItemAtIndexPath:indexPath animated:animated scrollPosition:scrollPosition]; + } else { + NSLog(@"Failed to select item at index path %@ because the item never reached the view.", indexPath); + } +} + +- (void)deselectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + ASCollectionView *collectionView = self.view; + + indexPath = [collectionView convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:YES]; + + if (indexPath != nil) { + [collectionView deselectItemAtIndexPath:indexPath animated:animated]; + } else { + NSLog(@"Failed to deselect item at index path %@ because the item never reached the view.", indexPath); + } +} + +- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + ASCollectionView *collectionView = self.view; + + indexPath = [collectionView convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:YES]; + + if (indexPath != nil) { + [collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; + } else { + NSLog(@"Failed to scroll to item at index path %@ because the item never reached the view.", indexPath); + } +} + +#pragma mark - Querying Data + +- (void)reloadDataInitiallyIfNeeded +{ + if (!self.dataController.initialReloadDataHasBeenCalled) { + [self reloadData]; + } +} + +- (NSInteger)numberOfItemsInSection:(NSInteger)section +{ + [self reloadDataInitiallyIfNeeded]; + return [self.dataController.pendingMap numberOfItemsInSection:section]; +} + +- (NSInteger)numberOfSections +{ + [self reloadDataInitiallyIfNeeded]; + return self.dataController.pendingMap.numberOfSections; +} + +- (NSArray<__kindof ASCellNode *> *)visibleNodes +{ + ASDisplayNodeAssertMainThread(); + return self.isNodeLoaded ? [self.view visibleNodes] : @[]; +} + +- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self reloadDataInitiallyIfNeeded]; + return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].node; +} + +- (id)nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self reloadDataInitiallyIfNeeded]; + return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].nodeModel; +} + +- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode +{ + return [self.dataController.pendingMap indexPathForElement:cellNode.collectionElement]; +} + +- (NSArray *)indexPathsForVisibleItems +{ + ASDisplayNodeAssertMainThread(); + NSMutableArray *indexPathsArray = [NSMutableArray new]; + for (ASCellNode *cell in [self visibleNodes]) { + NSIndexPath *indexPath = [self indexPathForNode:cell]; + if (indexPath) { + [indexPathsArray addObject:indexPath]; + } + } + return indexPathsArray; +} + +- (nullable NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point +{ + ASDisplayNodeAssertMainThread(); + ASCollectionView *collectionView = self.view; + + NSIndexPath *indexPath = [collectionView indexPathForItemAtPoint:point]; + if (indexPath != nil) { + return [collectionView convertIndexPathToCollectionNode:indexPath]; + } + return indexPath; +} + +- (nullable UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + ASCollectionView *collectionView = self.view; + + indexPath = [collectionView convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:YES]; + if (indexPath == nil) { + return nil; + } + return [collectionView cellForItemAtIndexPath:indexPath]; +} + +- (id)contextForSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + return [self.dataController.pendingMap contextForSection:section]; +} + +#pragma mark - Editing + +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind +{ + [self.view registerSupplementaryNodeOfKind:elementKind]; +} + +- (void)performBatchAnimated:(BOOL)animated updates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view performBatchAnimated:animated updates:updates completion:completion]; + } else { + if (updates) { + updates(); + } + if (completion) { + completion(YES); + } + } +} + +- (void)performBatchUpdates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion +{ + [self performBatchAnimated:UIView.areAnimationsEnabled updates:updates completion:completion]; +} + +- (BOOL)isProcessingUpdates +{ + return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO); +} + +- (void)onDidFinishProcessingUpdates:(void (^)())completion +{ + if (!completion) { + return; + } + if (!self.nodeLoaded) { + completion(); + } else { + [self.view onDidFinishProcessingUpdates:completion]; + } +} + +- (BOOL)isSynchronized +{ + return (self.nodeLoaded ? [self.view isSynchronized] : YES); +} + +- (void)onDidFinishSynchronizing:(void (^)())completion +{ + if (!completion) { + return; + } + if (!self.nodeLoaded) { + completion(); + } else { + [self.view onDidFinishSynchronizing:completion]; + } +} + +- (void)waitUntilAllUpdatesAreProcessed +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view waitUntilAllUpdatesAreCommitted]; + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)waitUntilAllUpdatesAreCommitted +{ + [self waitUntilAllUpdatesAreProcessed]; +} +#pragma clang diagnostic pop + +- (void)reloadDataWithCompletion:(void (^)())completion +{ + ASDisplayNodeAssertMainThread(); + if (!self.nodeLoaded) { + return; + } + + [self performBatchUpdates:^{ + [self.view.changeSet reloadData]; + } completion:^(BOOL finished){ + if (completion) { + completion(); + } + }]; +} + +- (void)reloadData +{ + [self reloadDataWithCompletion:nil]; +} + +- (void)relayoutItems +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view relayoutItems]; + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view beginUpdates]; + } +} + +- (void)endUpdatesAnimated:(BOOL)animated +{ + [self endUpdatesAnimated:animated completion:nil]; +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view endUpdatesAnimated:animated completion:completion]; + } +} +#pragma clang diagnostic pop + +- (void)invalidateFlowLayoutDelegateMetrics { + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view invalidateFlowLayoutDelegateMetrics]; + } +} + +- (void)insertSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view insertSections:sections]; + } +} + +- (void)deleteSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view deleteSections:sections]; + } +} + +- (void)reloadSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view reloadSections:sections]; + } +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view moveSection:section toSection:newSection]; + } +} + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view insertItemsAtIndexPaths:indexPaths]; + } +} + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view deleteItemsAtIndexPaths:indexPaths]; + } +} + +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view reloadItemsAtIndexPaths:indexPaths]; + } +} + +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; + } +} + +#pragma mark - ASRangeControllerUpdateRangeProtocol + +- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode; +{ + if ([self pendingState]) { + _pendingState.rangeMode = rangeMode; + } else { + [self.rangeController updateCurrentRangeWithMode:rangeMode]; + } +} + +#pragma mark - ASPrimitiveTraitCollection + +ASLayoutElementCollectionTableSetTraitCollection(_environmentStateLock) + +#pragma mark - Debugging (Private) + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [super propertiesForDebugDescription]; + [result addObject:@{ @"dataSource" : ASObjectDescriptionMakeTiny(self.dataSource) }]; + [result addObject:@{ @"delegate" : ASObjectDescriptionMakeTiny(self.delegate) }]; + return result; +} + +#pragma mark - Private methods + +- (void)_configureCollectionViewLayout:(UICollectionViewLayout *)layout +{ + if ([layout isKindOfClass:[ASCollectionLayout class]]) { + ASCollectionLayout *collectionLayout = (ASCollectionLayout *)layout; + collectionLayout.collectionNode = self; + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionView.h b/submodules/AsyncDisplayKit/Source/ASCollectionView.h new file mode 100644 index 0000000000..740efc41d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionView.h @@ -0,0 +1,496 @@ +// +// ASCollectionView.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import + +@class ASCellNode; +@class ASCollectionNode; +@protocol ASCollectionDataSource; +@protocol ASCollectionDelegate; +@protocol ASCollectionViewLayoutInspecting; +@protocol ASSectionContext; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Asynchronous UICollectionView with Intelligent Preloading capabilities. + * + * @note ASCollectionNode is strongly recommended over ASCollectionView. This class exists for adoption convenience. + */ +@interface ASCollectionView : UICollectionView + +/** + * Returns the corresponding ASCollectionNode + * + * @return collectionNode The corresponding ASCollectionNode, if one exists. + */ +@property (nonatomic, weak, readonly) ASCollectionNode *collectionNode; + +/** + * Retrieves the node for the item at the given index path. + * + * @param indexPath The index path of the requested node. + * @return The node at the given index path, or @c nil if no item exists at the specified path. + */ +- (nullable ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Similar to -indexPathForCell:. + * + * @param cellNode a cellNode in the collection view + * + * @return The index path for this cell node. + * + * @discussion This index path returned by this method is in the _view's_ index space + * and should only be used with @c ASCollectionView directly. To get an index path suitable + * for use with your data source and @c ASCollectionNode, call @c indexPathForNode: on the + * collection node instead. + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; + +/** + * Similar to -supplementaryViewForElementKind:atIndexPath: + * + * @param elementKind The kind of supplementary node to locate. + * @param indexPath The index path of the requested supplementary node. + * + * @return The specified supplementary node or @c nil. + */ +- (nullable ASCellNode *)supplementaryNodeForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Retrieves the context object for the given section, as provided by the data source in + * the @c collectionNode:contextForSection: method. This method must be called on the main thread. + * + * @param section The section to get the context for. + * + * @return The context object, or @c nil if no context was provided. + */ +- (nullable id)contextForSection:(NSInteger)section AS_WARN_UNUSED_RESULT; + +@end + +@interface ASCollectionView (Deprecated) + +/* + * A Boolean value that determines whether the nodes that the data source renders will be flipped. + */ +@property (nonatomic) BOOL inverted ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * The number of screens left to scroll before the delegate -collectionView:beginBatchFetchingWithContext: is called. + * + * Defaults to two screenfuls. + */ +@property (nonatomic) CGFloat leadingScreensForBatching ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * Optional introspection object for the collection view's layout. + * + * @discussion Since supplementary and decoration views are controlled by the collection view's layout, this object + * is used as a bridge to provide information to the internal data controller about the existence of these views and + * their associated index paths. For collection views using `UICollectionViewFlowLayout`, a default inspector + * implementation `ASCollectionViewFlowLayoutInspector` is created and set on this property by default. Custom + * collection view layout subclasses will need to provide their own implementation of an inspector object for their + * supplementary views to be compatible with `ASCollectionView`'s supplementary node support. + */ +@property (nonatomic, weak) id layoutInspector ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * Determines collection view's current scroll direction. Supports 2-axis collection views. + * + * @return a bitmask of ASScrollDirection values. + */ +@property (nonatomic, readonly) ASScrollDirection scrollDirection ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * Determines collection view's scrollable directions. + * + * @return a bitmask of ASScrollDirection values. + */ +@property (nonatomic, readonly) ASScrollDirection scrollableDirections ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * Forces the .contentInset to be UIEdgeInsetsZero. + * + * @discussion By default, UIKit sets the top inset to the navigation bar height, even for horizontally + * scrolling views. This can only be disabled by setting a property on the containing UIViewController, + * automaticallyAdjustsScrollViewInsets, which may not be accessible. ASPagerNode uses this to ensure + * its flow layout behaves predictably and does not log undefined layout warnings. + */ +@property (nonatomic) BOOL zeroContentInsets ASDISPLAYNODE_DEPRECATED_MSG("Set automaticallyAdjustsScrollViewInsets=NO on your view controller instead."); + +/** + * The distance that the content view is inset from the collection view edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets contentInset ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead"); + +/** + * The point at which the origin of the content view is offset from the origin of the collection view. + */ +@property (nonatomic) CGPoint contentOffset ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * The object that acts as the asynchronous delegate of the collection view + * + * @discussion The delegate must adopt the ASCollectionDelegate protocol. The collection view maintains a weak reference to the delegate object. + * + * The delegate object is responsible for providing size constraints for nodes and indicating whether batch fetching should begin. + */ +@property (nonatomic, weak) id asyncDelegate ASDISPLAYNODE_DEPRECATED_MSG("Please use ASCollectionNode's .delegate property instead."); + +/** + * The object that acts as the asynchronous data source of the collection view + * + * @discussion The datasource must adopt the ASCollectionDataSource protocol. The collection view maintains a weak reference to the datasource object. + * + * The datasource object is responsible for providing nodes or node creation blocks to the collection view. + */ +@property (nonatomic, weak) id asyncDataSource ASDISPLAYNODE_DEPRECATED_MSG("Please use ASCollectionNode's .dataSource property instead."); + +/** + * Initializes an ASCollectionView + * + * @discussion Initializes and returns a newly allocated collection view object with the specified layout. + * + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil. + */ +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout ASDISPLAYNODE_DEPRECATED_MSG("Please use ASCollectionNode instead of ASCollectionView."); + +/** + * Initializes an ASCollectionView + * + * @discussion Initializes and returns a newly allocated collection view object with the specified frame and layout. + * + * @param frame The frame rectangle for the collection view, measured in points. The origin of the frame is relative to the superview in which you plan to add it. This frame is passed to the superclass during initialization. + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil. + */ +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout ASDISPLAYNODE_DEPRECATED_MSG("Please use ASCollectionNode instead of ASCollectionView."); + +/** + * Tuning parameters for a range type in full mode. + * + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in full mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Set the tuning parameters for a range type in full mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Tuning parameters for a range type in the specified mode. + * + * @param rangeMode The range mode to get the running parameters for. + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in the given mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Set the tuning parameters for a range type in the specified mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeMode The range mode to set the running parameters for. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +- (nullable __kindof UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +- (void)selectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +@property (nonatomic, copy, readonly) NSArray *indexPathsForVisibleItems ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +@property (nullable, nonatomic, copy, readonly) NSArray *indexPathsForSelectedItems ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + +/** + * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. + * The asyncDataSource must be updated to reflect the changes before the update block completes. + * + * @param animated NO to disable animations for this batch + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Perform a batch of updates asynchronously. This method must be called from the main thread. + * The asyncDataSource must be updated to reflect the changes before update block completes. + * + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @param completion block to run on completion of asynchronous loading or nil. If supplied, the block is run on + * the main thread. + * @warning This method is substantially more expensive than UICollectionView's version. + */ +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion AS_UNAVAILABLE("Use ASCollectionNode method instead."); + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UICollectionView's version. + */ +- (void)reloadData AS_UNAVAILABLE("Use ASCollectionNode method instead."); + +/** + * Triggers a relayout of all nodes. + * + * @discussion This method invalidates and lays out every cell node in the collection. + */ +- (void)relayoutItems ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(void (^)(void))completion; +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASCollectionNode waitUntilAllUpdatesAreProcessed] instead."); + +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; +- (void)onDidFinishSynchronizing:(void (^)(void))completion; + +/** + * Registers the given kind of supplementary node for use in creating node-backed supplementary views. + * + * @param elementKind The kind of supplementary node that will be requested through the data source. + * + * @discussion Use this method to register support for the use of supplementary nodes in place of the default + * `registerClass:forSupplementaryViewOfKind:withReuseIdentifier:` and `registerNib:forSupplementaryViewOfKind:withReuseIdentifier:` + * methods. This method will register an internal backing view that will host the contents of the supplementary nodes + * returned from the data source. + */ +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Inserts one or more sections. + * + * @param sections An index set that specifies the sections to insert. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertSections:(NSIndexSet *)sections ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Deletes one or more sections. + * + * @param sections An index set that specifies the sections to delete. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteSections:(NSIndexSet *)sections ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Reloads the specified sections. + * + * @param sections An index set that specifies the sections to reload. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadSections:(NSIndexSet *)sections ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Inserts items at the locations identified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects, each representing an item index and section index that together identify an item. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Deletes the items specified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to delete. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Reloads the specified items. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to reload. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Moves the item at a specified location to a destination location. + * + * @param indexPath The index path identifying the item to move. + * + * @param newIndexPath The index path that is the destination of the move for the item. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +/** + * Query the sized node at @c indexPath for its calculatedSize. + * + * @param indexPath The index path for the node of interest. + * + * This method is deprecated. Call @c calculatedSize on the node of interest instead. First deprecated in version 2.0. + */ +- (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Call -calculatedSize on the node of interest instead."); + +/** + * Similar to -visibleCells. + * + * @return an array containing the nodes being displayed on screen. + */ +- (NSArray<__kindof ASCellNode *> *)visibleNodes AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + +@end + +ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASCollectionDataSource.") +@protocol ASCollectionViewDataSource +@end + +ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASCollectionDelegate.") +@protocol ASCollectionViewDelegate +@end + +/** + * Defines methods that let you coordinate a `UICollectionViewFlowLayout` in combination with an `ASCollectionNode`. + */ +@protocol ASCollectionDelegateFlowLayout + +@optional + +/** + * Asks the delegate for the inset that should be applied to the given section. + * + * @see the same method in UICollectionViewDelegate. + */ +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; + +/** + * Asks the delegate for the size range that should be used to measure the header in the given flow layout section. + * + * @param collectionNode The sender. + * @param section The section. + * + * @return The size range for the header, or @c ASSizeRangeZero if there is no header in this section. + * + * If you want the header to completely determine its own size, return @c ASSizeRangeUnconstrained. + * + * @note Only the scrollable dimension of the returned size range will be used. In a vertical flow, + * only the height will be used. In a horizontal flow, only the width will be used. The other dimension + * will be constrained to fill the collection node. + * + * @discussion If you do not implement this method, ASDK will fall back to calling @c collectionView:layout:referenceSizeForHeaderInSection: + * and using that as the exact constrained size. If you don't implement that method, ASDK will read the @c headerReferenceSize from the layout. + */ +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForHeaderInSection:(NSInteger)section; + +/** + * Asks the delegate for the size range that should be used to measure the footer in the given flow layout section. + * + * @param collectionNode The sender. + * @param section The section. + * + * @return The size range for the footer, or @c ASSizeRangeZero if there is no footer in this section. + * + * If you want the footer to completely determine its own size, return @c ASSizeRangeUnconstrained. + * + * @note Only the scrollable dimension of the returned size range will be used. In a vertical flow, + * only the height will be used. In a horizontal flow, only the width will be used. The other dimension + * will be constrained to fill the collection node. + * + * @discussion If you do not implement this method, ASDK will fall back to calling @c collectionView:layout:referenceSizeForFooterInSection: + * and using that as the exact constrained size. If you don't implement that method, ASDK will read the @c footerReferenceSize from the layout. + */ +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForFooterInSection:(NSInteger)section; + +/** + * Asks the delegate for the size of the header in the specified section. + */ +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:sizeRangeForHeaderInSection: instead."); + +/** + * Asks the delegate for the size of the footer in the specified section. + */ +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:sizeRangeForFooterInSection: instead."); + +@end + +ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASCollectionDelegateFlowLayout.") +@protocol ASCollectionViewDelegateFlowLayout +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionView.mm b/submodules/AsyncDisplayKit/Source/ASCollectionView.mm new file mode 100644 index 0000000000..b619fd53a7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionView.mm @@ -0,0 +1,2521 @@ +// +// ASCollectionView.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +/** + * A macro to get self.collectionNode and assign it to a local variable, or return + * the given value if nil. + * + * Previously we would set ASCollectionNode's dataSource & delegate to nil + * during dealloc. However, our asyncDelegate & asyncDataSource must be set on the + * main thread, so if the node is deallocated off-main, we won't learn about the change + * until later on. Since our @c collectionNode parameter to delegate methods (e.g. + * collectionNode:didEndDisplayingItemWithNode:) is nonnull, it's important that we never + * unintentionally pass nil (this will crash in Swift, in production). So we can use + * this macro to ensure that our node is still alive before calling out to the user + * on its behalf. + */ +#define GET_COLLECTIONNODE_OR_RETURN(__var, __val) \ + ASCollectionNode *__var = self.collectionNode; \ + if (__var == nil) { \ + return __val; \ + } + +#define ASFlowLayoutDefault(layout, property, default) \ +({ \ + UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ + flowLayout ? flowLayout.property : default; \ +}) + +// ASCellLayoutMode is an NSUInteger-based NS_OPTIONS field. Be careful with BOOL handling on the +// 32-bit Objective-C runtime, and pattern after ASInterfaceStateIncludesVisible() & friends. +#define ASCellLayoutModeIncludes(layoutMode) ((_cellLayoutMode & layoutMode) == layoutMode) + +/// What, if any, invalidation should we perform during the next -layoutSubviews. +typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) { + /// Perform no invalidation. + ASCollectionViewInvalidationStyleNone, + /// Perform invalidation with animation (use an empty batch update). + ASCollectionViewInvalidationStyleWithoutAnimation, + /// Perform invalidation without animation (use -invalidateLayout). + ASCollectionViewInvalidationStyleWithAnimation, +}; + +static const NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimationNone; + +/// Used for all cells and supplementaries. UICV keys by supp-kind+reuseID so this is plenty. +static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; + +#pragma mark - +#pragma mark ASCollectionView. + +@interface ASCollectionView () { + ASCollectionViewProxy *_proxyDataSource; + ASCollectionViewProxy *_proxyDelegate; + + ASDataController *_dataController; + ASRangeController *_rangeController; + ASCollectionViewLayoutController *_layoutController; + id _defaultLayoutInspector; + __weak id _layoutInspector; + NSHashTable<_ASCollectionViewCell *> *_cellsForVisibilityUpdates; + NSHashTable *_cellsForLayoutUpdates; + id _layoutFacilitator; + CGFloat _leadingScreensForBatching; + + // When we update our data controller in response to an interactive move, + // we don't want to tell the collection view about the change (it knows!) + BOOL _updatingInResponseToInteractiveMove; + BOOL _inverted; + + NSUInteger _superBatchUpdateCount; + BOOL _isDeallocating; + + ASBatchContext *_batchContext; + + CGSize _lastBoundsSizeUsedForMeasuringNodes; + + NSMutableSet *_registeredSupplementaryKinds; + + // CountedSet because UIKit may display the same element in multiple cells e.g. during animations. + NSCountedSet *_visibleElements; + + CGPoint _deceleratingVelocity; + + BOOL _zeroContentInsets; + + ASCollectionViewInvalidationStyle _nextLayoutInvalidationStyle; + + /** + * If YES, the `UICollectionView` will reload its data on next layout pass so we should not forward any updates to it. + + * Rationale: + * In `reloadData`, a collection view invalidates its data and marks itself as needing reload, and waits until `layoutSubviews` to requery its data source. + * This can lead to data inconsistency problems. + * Say you have an empty collection view. You call `reloadData`, then immediately insert an item into your data source and call `insertItemsAtIndexPaths:[0,0]`. + * You will get an assertion failure saying `Invalid number of items in section 0. + * The number of items after the update (1) must be equal to the number of items before the update (1) plus or minus the items added and removed (1 added, 0 removed).` + * The collection view never queried your data source before the update to see that it actually had 0 items. + */ + BOOL _superIsPendingDataLoad; + + /** + * It's important that we always check for batch fetching at least once, but also + * that we do not check for batch fetching for empty updates (as that may cause an infinite + * loop of batch fetching, where the batch completes and performBatchUpdates: is called without + * actually making any changes.) So to handle the case where a collection is completely empty + * (0 sections) we always check at least once after each update (initial reload is the first update.) + */ + BOOL _hasEverCheckedForBatchFetchingDueToUpdate; + + /** + * Set during beginInteractiveMovementForItemAtIndexPath and UIGestureRecognizerStateEnded + * (or UIGestureRecognizerStateFailed, UIGestureRecognizerStateCancelled. + */ + BOOL _reordering; + + /** + * Counter used to keep track of nested batch updates. + */ + NSInteger _batchUpdateCount; + + /** + * Keep a strong reference to node till view is ready to release. + */ + ASCollectionNode *_keepalive_node; + + struct { + unsigned int scrollViewDidScroll:1; + unsigned int scrollViewWillBeginDragging:1; + unsigned int scrollViewDidEndDragging:1; + unsigned int scrollViewWillEndDragging:1; + unsigned int scrollViewDidEndDecelerating:1; + unsigned int collectionViewWillDisplayNodeForItem:1; + unsigned int collectionViewWillDisplayNodeForItemDeprecated:1; + unsigned int collectionViewDidEndDisplayingNodeForItem:1; + unsigned int collectionViewShouldSelectItem:1; + unsigned int collectionViewDidSelectItem:1; + unsigned int collectionViewShouldDeselectItem:1; + unsigned int collectionViewDidDeselectItem:1; + unsigned int collectionViewShouldHighlightItem:1; + unsigned int collectionViewDidHighlightItem:1; + unsigned int collectionViewDidUnhighlightItem:1; + unsigned int collectionViewShouldShowMenuForItem:1; + unsigned int collectionViewCanPerformActionForItem:1; + unsigned int collectionViewPerformActionForItem:1; + unsigned int collectionViewWillBeginBatchFetch:1; + unsigned int shouldBatchFetchForCollectionView:1; + unsigned int collectionNodeWillDisplayItem:1; + unsigned int collectionNodeDidEndDisplayingItem:1; + unsigned int collectionNodeShouldSelectItem:1; + unsigned int collectionNodeDidSelectItem:1; + unsigned int collectionNodeShouldDeselectItem:1; + unsigned int collectionNodeDidDeselectItem:1; + unsigned int collectionNodeShouldHighlightItem:1; + unsigned int collectionNodeDidHighlightItem:1; + unsigned int collectionNodeDidUnhighlightItem:1; + unsigned int collectionNodeShouldShowMenuForItem:1; + unsigned int collectionNodeCanPerformActionForItem:1; + unsigned int collectionNodePerformActionForItem:1; + unsigned int collectionNodeWillBeginBatchFetch:1; + unsigned int collectionNodeWillDisplaySupplementaryElement:1; + unsigned int collectionNodeDidEndDisplayingSupplementaryElement:1; + unsigned int shouldBatchFetchForCollectionNode:1; + + // Interop flags + unsigned int interop:1; + unsigned int interopWillDisplayCell:1; + unsigned int interopDidEndDisplayingCell:1; + unsigned int interopWillDisplaySupplementaryView:1; + unsigned int interopdidEndDisplayingSupplementaryView:1; + } _asyncDelegateFlags; + + struct { + unsigned int collectionViewNodeForItem:1; + unsigned int collectionViewNodeBlockForItem:1; + unsigned int collectionViewNodeForSupplementaryElement:1; + unsigned int numberOfSectionsInCollectionView:1; + unsigned int collectionViewNumberOfItemsInSection:1; + unsigned int collectionNodeNodeForItem:1; + unsigned int collectionNodeNodeBlockForItem:1; + unsigned int nodeModelForItem:1; + unsigned int collectionNodeNodeForSupplementaryElement:1; + unsigned int collectionNodeNodeBlockForSupplementaryElement:1; + unsigned int collectionNodeSupplementaryElementKindsInSection:1; + unsigned int numberOfSectionsInCollectionNode:1; + unsigned int collectionNodeNumberOfItemsInSection:1; + unsigned int collectionNodeContextForSection:1; + unsigned int collectionNodeCanMoveItem:1; + unsigned int collectionNodeMoveItem:1; + + // Whether this data source conforms to ASCollectionDataSourceInterop + unsigned int interop:1; + // Whether this interop data source returns YES from +dequeuesCellsForNodeBackedItems + unsigned int interopAlwaysDequeue:1; + // Whether this interop data source implements viewForSupplementaryElementOfKind: + unsigned int interopViewForSupplementaryElement:1; + unsigned int modelIdentifierMethods:1; // if both modelIdentifierForElementAtIndexPath and indexPathForElementWithModelIdentifier are implemented + } _asyncDataSourceFlags; + + struct { + unsigned int constrainedSizeForSupplementaryNodeOfKindAtIndexPath:1; + unsigned int supplementaryNodesOfKindInSection:1; + unsigned int didChangeCollectionViewDataSource:1; + unsigned int didChangeCollectionViewDelegate:1; + } _layoutInspectorFlags; + + BOOL _hasDataControllerLayoutDelegate; +} + +@end + +@implementation ASCollectionView +{ + __weak id _asyncDelegate; + __weak id _asyncDataSource; +} + +// Using _ASDisplayLayer ensures things like -layout are properly forwarded to ASCollectionNode. ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - +#pragma mark Lifecycle. + +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout +{ + return [self initWithFrame:CGRectZero collectionViewLayout:layout]; +} + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout +{ + return [self _initWithFrame:frame collectionViewLayout:layout layoutFacilitator:nil owningNode:nil eventLog:nil]; +} + +- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(id)layoutFacilitator owningNode:(ASCollectionNode *)owningNode eventLog:(ASEventLog *)eventLog +{ + if (!(self = [super initWithFrame:frame collectionViewLayout:layout])) + return nil; + + // Disable UICollectionView prefetching. Use super, because self disables this method. + // Experiments done by Instagram show that this option being YES (default) + // when unused causes a significant hit to scroll performance. + // https://github.com/Instagram/IGListKit/issues/318 + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + super.prefetchingEnabled = NO; + } + + _layoutController = [[ASCollectionViewLayoutController alloc] initWithCollectionView:self]; + + _rangeController = [[ASRangeController alloc] init]; + _rangeController.dataSource = self; + _rangeController.delegate = self; + _rangeController.layoutController = _layoutController; + + _dataController = [[ASDataController alloc] initWithDataSource:self node:owningNode eventLog:eventLog]; + _dataController.delegate = _rangeController; + + _batchContext = [[ASBatchContext alloc] init]; + + _leadingScreensForBatching = 2.0; + + _lastBoundsSizeUsedForMeasuringNodes = self.bounds.size; + + _layoutFacilitator = layoutFacilitator; + + _proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; + super.delegate = (id)_proxyDelegate; + + _proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; + super.dataSource = (id)_proxyDataSource; + + _registeredSupplementaryKinds = [[NSMutableSet alloc] init]; + _visibleElements = [[NSCountedSet alloc] init]; + + _cellsForVisibilityUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + _cellsForLayoutUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + self.backgroundColor = [UIColor whiteColor]; + + [self registerClass:[_ASCollectionViewCell class] forCellWithReuseIdentifier:kReuseIdentifier]; + + [self _configureCollectionViewLayout:layout]; + + return self; +} + +- (void)dealloc +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeCAssert(_batchUpdateCount == 0, @"ASCollectionView deallocated in the middle of a batch update."); + + // Sometimes the UIKit classes can call back to their delegate even during deallocation, due to animation completion blocks etc. + _isDeallocating = YES; + if (!ASActivateExperimentalFeature(ASExperimentalCollectionTeardown)) { + [self setAsyncDelegate:nil]; + [self setAsyncDataSource:nil]; + } + + // Data controller & range controller may own a ton of nodes, let's deallocate those off-main. + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); +} + +#pragma mark - +#pragma mark Overrides. + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +/** + * This method is not available to be called by the public i.e. + * it should only be called by UICollectionView itself. UICollectionView + * does this e.g. during the first layout pass, or if you call -numberOfSections + * before its content is loaded. + */ +- (void)reloadData +{ + [self _superReloadData:nil completion:nil]; + + // UICollectionView calls -reloadData during first layoutSubviews and when the data source changes. + // This fires off the first load of cell nodes. + if (_asyncDataSource != nil && !self.dataController.initialReloadDataHasBeenCalled) { + [self performBatchUpdates:^{ + [_changeSet reloadData]; + } completion:nil]; + } +} +#pragma clang diagnostic pop + +- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated +{ + if ([self validateIndexPath:indexPath]) { + [super scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; + } +} + +- (void)relayoutItems +{ + [_dataController relayoutAllNodesWithInvalidationBlock:^{ + [self.collectionViewLayout invalidateLayout]; + [self invalidateFlowLayoutDelegateMetrics]; + }]; +} + +- (BOOL)isProcessingUpdates +{ + return [_dataController isProcessingUpdates]; +} + +- (void)onDidFinishProcessingUpdates:(void (^)())completion +{ + [_dataController onDidFinishProcessingUpdates:completion]; +} + +- (void)waitUntilAllUpdatesAreCommitted +{ + ASDisplayNodeAssertMainThread(); + if (_batchUpdateCount > 0) { + // This assertion will be enabled soon. + // ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); + return; + } + + [_dataController waitUntilAllUpdatesAreProcessed]; +} + +- (BOOL)isSynchronized +{ + return [_dataController isSynchronized]; +} + +- (void)onDidFinishSynchronizing:(void (^)())completion +{ + [_dataController onDidFinishSynchronizing:completion]; +} + +- (void)setDataSource:(id)dataSource +{ + // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. We also allow this when we're doing interop. + ASDisplayNodeAssert(_asyncDelegateFlags.interop || dataSource == nil, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property."); +} + +- (void)setDelegate:(id)delegate +{ + // Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here. We also allow this when we're doing interop. + ASDisplayNodeAssert(_asyncDelegateFlags.interop || delegate == nil, @"ASCollectionView uses asyncDelegate, not UICollectionView's delegate property."); +} + +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy +{ + if (proxy == _proxyDelegate) { + [self setAsyncDelegate:nil]; + } else if (proxy == _proxyDataSource) { + [self setAsyncDataSource:nil]; + } +} + +- (id)asyncDataSource +{ + return _asyncDataSource; +} + +- (void)setAsyncDataSource:(id)asyncDataSource +{ + // Changing super.dataSource will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle + // the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource + // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong + // reference to the old dataSource in this case because calls to ASCollectionViewProxy will start failing and cause crashes. + NS_VALID_UNTIL_END_OF_SCOPE id oldDataSource = super.dataSource; + + if (asyncDataSource == nil) { + _asyncDataSource = nil; + _proxyDataSource = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; + _asyncDataSourceFlags = {}; + + } else { + _asyncDataSource = asyncDataSource; + _proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; + + _asyncDataSourceFlags.collectionViewNodeForItem = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeForItemAtIndexPath:)]; + _asyncDataSourceFlags.collectionViewNodeBlockForItem = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeBlockForItemAtIndexPath:)]; + _asyncDataSourceFlags.numberOfSectionsInCollectionView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]; + _asyncDataSourceFlags.collectionViewNumberOfItemsInSection = [_asyncDataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)]; + _asyncDataSourceFlags.collectionViewNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeForSupplementaryElementOfKind:atIndexPath:)]; + + _asyncDataSourceFlags.collectionNodeNodeForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForItemAtIndexPath:)]; + _asyncDataSourceFlags.collectionNodeNodeBlockForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForItemAtIndexPath:)]; + _asyncDataSourceFlags.numberOfSectionsInCollectionNode = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionNode:)]; + _asyncDataSourceFlags.collectionNodeNumberOfItemsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:numberOfItemsInSection:)]; + _asyncDataSourceFlags.collectionNodeContextForSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:contextForSection:)]; + _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForSupplementaryElementOfKind:atIndexPath:)]; + _asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForSupplementaryElementOfKind:atIndexPath:)]; + _asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)]; + _asyncDataSourceFlags.nodeModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeModelForItemAtIndexPath:)]; + _asyncDataSourceFlags.collectionNodeCanMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:canMoveItemWithNode:)]; + _asyncDataSourceFlags.collectionNodeMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:moveItemAtIndexPath:toIndexPath:)]; + + _asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)]; + if (_asyncDataSourceFlags.interop) { + id interopDataSource = (id)_asyncDataSource; + _asyncDataSourceFlags.interopAlwaysDequeue = [[interopDataSource class] respondsToSelector:@selector(dequeuesCellsForNodeBackedItems)] && [[interopDataSource class] dequeuesCellsForNodeBackedItems]; + _asyncDataSourceFlags.interopViewForSupplementaryElement = [interopDataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)]; + } + + _asyncDataSourceFlags.modelIdentifierMethods = [_asyncDataSource respondsToSelector:@selector(modelIdentifierForElementAtIndexPath:inNode:)] && [_asyncDataSource respondsToSelector:@selector(indexPathForElementWithModelIdentifier:inNode:)]; + + + ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection || _asyncDataSourceFlags.collectionViewNumberOfItemsInSection, @"Data source must implement collectionNode:numberOfItemsInSection:"); + ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNodeBlockForItem + || _asyncDataSourceFlags.collectionNodeNodeForItem + || _asyncDataSourceFlags.collectionViewNodeBlockForItem + || _asyncDataSourceFlags.collectionViewNodeForItem, @"Data source must implement collectionNode:nodeBlockForItemAtIndexPath: or collectionNode:nodeForItemAtIndexPath:"); + } + + _dataController.validationErrorSource = asyncDataSource; + super.dataSource = (id)_proxyDataSource; + + //Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one. + id layoutInspector = self.layoutInspector; + if (_layoutInspectorFlags.didChangeCollectionViewDataSource) { + [layoutInspector didChangeCollectionViewDataSource:asyncDataSource]; + } + [self _asyncDelegateOrDataSourceDidChange]; +} + +- (id)asyncDelegate +{ + return _asyncDelegate; +} + +- (void)setAsyncDelegate:(id)asyncDelegate +{ + // Changing super.delegate will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle + // the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate + // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong + // reference to the old delegate in this case because calls to ASCollectionViewProxy will start failing and cause crashes. + NS_VALID_UNTIL_END_OF_SCOPE id oldDelegate = super.delegate; + + if (asyncDelegate == nil) { + _asyncDelegate = nil; + _proxyDelegate = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; + _asyncDelegateFlags = {}; + } else { + _asyncDelegate = asyncDelegate; + _proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; + + _asyncDelegateFlags.scrollViewDidScroll = [_asyncDelegate respondsToSelector:@selector(scrollViewDidScroll:)]; + _asyncDelegateFlags.scrollViewWillEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]; + _asyncDelegateFlags.scrollViewDidEndDecelerating = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]; + _asyncDelegateFlags.scrollViewWillBeginDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]; + _asyncDelegateFlags.scrollViewDidEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]; + _asyncDelegateFlags.collectionViewWillDisplayNodeForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNode:forItemAtIndexPath:)]; + if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem == NO) { + _asyncDelegateFlags.collectionViewWillDisplayNodeForItemDeprecated = [_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]; + } + _asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNode:forItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)]; + _asyncDelegateFlags.shouldBatchFetchForCollectionView = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]; + _asyncDelegateFlags.collectionViewShouldSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldSelectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewDidSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewShouldDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldDeselectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewDidDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didDeselectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewShouldHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldHighlightItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewDidHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didHighlightItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewDidUnhighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didUnhighlightItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldShowMenuForItemAtIndexPath:)]; + _asyncDelegateFlags.collectionViewCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:canPerformAction:forItemAtIndexPath:withSender:)]; + _asyncDelegateFlags.collectionViewPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:performAction:forItemAtIndexPath:withSender:)]; + _asyncDelegateFlags.collectionNodeWillDisplayItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayItemWithNode:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingItemWithNode:)]; + _asyncDelegateFlags.collectionNodeWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(collectionNode:willBeginBatchFetchWithContext:)]; + _asyncDelegateFlags.shouldBatchFetchForCollectionNode = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionNode:)]; + _asyncDelegateFlags.collectionNodeShouldSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldSelectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeDidSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didSelectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeShouldDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldDeselectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeDidDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didDeselectItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeShouldHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldHighlightItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeDidHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didHighlightItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeDidUnhighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didUnhighlightItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldShowMenuForItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:canPerformAction:forItemAtIndexPath:sender:)]; + _asyncDelegateFlags.collectionNodePerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:performAction:forItemAtIndexPath:sender:)]; + _asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplaySupplementaryElementWithNode:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingSupplementaryElementWithNode:)]; + _asyncDelegateFlags.interop = [_asyncDelegate conformsToProtocol:@protocol(ASCollectionDelegateInterop)]; + if (_asyncDelegateFlags.interop) { + id interopDelegate = (id)_asyncDelegate; + _asyncDelegateFlags.interopWillDisplayCell = [interopDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]; + _asyncDelegateFlags.interopDidEndDisplayingCell = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]; + _asyncDelegateFlags.interopWillDisplaySupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:)]; + _asyncDelegateFlags.interopdidEndDisplayingSupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)]; + } + } + + super.delegate = (id)_proxyDelegate; + + //Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one. + id layoutInspector = self.layoutInspector; + if (_layoutInspectorFlags.didChangeCollectionViewDelegate) { + [layoutInspector didChangeCollectionViewDelegate:asyncDelegate]; + } + [self _asyncDelegateOrDataSourceDidChange]; +} + +- (void)_asyncDelegateOrDataSourceDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (_asyncDataSource == nil && _asyncDelegate == nil && !ASActivateExperimentalFeature(ASExperimentalSkipClearData)) { + [_dataController clearData]; + } +} + +- (void)setCollectionViewLayout:(nonnull UICollectionViewLayout *)collectionViewLayout +{ + ASDisplayNodeAssertMainThread(); + [super setCollectionViewLayout:collectionViewLayout]; + + [self _configureCollectionViewLayout:collectionViewLayout]; + + // Trigger recreation of layout inspector with new collection view layout + if (_layoutInspector != nil) { + _layoutInspector = nil; + [self layoutInspector]; + } +} + +- (id)layoutInspector +{ + if (_layoutInspector == nil) { + UICollectionViewLayout *layout = self.collectionViewLayout; + if (layout == nil) { + // Layout hasn't been set yet, we're still init'ing + return nil; + } + + _defaultLayoutInspector = [layout asdk_layoutInspector]; + ASDisplayNodeAssertNotNil(_defaultLayoutInspector, @"You must not return nil from -asdk_layoutInspector. Return [super asdk_layoutInspector] if you have to! Layout: %@", layout); + + // Explicitly call the setter to wire up the _layoutInspectorFlags + self.layoutInspector = _defaultLayoutInspector; + } + + return _layoutInspector; +} + +- (void)setLayoutInspector:(id)layoutInspector +{ + _layoutInspector = layoutInspector; + + _layoutInspectorFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_layoutInspector respondsToSelector:@selector(collectionView:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; + _layoutInspectorFlags.supplementaryNodesOfKindInSection = [_layoutInspector respondsToSelector:@selector(collectionView:supplementaryNodesOfKind:inSection:)]; + _layoutInspectorFlags.didChangeCollectionViewDataSource = [_layoutInspector respondsToSelector:@selector(didChangeCollectionViewDataSource:)]; + _layoutInspectorFlags.didChangeCollectionViewDelegate = [_layoutInspector respondsToSelector:@selector(didChangeCollectionViewDelegate:)]; + + if (_layoutInspectorFlags.didChangeCollectionViewDataSource) { + [_layoutInspector didChangeCollectionViewDataSource:self.asyncDataSource]; + } + if (_layoutInspectorFlags.didChangeCollectionViewDelegate) { + [_layoutInspector didChangeCollectionViewDelegate:self.asyncDelegate]; + } +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + [_rangeController setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [_rangeController tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + [_rangeController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + return [_rangeController tuningParametersForRangeMode:rangeMode rangeType:rangeType]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)setZeroContentInsets:(BOOL)zeroContentInsets +{ + _zeroContentInsets = zeroContentInsets; +} + +- (BOOL)zeroContentInsets +{ + return _zeroContentInsets; +} +#pragma clang diagnostic pop + +/// Uses latest size range from data source and -layoutThatFits:. +- (CGSize)sizeForElement:(ASCollectionElement *)element +{ + ASDisplayNodeAssertMainThread(); + if (element == nil) { + return CGSizeZero; + } + + ASCellNode *node = element.node; + ASDisplayNodeAssertNotNil(node, @"Node must not be nil!"); + + BOOL useUIKitCell = node.shouldUseUIKitCell; + if (useUIKitCell) { + ASWrapperCellNode *wrapperNode = (ASWrapperCellNode *)node; + if (wrapperNode.sizeForItemBlock) { + return wrapperNode.sizeForItemBlock(wrapperNode, element.constrainedSize.max); + } else { + // In this case, it is possible the model indexPath for this element will be nil. Attempt to convert it, + // and call out to the delegate directly. If it has been deleted from the model, the size returned will be the layout's default. + NSIndexPath *indexPath = [_dataController.visibleMap indexPathForElement:element]; + return [self _sizeForUIKitCellWithKind:element.supplementaryElementKind atIndexPath:indexPath]; + } + } else { + return [node layoutThatFits:element.constrainedSize].size; + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + + ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; + return [self sizeForElement:e]; +} +#pragma clang diagnostic pop + +- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [_dataController.visibleMap elementForItemAtIndexPath:indexPath].node; +} + +- (NSIndexPath *)convertIndexPathFromCollectionNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait +{ + if (indexPath == nil) { + return nil; + } + + NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; + if (viewIndexPath == nil && wait) { + [self waitUntilAllUpdatesAreCommitted]; + return [self convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:NO]; + } + return viewIndexPath; +} + +/** + * Asserts that the index path is a valid view-index-path, and returns it if so, nil otherwise. + */ +- (nullable NSIndexPath *)validateIndexPath:(nullable NSIndexPath *)indexPath +{ + if (indexPath == nil) { + return nil; + } + + NSInteger section = indexPath.section; + if (section >= self.numberOfSections) { + ASDisplayNodeFailAssert(@"Collection view index path has invalid section %lu, section count = %lu", (unsigned long)section, (unsigned long)self.numberOfSections); + return nil; + } + + NSInteger item = indexPath.item; + // item == NSNotFound means e.g. "scroll to this section" and is acceptable + if (item != NSNotFound && item >= [self numberOfItemsInSection:section]) { + ASDisplayNodeFailAssert(@"Collection view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfItemsInSection:section]); + return nil; + } + + return indexPath; +} + +- (NSIndexPath *)convertIndexPathToCollectionNode:(NSIndexPath *)indexPath +{ + if ([self validateIndexPath:indexPath] == nil) { + return nil; + } + + return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; +} + +- (NSArray *)convertIndexPathsToCollectionNode:(NSArray *)indexPaths +{ + return ASArrayByFlatMapping(indexPaths, NSIndexPath *viewIndexPath, [self convertIndexPathToCollectionNode:viewIndexPath]); +} + +- (ASCellNode *)supplementaryNodeForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath +{ + return [_dataController.visibleMap supplementaryElementOfKind:elementKind atIndexPath:indexPath].node; +} + +- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode +{ + return [_dataController.visibleMap indexPathForElement:cellNode.collectionElement]; +} + +- (NSArray *)visibleNodes +{ + NSArray *indexPaths = [self indexPathsForVisibleItems]; + NSMutableArray *visibleNodes = [[NSMutableArray alloc] init]; + + for (NSIndexPath *indexPath in indexPaths) { + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + if (node) { + // It is possible for UICollectionView to return indexPaths before the node is completed. + [visibleNodes addObject:node]; + } + } + + return visibleNodes; +} + +- (void)invalidateFlowLayoutDelegateMetrics +{ + // Subclass hook +} + +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + NSIndexPath *convertedPath = [self convertIndexPathToCollectionNode:indexPath]; + if (convertedPath == nil) { + return nil; + } else { + return [_asyncDataSource modelIdentifierForElementAtIndexPath:convertedPath inNode:collectionNode]; + } + } else { + return nil; + } +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + return [_asyncDataSource indexPathForElementWithModelIdentifier:identifier inNode:collectionNode]; + } else { + return nil; + } +} + +#pragma mark Internal + +- (void)_configureCollectionViewLayout:(nonnull UICollectionViewLayout *)layout +{ + _hasDataControllerLayoutDelegate = [layout conformsToProtocol:@protocol(ASDataControllerLayoutDelegate)]; + if (_hasDataControllerLayoutDelegate) { + _dataController.layoutDelegate = (id)layout; + } +} + +/** + This method is called only for UIKit Passthrough cells - either regular Items or Supplementary elements. + It checks if the delegate implements the UICollectionViewFlowLayout methods that provide sizes, and if not, + uses the default values set on the flow layout. If a flow layout is not in use, UICollectionView Passthrough + cells must be sized by logic in the Layout object, and Texture does not participate in these paths. +*/ +- (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + CGSize size = CGSizeZero; + UICollectionViewLayout *l = self.collectionViewLayout; + + if (kind == nil) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interop, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); + if (indexPath && [_asyncDelegate respondsToSelector:sizeForItem]) { + size = [(id)_asyncDelegate collectionView:self layout:l sizeForItemAtIndexPath:indexPath]; + } else { + size = ASFlowLayoutDefault(l, itemSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); + if (indexPath && [_asyncDelegate respondsToSelector:sizeForHeader]) { + size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForHeaderInSection:indexPath.section]; + } else { + size = ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); + if (indexPath && [_asyncDelegate respondsToSelector:sizeForFooter]) { + size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForFooterInSection:indexPath.section]; + } else { + size = ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); + } + } + + return size; +} + +- (void)_superReloadData:(void(^)())updates completion:(void(^)(BOOL finished))completion +{ + if (updates) { + updates(); + } + [super reloadData]; + if (completion) { + completion(YES); + } +} + +/** + * Performing nested batch updates with super (e.g. resizing a cell node & updating collection view + * during same frame) can cause super to throw data integrity exceptions because it checks the data + * source counts before the update is complete. + * + * Always call [self _superPerform:] rather than [super performBatch:] so that we can keep our + * `superPerformingBatchUpdates` flag updated. +*/ +- (void)_superPerformBatchUpdates:(void(^)())updates completion:(void(^)(BOOL finished))completion +{ + ASDisplayNodeAssertMainThread(); + + _superBatchUpdateCount++; + [super performBatchUpdates:updates completion:completion]; + _superBatchUpdateCount--; +} + +#pragma mark Assertions. + +- (ASDataController *)dataController +{ + return _dataController; +} + +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + // _changeSet must be available during batch update + ASDisplayNodeAssertTrue((_batchUpdateCount > 0) == (_changeSet != nil)); + + if (_batchUpdateCount == 0) { + _changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[_dataController itemCountsFromDataSource]]; + _changeSet.rootActivity = as_activity_create("Perform async collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT); + _changeSet.submitActivity = as_activity_create("Submit changes for collection update", _changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT); + } + _batchUpdateCount++; +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssertNotNil(_changeSet, @"_changeSet must be available when batch update ends"); + + _batchUpdateCount--; + // Prevent calling endUpdatesAnimated:completion: in an unbalanced way + NSAssert(_batchUpdateCount >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call"); + + [_changeSet addCompletionHandler:completion]; + + if (_batchUpdateCount == 0) { + _ASHierarchyChangeSet *changeSet = _changeSet; + + // Nil out _changeSet before forwarding to _dataController to allow the change set to cause subsequent batch updates on the same run loop + _changeSet = nil; + changeSet.animated = animated; + [_dataController updateWithChangeSet:changeSet]; + } +} + +- (void)performBatchAnimated:(BOOL)animated updates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + as_activity_scope(_changeSet.rootActivity); + { + // Only include client code in the submit activity, the rest just lives in the root activity. + as_activity_scope(_changeSet.submitActivity); + if (updates) { + updates(); + } + } + [self endUpdatesAnimated:animated completion:completion]; +} + +- (void)performBatchUpdates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion +{ + // We capture the current state of whether animations are enabled if they don't provide us with one. + [self performBatchAnimated:[UIView areAnimationsEnabled] updates:updates completion:completion]; +} + +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind +{ + ASDisplayNodeAssert(elementKind != nil, @"A kind is needed for supplementary node registration"); + [_registeredSupplementaryKinds addObject:elementKind]; + [self registerClass:[_ASCollectionReusableView class] forSupplementaryViewOfKind:elementKind withReuseIdentifier:kReuseIdentifier]; +} + +- (void)insertSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } + [self performBatchUpdates:^{ + [_changeSet insertSections:sections animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (void)deleteSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } + [self performBatchUpdates:^{ + [_changeSet deleteSections:sections animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (void)reloadSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } + [self performBatchUpdates:^{ + [_changeSet reloadSections:sections animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + ASDisplayNodeAssertMainThread(); + [self performBatchUpdates:^{ + [_changeSet moveSection:section toSection:newSection animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (id)contextForSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + return [_dataController.visibleMap contextForSection:section]; +} + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } + [self performBatchUpdates:^{ + [_changeSet insertItems:indexPaths animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } + [self performBatchUpdates:^{ + [_changeSet deleteItems:indexPaths animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } + [self performBatchUpdates:^{ + [_changeSet reloadItems:indexPaths animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; +} + +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +{ + ASDisplayNodeAssertMainThread(); + if (!_reordering) { + [self performBatchUpdates:^{ + [_changeSet moveItemAtIndexPath:indexPath toIndexPath:newIndexPath animationOptions:kASCollectionViewAnimationNone]; + } completion:nil]; + } else { + [super moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; + } +} + +- (BOOL)beginInteractiveMovementForItemAtIndexPath:(NSIndexPath *)indexPath { + BOOL success = [super beginInteractiveMovementForItemAtIndexPath:indexPath]; + _reordering = success; + return success; +} + +- (void)endInteractiveMovement { + _reordering = NO; + [super endInteractiveMovement]; +} + +- (void)cancelInteractiveMovement { + _reordering = NO; + [super cancelInteractiveMovement]; +} + +#pragma mark - +#pragma mark Intercepted selectors. + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + if (_superIsPendingDataLoad) { + [_rangeController setNeedsUpdate]; + [self _scheduleCheckForBatchFetchingForNumberOfChanges:1]; + _superIsPendingDataLoad = NO; + } + return _dataController.visibleMap.numberOfSections; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return [_dataController.visibleMap numberOfItemsInSection:section]; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout + sizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; + return e ? [self sizeForElement:e] : ASFlowLayoutDefault(layout, itemSize, CGSizeZero); +} + +- (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l + referenceSizeForHeaderInSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + ASElementMap *map = _dataController.visibleMap; + ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionHeader + atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); +} + +- (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l + referenceSizeForFooterInSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + ASElementMap *map = _dataController.visibleMap; + ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionFooter + atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); +} + +// For the methods that call delegateIndexPathForSection:withSelector:, translate the section from +// visibleMap to pendingMap. If the section no longer exists, or the delegate doesn't implement +// the selector, we will return NSNotFound (and then use the ASFlowLayoutDefault). +- (NSInteger)delegateIndexForSection:(NSInteger)section withSelector:(SEL)selector +{ + if ([_asyncDelegate respondsToSelector:selector]) { + return [_dataController.pendingMap convertSection:section fromMap:_dataController.visibleMap]; + } else { + return NSNotFound; + } +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l + insetForSectionAtIndex:(NSInteger)section +{ + section = [self delegateIndexForSection:section withSelector:_cmd]; + if (section != NSNotFound) { + return [(id)_asyncDelegate collectionView:cv layout:l insetForSectionAtIndex:section]; + } + return ASFlowLayoutDefault(l, sectionInset, UIEdgeInsetsZero); +} + +- (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l + minimumInteritemSpacingForSectionAtIndex:(NSInteger)section +{ + section = [self delegateIndexForSection:section withSelector:_cmd]; + if (section != NSNotFound) { + return [(id)_asyncDelegate collectionView:cv layout:l + minimumInteritemSpacingForSectionAtIndex:section]; + } + return ASFlowLayoutDefault(l, minimumInteritemSpacing, 10.0); // Default is documented as 10.0 +} + +- (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l + minimumLineSpacingForSectionAtIndex:(NSInteger)section +{ + section = [self delegateIndexForSection:section withSelector:_cmd]; + if (section != NSNotFound) { + return [(id)_asyncDelegate collectionView:cv layout:l + minimumLineSpacingForSectionAtIndex:section]; + } + return ASFlowLayoutDefault(l, minimumLineSpacing, 10.0); // Default is documented as 10.0 +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if ([_registeredSupplementaryKinds containsObject:kind] == NO) { + [self registerSupplementaryNodeOfKind:kind]; + } + + UICollectionReusableView *view = nil; + ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath]; + ASCellNode *node = element.node; + ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil); + BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interopViewForSupplementaryElement && wrapperNode); + + if (wrapperNode.viewForSupplementaryBlock) { + view = wrapperNode.viewForSupplementaryBlock(wrapperNode); + } else if (shouldDequeueExternally) { + // This codepath is used for both IGListKit mode, and app-level UICollectionView interop. + view = [(id)_asyncDataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + } else { + ASDisplayNodeAssert(node != nil, @"Supplementary node should exist. Kind = %@, indexPath = %@, collectionDataSource = %@", kind, indexPath, self); + view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; + } + + if (_ASCollectionReusableView *reusableView = ASDynamicCastStrict(view, _ASCollectionReusableView)) { + reusableView.element = element; + } + + if (node) { + [_rangeController configureContentView:view forCellNode:node]; + } + + return view; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + UICollectionViewCell *cell = nil; + ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; + ASCellNode *node = element.node; + ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil); + BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interop && wrapperNode); + + if (wrapperNode.cellForItemBlock) { + cell = wrapperNode.cellForItemBlock(wrapperNode); + } else if (shouldDequeueExternally) { + cell = [(id)_asyncDataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; + } else { + cell = [self dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; + } + + ASDisplayNodeAssert(element != nil, @"Element should exist. indexPath = %@, collectionDataSource = %@", indexPath, self); + ASDisplayNodeAssert(cell != nil, @"UICollectionViewCell must not be nil. indexPath = %@, collectionDataSource = %@", indexPath, self); + + if (_ASCollectionViewCell *asCell = ASDynamicCastStrict(cell, _ASCollectionViewCell)) { + asCell.element = element; + [_rangeController configureContentView:cell.contentView forCellNode:node]; + } + + return cell; +} + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.interopWillDisplayCell) { + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; + } + } + + _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); + if (cell == nil) { + [_rangeController setNeedsUpdate]; + return; + } + + ASCollectionElement *element = cell.element; + if (element) { + ASDisplayNodeAssertTrue([_dataController.visibleMap elementForItemAtIndexPath:indexPath] == element); + [_visibleElements addObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplayCell: %@, %@, %@", rawCell, self, indexPath); + return; + } + + ASCellNode *cellNode = element.node; + cellNode.scrollView = collectionView; + + // Update the selected background view in collectionView:willDisplayCell:forItemAtIndexPath: otherwise it could be too + // early e.g. if the selectedBackgroundView was set in didLoad() + cell.selectedBackgroundView = cellNode.selectedBackgroundView; + cell.backgroundView = cellNode.backgroundView; + + // Under iOS 10+, cells may be removed/re-added to the collection view without + // receiving prepareForReuse/applyLayoutAttributes, as an optimization for e.g. + // if the user is scrolling back and forth across a small set of items. + // In this case, we have to fetch the layout attributes manually. + // This may be possible under iOS < 10 but it has not been observed yet. + if (cell.layoutAttributes == nil) { + cell.layoutAttributes = [collectionView layoutAttributesForItemAtIndexPath:indexPath]; + } + + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); + + if (_asyncDelegateFlags.collectionNodeWillDisplayItem && self.collectionNode != nil) { + [_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode]; + } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self willDisplayNode:cellNode forItemAtIndexPath:indexPath]; + } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItemDeprecated) { + [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; + } +#pragma clang diagnostic pop + + [_rangeController setNeedsUpdate]; + + if ([cell consumesCellNodeVisibilityEvents]) { + [_cellsForVisibilityUpdates addObject:cell]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.interopDidEndDisplayingCell) { + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; + } + } + + _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); + if (cell == nil) { + [_rangeController setNeedsUpdate]; + return; + } + + // Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. + ASCollectionElement *element = cell.element; + if (element) { + [_visibleElements removeObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingCell: %@, %@, %@", rawCell, self, indexPath); + return; + } + + ASCellNode *cellNode = element.node; + + if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { + if (ASCollectionNode *collectionNode = self.collectionNode) { + [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; + } + } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + + [_rangeController setNeedsUpdate]; + + [_cellsForVisibilityUpdates removeObject:cell]; + + cellNode.scrollView = nil; + cell.layoutAttributes = nil; +} + +- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.interopWillDisplaySupplementaryView) { + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:indexPath]; + } + } + + _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); + if (view == nil) { + return; + } + + ASCollectionElement *element = view.element; + if (element) { + ASDisplayNodeAssertTrue([_dataController.visibleMap supplementaryElementOfKind:elementKind atIndexPath:indexPath] == view.element); + [_visibleElements addObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplaySupplementaryView: %@, %@, %@", rawView, self, indexPath); + return; + } + + // Under iOS 10+, cells may be removed/re-added to the collection view without + // receiving prepareForReuse/applyLayoutAttributes, as an optimization for e.g. + // if the user is scrolling back and forth across a small set of items. + // In this case, we have to fetch the layout attributes manually. + // This may be possible under iOS < 10 but it has not been observed yet. + if (view.layoutAttributes == nil) { + view.layoutAttributes = [collectionView layoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath]; + } + + if (_asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + ASCellNode *node = element.node; + ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); + [_asyncDelegate collectionNode:collectionNode willDisplaySupplementaryElementWithNode:node]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)rawView forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.interopdidEndDisplayingSupplementaryView) { + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:indexPath]; + } + } + + _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); + if (view == nil) { + return; + } + + // Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. + ASCollectionElement *element = view.element; + if (element) { + [_visibleElements removeObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingSupplementaryView: %@, %@, %@", rawView, self, indexPath); + return; + } + + if (_asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + ASCellNode *node = element.node; + ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); + [_asyncDelegate collectionNode:collectionNode didEndDisplayingSupplementaryElementWithNode:node]; + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeShouldSelectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate collectionNode:collectionNode shouldSelectItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewShouldSelectItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate collectionView:self shouldSelectItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return YES; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeDidSelectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate collectionNode:collectionNode didSelectItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewDidSelectItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self didSelectItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeShouldDeselectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate collectionNode:collectionNode shouldDeselectItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewShouldDeselectItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate collectionView:self shouldDeselectItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return YES; +} + +- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeDidDeselectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate collectionNode:collectionNode didDeselectItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewDidDeselectItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self didDeselectItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeShouldHighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate collectionNode:collectionNode shouldHighlightItemAtIndexPath:indexPath]; + } else { + return YES; + } + } else if (_asyncDelegateFlags.collectionViewShouldHighlightItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate collectionView:self shouldHighlightItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return YES; +} + +- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeDidHighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate collectionNode:collectionNode didHighlightItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewDidHighlightItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self didHighlightItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeDidUnhighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate collectionNode:collectionNode didUnhighlightItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewDidUnhighlightItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self didUnhighlightItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.collectionNodeShouldShowMenuForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate collectionNode:collectionNode shouldShowMenuForItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionViewShouldShowMenuForItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate collectionView:self shouldShowMenuForItemAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return NO; +} + +- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender +{ + if (_asyncDelegateFlags.collectionNodeCanPerformActionForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate collectionNode:collectionNode canPerformAction:action forItemAtIndexPath:indexPath sender:sender]; + } + } else if (_asyncDelegateFlags.collectionViewCanPerformActionForItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate collectionView:self canPerformAction:action forItemAtIndexPath:indexPath withSender:sender]; +#pragma clang diagnostic pop + } + return NO; +} + +- (void)collectionView:(UICollectionView *)collectionView performAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender +{ + if (_asyncDelegateFlags.collectionNodePerformActionForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + indexPath = [self convertIndexPathToCollectionNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate collectionNode:collectionNode performAction:action forItemAtIndexPath:indexPath sender:sender]; + } + } else if (_asyncDelegateFlags.collectionViewPerformActionForItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self performAction:action forItemAtIndexPath:indexPath withSender:sender]; +#pragma clang diagnostic pop + } +} + +- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath +{ + // Mimic UIKit's gating logic. + // If the data source doesn't support moving, then all bets are off. + if (!_asyncDataSourceFlags.collectionNodeMoveItem) { + return NO; + } + + // Currently we do not support interactive moves when using async layout. The reason is, we do not have a mechanism + // to propagate the "presentation data" element map (containing the speculative in-progress moves) to the layout delegate, + // and this can cause exceptions to be thrown from UICV. For example, if you drag an item out of a section, + // the element map will still contain N items in that section, even though there's only N-1 shown, and UICV will + // throw an exception that you specified an element that doesn't exist. + // + // In iOS >= 11, this is made much easier by the UIDataSourceTranslating API. In previous versions of iOS our best bet + // would be to capture the invalidation contexts that are sent during interactive moves and make our own data source translator. + if ([self.collectionViewLayout isKindOfClass:[ASCollectionLayout class]]) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + as_log_debug(ASCollectionLog(), "Collection node item interactive movement is not supported when using a layout delegate. This message will only be logged once. Node: %@", ASObjectDescriptionMakeTiny(self)); + }); + return NO; + } + + // If the data source implements canMoveItem, let them decide. + if (_asyncDataSourceFlags.collectionNodeCanMoveItem) { + if (ASCellNode *cellNode = [self nodeForItemAtIndexPath:indexPath]) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + return [_asyncDataSource collectionNode:collectionNode canMoveItemWithNode:cellNode]; + } + } + + // Otherwise allow the move for all items. + return YES; +} + +- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath +{ + ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeMoveItem, @"Should not allow interactive collection item movement if data source does not support it."); + + // Inform the data source first, in case they call nodeForItemAtIndexPath:. + // We want to make sure we return them the node for the item they have in mind. + if (ASCollectionNode *collectionNode = self.collectionNode) { + [_asyncDataSource collectionNode:collectionNode moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + } + + // Now we update our data controller's store. + // Get up to date + [self waitUntilAllUpdatesAreCommitted]; + // Set our flag to suppress informing super about the change. + ASDisplayNodeAssertFalse(_updatingInResponseToInteractiveMove); + _updatingInResponseToInteractiveMove = YES; + // Submit the move + [self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + // Wait for it to finish – should be fast! + [self waitUntilAllUpdatesAreCommitted]; + // Clear the flag + _updatingInResponseToInteractiveMove = NO; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; + if (ASInterfaceStateIncludesVisible(interfaceState)) { + [self _checkForBatchFetching]; + } + for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { + // _cellsForVisibilityUpdates only includes cells for ASCellNode subclasses with overrides of the visibility method. + [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView]; + } + if (_asyncDelegateFlags.scrollViewDidScroll) { + [_asyncDelegate scrollViewDidScroll:scrollView]; + } +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + CGPoint contentOffset = scrollView.contentOffset; + _deceleratingVelocity = CGPointMake( + contentOffset.x - ((targetContentOffset != NULL) ? targetContentOffset->x : 0), + contentOffset.y - ((targetContentOffset != NULL) ? targetContentOffset->y : 0) + ); + + if (targetContentOffset != NULL) { + ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); + [self _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset velocity:velocity]; + } + + if (_asyncDelegateFlags.scrollViewWillEndDragging) { + [_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:(targetContentOffset ? : &contentOffset)]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView +{ + _deceleratingVelocity = CGPointZero; + + if (_asyncDelegateFlags.scrollViewDidEndDecelerating) { + [_asyncDelegate scrollViewDidEndDecelerating:scrollView]; + } +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + // If a scroll happens the current range mode needs to go to full + _rangeController.contentHasBeenScrolled = YES; + [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; + + for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { + [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging inScrollView:scrollView]; + } + if (_asyncDelegateFlags.scrollViewWillBeginDragging) { + [_asyncDelegate scrollViewWillBeginDragging:scrollView]; + } +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate +{ + for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { + [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging inScrollView:scrollView]; + } + if (_asyncDelegateFlags.scrollViewDidEndDragging) { + [_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; + } +} + +#pragma mark - Scroll Direction. + +- (BOOL)inverted +{ + return _inverted; +} + +- (void)setInverted:(BOOL)inverted +{ + _inverted = inverted; +} + +- (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching +{ + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + // Push this to the next runloop to be sure the scroll view has the right content size + dispatch_async(dispatch_get_main_queue(), ^{ + [self _checkForBatchFetching]; + }); + } +} + +- (CGFloat)leadingScreensForBatching +{ + return _leadingScreensForBatching; +} + +- (ASScrollDirection)scrollDirection +{ + CGPoint scrollVelocity; + if (self.isTracking) { + scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; + } else { + scrollVelocity = _deceleratingVelocity; + } + + ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity]; + return ASScrollDirectionApplyTransform(scrollDirection, self.transform); +} + +- (ASScrollDirection)_scrollDirectionForVelocity:(CGPoint)scrollVelocity +{ + ASScrollDirection direction = ASScrollDirectionNone; + ASScrollDirection scrollableDirections = [self scrollableDirections]; + + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { // Can scroll horizontally. + if (scrollVelocity.x < 0.0) { + direction |= ASScrollDirectionRight; + } else if (scrollVelocity.x > 0.0) { + direction |= ASScrollDirectionLeft; + } + } + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { // Can scroll vertically. + if (scrollVelocity.y < 0.0) { + direction |= ASScrollDirectionDown; + } else if (scrollVelocity.y > 0.0) { + direction |= ASScrollDirectionUp; + } + } + + return direction; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertNotNil(self.layoutInspector, @"Layout inspector should be assigned."); + return [self.layoutInspector scrollableDirections]; +} + +- (void)layoutSubviews +{ + if (_cellsForLayoutUpdates.count > 0) { + NSArray *nodes = [_cellsForLayoutUpdates allObjects]; + [_cellsForLayoutUpdates removeAllObjects]; + + NSMutableArray *nodesSizeChanged = [[NSMutableArray alloc] init]; + + [_dataController relayoutNodes:nodes nodesSizeChanged:nodesSizeChanged]; + [self nodesDidRelayout:nodesSizeChanged]; + } + + // Flush any pending invalidation action if needed. + ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle; + _nextLayoutInvalidationStyle = ASCollectionViewInvalidationStyleNone; + switch (invalidationStyle) { + case ASCollectionViewInvalidationStyleWithAnimation: + if (0 == _superBatchUpdateCount) { + if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysReloadData)) { + [self _superReloadData:nil completion:nil]; + } else { + [self _superPerformBatchUpdates:nil completion:nil]; + } + } + break; + case ASCollectionViewInvalidationStyleWithoutAnimation: + [self.collectionViewLayout invalidateLayout]; + break; + default: + break; + } + + // To ensure _maxSizeForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last + [super layoutSubviews]; + + if (_zeroContentInsets) { + self.contentInset = UIEdgeInsetsZero; + } + + // Update range controller immediately if possible & needed. + // Calling -updateIfNeeded in here with self.window == nil (early in the collection view's life) + // may cause UICollectionView data related crashes. We'll update in -didMoveToWindow anyway. + if (self.window != nil) { + [_rangeController updateIfNeeded]; + } +} + + +#pragma mark - Batch Fetching + +- (ASBatchContext *)batchContext +{ + return _batchContext; +} + +- (BOOL)canBatchFetch +{ + // if the delegate does not respond to this method, there is no point in starting to fetch + BOOL canFetch = _asyncDelegateFlags.collectionNodeWillBeginBatchFetch || _asyncDelegateFlags.collectionViewWillBeginBatchFetch; + if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionNode) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + return [_asyncDelegate shouldBatchFetchForCollectionNode:collectionNode]; + } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionView) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate shouldBatchFetchForCollectionView:self]; +#pragma clang diagnostic pop + } else { + return canFetch; + } +} + +- (id)batchFetchingDelegate{ + return self.collectionNode.batchFetchingDelegate; +} + +- (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes +{ + // Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided + if (changes == 0 && _hasEverCheckedForBatchFetchingDueToUpdate) { + return; + } + _hasEverCheckedForBatchFetchingDueToUpdate = YES; + + // Push this to the next runloop to be sure the scroll view has the right content size + dispatch_async(dispatch_get_main_queue(), ^{ + [self _checkForBatchFetching]; + }); +} + +- (void)_checkForBatchFetching +{ + // Dragging will be handled in scrollViewWillEndDragging:withVelocity:targetContentOffset: + if (self.isDragging || self.isTracking) { + return; + } + + [self _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset velocity:CGPointZero]; +} + +- (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset velocity:(CGPoint)velocity +{ + if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, self.scrollableDirections, contentOffset, velocity)) { + [self _beginBatchFetching]; + } +} + +- (void)_beginBatchFetching +{ + as_activity_create_for_scope("Batch fetch for collection node"); + [_batchContext beginBatchFetching]; + if (_asyncDelegateFlags.collectionNodeWillBeginBatchFetch) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + as_log_debug(ASCollectionLog(), "Beginning batch fetch for %@ with context %@", collectionNode, _batchContext); + [_asyncDelegate collectionNode:collectionNode willBeginBatchFetchWithContext:_batchContext]; + }); + } else if (_asyncDelegateFlags.collectionViewWillBeginBatchFetch) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext]; +#pragma clang diagnostic pop + }); + } +} + +#pragma mark - ASDataControllerSource + +- (BOOL)dataController:(ASDataController *)dataController shouldEagerlyLayoutNode:(ASCellNode *)node +{ + NSAssert(!ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysLazy), + @"ASCellLayoutModeAlwaysLazy flag is no longer supported"); + return !node.shouldUseUIKitCell; +} + +- (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet +{ + // If we have AlwaysSync set, block and donate main priority. + if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysSync)) { + return YES; + } + // Prioritize AlwaysAsync over the remaining heuristics for the Default mode. + if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysAsync)) { + return NO; + } + // Reload data is expensive, don't block main while doing so. + if (changeSet.includesReloadData) { + return NO; + } + // If we have very few ASCellNodes (besides UIKit passthrough ones), match UIKit by blocking. + if (changeSet.countForAsyncLayout < 2) { + return YES; + } + CGSize contentSize = self.contentSize; + CGSize boundsSize = self.bounds.size; + if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { + return YES; + } + return NO; // ASCellLayoutModeNone +} + +- (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataController +{ + return ASCellLayoutModeIncludes(ASCellLayoutModeSerializeNodeCreation); +} + +- (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (!_asyncDataSourceFlags.nodeModelForItem) { + return nil; + } + + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + return [_asyncDataSource collectionNode:collectionNode nodeModelForItemAtIndexPath:indexPath]; +} + +- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout +{ + ASDisplayNodeAssertMainThread(); + ASCellNodeBlock block = nil; + ASCellNode *cell = nil; + + if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + block = [_asyncDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]; + } + if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + cell = [_asyncDataSource collectionNode:collectionNode nodeForItemAtIndexPath:indexPath]; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeBlockForItem) { + block = [_asyncDataSource collectionView:self nodeBlockForItemAtIndexPath:indexPath]; + } + if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForItem) { + cell = [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; + } +#pragma clang diagnostic pop + + if (block == nil) { + if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) { + // In this case, either the client is expecting a UIKit passthrough cell to be created automatically, + // or it is an error. + if (_asyncDataSourceFlags.interop) { + cell = [[ASWrapperCellNode alloc] init]; + } else { + ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); + cell = [[ASCellNode alloc] init]; + } + } + + // This condition is intended to run for either cells received from the datasource, or created just above. + if (cell.shouldUseUIKitCell) { + *shouldAsyncLayout = NO; + } + } + + // Wrap the node block + BOOL disableRangeController = ASCellLayoutModeIncludes(ASCellLayoutModeDisableRangeController); + __weak __typeof__(self) weakSelf = self; + return ^{ + __typeof__(self) strongSelf = weakSelf; + ASCellNode *node = (block ? block() : cell); + ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASCollectionNode provided a non-ASCellNode! %@, %@", node, strongSelf); + + if (!disableRangeController) { + [node enterHierarchyState:ASHierarchyStateRangeManaged]; + } + if (node.interactionDelegate == nil) { + node.interactionDelegate = strongSelf; + } + if (strongSelf.inverted) { + node.transform = CATransform3DMakeScale(1, -1, 1); + } + return node; + }; +} + +- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section +{ + if (_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); + return [_asyncDataSource collectionNode:collectionNode numberOfItemsInSection:section]; + } else if (_asyncDataSourceFlags.collectionViewNumberOfItemsInSection) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDataSource collectionView:self numberOfItemsInSection:section]; +#pragma clang diagnostic pop + } else { + return 0; + } +} + +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { + if (_asyncDataSourceFlags.numberOfSectionsInCollectionNode) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); + return [_asyncDataSource numberOfSectionsInCollectionNode:collectionNode]; + } else if (_asyncDataSourceFlags.numberOfSectionsInCollectionView) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDataSource numberOfSectionsInCollectionView:self]; +#pragma clang diagnostic pop + } else { + return 1; + } +} + +- (BOOL)dataController:(ASDataController *)dataController presentedSizeForElement:(ASCollectionElement *)element matchesSize:(CGSize)size +{ + NSIndexPath *indexPath = [self indexPathForNode:element.node]; + if (indexPath == nil) { + ASDisplayNodeFailAssert(@"Data controller should not ask for presented size for element that is not presented."); + return YES; + } + + UICollectionViewLayoutAttributes *attributes; + if (element.supplementaryElementKind == nil) { + attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; + } else { + attributes = [self layoutAttributesForSupplementaryElementOfKind:element.supplementaryElementKind atIndexPath:indexPath]; + } + return CGSizeEqualToSizeWithIn(attributes.size, size, FLT_EPSILON); +} + +#pragma mark - ASDataControllerSource optional methods + +- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout +{ + ASDisplayNodeAssertMainThread(); + ASCellNodeBlock block = nil; + ASCellNode *cell = nil; + if (_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + block = [_asyncDataSource collectionNode:collectionNode nodeBlockForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + } + if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + cell = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + } + if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + cell = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; +#pragma clang diagnostic pop + } + + if (block == nil) { + if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) { + // In this case, the app code returned nil for the node and the nodeBlock. + // If the UIKit method is implemented, then we should use a passthrough cell. + // Otherwise the CGSizeZero default will cause UIKit to not show it (so this isn't an error like the cellForItem case). + + BOOL useUIKitCell = _asyncDataSourceFlags.interopViewForSupplementaryElement; + if (useUIKitCell) { + cell = [[ASWrapperCellNode alloc] init]; + } else { + cell = [[ASCellNode alloc] init]; + } + } + + // This condition is intended to run for either cells received from the datasource, or created just above. + if (cell.shouldUseUIKitCell) { + *shouldAsyncLayout = NO; + } + + block = ^{ return cell; }; + } + + // Wrap the node block + // BOOL disableRangeController = ASCellLayoutModeIncludes(ASCellLayoutModeDisableRangeController); + __weak __typeof__(self) weakSelf = self; + return ^{ + __typeof__(self) strongSelf = weakSelf; + ASCellNode *node = block(); + ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], + @"ASCollectionNode provided a non-ASCellNode! %@, %@", node, strongSelf); + + // TODO: ASRangeController doesn't currently support managing interfaceState for supplementary nodes. + // For now, we allow the standard ASInterfaceStateInHierarchy behavior by ensuring we do not inform + // the node that it should expect external management of interfaceState. + /* + if (!disableRangeController) { + [node enterHierarchyState:ASHierarchyStateRangeManaged]; + } + */ + + if (node.interactionDelegate == nil) { + node.interactionDelegate = strongSelf; + } + if (strongSelf.inverted) { + node.transform = CATransform3DMakeScale(1, -1, 1); + } + return node; + }; +} + +- (NSArray *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections +{ + if (_asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection) { + const auto kinds = [[NSMutableSet alloc] init]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, @[]); + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + NSArray *kindsForSection = [_asyncDataSource collectionNode:collectionNode supplementaryElementKindsInSection:section]; + [kinds addObjectsFromArray:kindsForSection]; + }]; + return [kinds allObjects]; + } else { + // TODO: Lock this + return [_registeredSupplementaryKinds allObjects]; + } +} + +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + return [self.layoutInspector collectionView:self constrainedSizeForNodeAtIndexPath:indexPath]; +} + +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if (_layoutInspectorFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath) { + return [self.layoutInspector collectionView:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; + } + + ASDisplayNodeAssert(NO, @"To support supplementary nodes in ASCollectionView, it must have a layoutInspector for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return ASSizeRangeMake(CGSizeZero, CGSizeZero); +} + +- (NSUInteger)dataController:(ASDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + if (_asyncDataSource == nil) { + return 0; + } + + if (_layoutInspectorFlags.supplementaryNodesOfKindInSection) { + return [self.layoutInspector collectionView:self supplementaryNodesOfKind:kind inSection:section]; + } + + ASDisplayNodeAssert(NO, @"To support supplementary nodes in ASCollectionView, it must have a layoutInspector for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return 0; +} + +- (id)dataController:(ASDataController *)dataController contextForSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + id context = nil; + + if (_asyncDataSourceFlags.collectionNodeContextForSection) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + context = [_asyncDataSource collectionNode:collectionNode contextForSection:section]; + } + + if (context != nil) { + context.collectionView = self; + } + return context; +} + +#pragma mark - ASRangeControllerDataSource + +- (ASRangeController *)rangeController +{ + return _rangeController; +} + +- (NSHashTable *)visibleElementsForRangeController:(ASRangeController *)rangeController +{ + return ASPointerTableByFlatMapping(_visibleElements, id element, element); +} + +- (ASElementMap *)elementMapForRangeController:(ASRangeController *)rangeController +{ + return _dataController.visibleMap; +} + +- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController +{ + return self.scrollDirection; +} + +- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController +{ + return ASInterfaceStateForDisplayNode(self.collectionNode, self.window); +} + +- (NSString *)nameForRangeControllerDataSource +{ + return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); +} + +#pragma mark - ASRangeControllerDelegate + +- (BOOL)rangeControllerShouldUpdateRanges:(ASRangeController *)rangeController +{ + return !ASCellLayoutModeIncludes(ASCellLayoutModeDisableRangeController); +} + +- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad || _updatingInResponseToInteractiveMove) { + updates(); + [changeSet executeCompletionHandlerWithFinished:NO]; + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + //TODO Do we need to notify _layoutFacilitator before reloadData? + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; + } + + ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ + as_activity_scope(as_activity_create("Commit collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + if (changeSet.includesReloadData) { + _superIsPendingDataLoad = YES; + updates(); + [self _superReloadData:nil completion:nil]; + as_log_debug(ASCollectionLog(), "Did reloadData %@", self.collectionNode); + [changeSet executeCompletionHandlerWithFinished:YES]; + } else { + [_layoutFacilitator collectionViewWillPerformBatchUpdates]; + + __block NSUInteger numberOfUpdates = 0; + const auto completion = ^(BOOL finished) { + as_activity_scope(as_activity_create("Handle collection update completion", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASCollectionLog(), "Update animation finished %{public}@", self.collectionNode); + // Flush any range changes that happened as part of the update animations ending. + [_rangeController updateIfNeeded]; + [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; + [changeSet executeCompletionHandlerWithFinished:finished]; + }; + + BOOL shouldReloadData = ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysReloadData); + // TODO: Consider adding !changeSet.isEmpty as a check to also disable shouldReloadData. + if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysBatchUpdateSectionReload) && + [changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload].count > 0) { + shouldReloadData = NO; + } + + if (shouldReloadData) { + // When doing a reloadData, the insert / delete calls are not necessary. + // Calling updates() is enough, as it commits .pendingMap to .visibleMap. + [self _superReloadData:updates completion:completion]; + } else { + [self _superPerformBatchUpdates:^{ + updates(); + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { + [super reloadItemsAtIndexPaths:change.indexPaths]; + numberOfUpdates++; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { + [super reloadSections:change.indexSet]; + numberOfUpdates++; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { + [super deleteItemsAtIndexPaths:change.indexPaths]; + numberOfUpdates++; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { + [super deleteSections:change.indexSet]; + numberOfUpdates++; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { + [super insertSections:change.indexSet]; + numberOfUpdates++; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { + [super insertItemsAtIndexPaths:change.indexPaths]; + numberOfUpdates++; + } + } completion:completion]; + } + + as_log_debug(ASCollectionLog(), "Completed batch update %{public}@", self.collectionNode); + + // Flush any range changes that happened as part of submitting the update. + as_activity_scope(changeSet.rootActivity); + [_rangeController updateIfNeeded]; + } + }); +} + +#pragma mark - ASCellNodeDelegate + +- (void)nodeSelectedStateDidChange:(ASCellNode *)node +{ + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath) { + if (node.isSelected) { + [super selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; + } else { + [super deselectItemAtIndexPath:indexPath animated:NO]; + } + } +} + +- (void)nodeHighlightedStateDidChange:(ASCellNode *)node +{ + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath) { + [self cellForItemAtIndexPath:indexPath].highlighted = node.isHighlighted; + } +} + +- (void)nodeDidInvalidateSize:(ASCellNode *)node +{ + [_cellsForLayoutUpdates addObject:node]; + [self setNeedsLayout]; +} + +- (void)nodesDidRelayout:(NSArray *)nodes +{ + ASDisplayNodeAssertMainThread(); + + if (nodes.count == 0) { + return; + } + + const auto uikitIndexPaths = ASArrayByFlatMapping(nodes, ASCellNode *node, [self indexPathForNode:node]); + + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:uikitIndexPaths batched:NO]; + + ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle; + for (ASCellNode *node in nodes) { + if (invalidationStyle == ASCollectionViewInvalidationStyleNone) { + // We nodesDidRelayout also while we are in layoutSubviews. This should be no problem as CA will ignore this + // call while be in a layout pass + [self setNeedsLayout]; + invalidationStyle = ASCollectionViewInvalidationStyleWithAnimation; + } + + // If we think we're going to animate, check if this node will prevent it. + if (invalidationStyle == ASCollectionViewInvalidationStyleWithAnimation) { + // TODO: Incorporate `shouldAnimateSizeChanges` into ASEnvironmentState for performance benefit. + static dispatch_once_t onceToken; + static BOOL (^shouldNotAnimateBlock)(ASDisplayNode *); + dispatch_once(&onceToken, ^{ + shouldNotAnimateBlock = ^BOOL(ASDisplayNode * _Nonnull node) { + return (node.shouldAnimateSizeChanges == NO); + }; + }); + if (ASDisplayNodeFindFirstNode(node, shouldNotAnimateBlock) != nil) { + // One single non-animated node causes the whole layout update to be non-animated + invalidationStyle = ASCollectionViewInvalidationStyleWithoutAnimation; + break; + } + } + } + _nextLayoutInvalidationStyle = invalidationStyle; +} + +#pragma mark - _ASDisplayView behavior substitutions +// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element. +// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView. +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + BOOL visible = (newWindow != nil); + ASDisplayNode *node = self.collectionNode; + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + BOOL visible = (self.window != nil); + ASDisplayNode *node = self.collectionNode; + BOOL rangeControllerNeedsUpdate = ![node supportsRangeManagedInterfaceState];; + + if (!visible && node.inHierarchy) { + if (rangeControllerNeedsUpdate) { + rangeControllerNeedsUpdate = NO; + // Exit CellNodes first before Collection to match UIKit behaviors (tear down bottom up). + // Although we have not yet cleared the interfaceState's Visible bit (this happens in __exitHierarchy), + // the ASRangeController will get the correct value from -interfaceStateForRangeController:. + [_rangeController updateRanges]; + } + [node __exitHierarchy]; + } + + // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their + // their update in the layout pass + if (rangeControllerNeedsUpdate) { + [_rangeController updateRanges]; + } + + // When we aren't visible, we will only fetch up to the visible area. Now that we are visible, + // we will fetch visible area + leading screens, so we need to check. + if (visible) { + [self _checkForBatchFetching]; + } +} + +- (void)willMoveToSuperview:(UIView *)newSuperview +{ + if (self.superview == nil && newSuperview != nil) { + _keepalive_node = self.collectionNode; + } +} + +- (void)didMoveToSuperview +{ + if (self.superview == nil) { + _keepalive_node = nil; + } +} + +#pragma mark ASCALayerExtendedDelegate + +/** + * TODO: This code was added when we used @c calculatedSize as the size for + * items (e.g. collectionView:layout:sizeForItemAtIndexPath:) and so it + * was critical that we remeasured all nodes at this time. + * + * The assumption was that cv-bounds-size-change -> constrained-size-change, so + * this was the time when we get new constrained sizes for all items and remeasure + * them. However, the constrained sizes for items can be invalidated for many other + * reasons, hence why we never reuse the old constrained size anymore. + * + * UICollectionView inadvertently triggers a -prepareLayout call to its layout object + * between [super setFrame:] and [self layoutSubviews] during size changes. So we need + * to get in there and re-measure our nodes before that -prepareLayout call. + * We can't wait until -layoutSubviews or the end of -setFrame:. + * + * @see @p testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation + */ +- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds +{ + CGSize newSize = newBounds.size; + CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; + if (CGSizeEqualToSize(lastUsedSize, newSize)) { + return; + } + if (_hasDataControllerLayoutDelegate || self.collectionViewLayout == nil) { + // Let the layout delegate handle bounds changes if it's available. If no layout, it will init in the new state. + return; + } + + _lastBoundsSizeUsedForMeasuringNodes = newSize; + + // Laying out all nodes is expensive. + // We only need to do this if the bounds changed in the non-scrollable direction. + // If, for example, a vertical flow layout has its height changed due to a status bar + // appearance update, we do not need to relayout all nodes. + // For a more permanent fix to the unsafety mentioned above, see https://github.com/facebook/AsyncDisplayKit/pull/2182 + ASScrollDirection scrollDirection = self.scrollableDirections; + BOOL fixedVertically = (ASScrollDirectionContainsVerticalDirection (scrollDirection) == NO); + BOOL fixedHorizontally = (ASScrollDirectionContainsHorizontalDirection(scrollDirection) == NO); + + BOOL changedInNonScrollingDirection = (fixedHorizontally && newSize.width != lastUsedSize.width) || + (fixedVertically && newSize.height != lastUsedSize.height); + + if (changedInNonScrollingDirection) { + [self relayoutItems]; + } +} + +#pragma mark - UICollectionView dead-end intercepts + +- (void)setPrefetchDataSource:(id)prefetchDataSource +{ + return; +} + +- (void)setPrefetchingEnabled:(BOOL)prefetchingEnabled +{ + return; +} + +#pragma mark - Accessibility overrides + +- (NSArray *)accessibilityElements +{ + [self waitUntilAllUpdatesAreCommitted]; + return [super accessibilityElements]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionViewLayoutFacilitatorProtocol.h b/submodules/AsyncDisplayKit/Source/ASCollectionViewLayoutFacilitatorProtocol.h new file mode 100644 index 0000000000..b60dd0e02f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionViewLayoutFacilitatorProtocol.h @@ -0,0 +1,48 @@ +// +// ASCollectionViewLayoutFacilitatorProtocol.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#pragma once +#import + +/** + * This facilitator protocol is intended to help Layout to better + * gel with the CollectionView + */ +@protocol ASCollectionViewLayoutFacilitatorProtocol + +/** + * Inform that the collectionView is editing the cells at a list of indexPaths + * + * @param indexPaths an array of NSIndexPath objects of cells being/will be edited. + * @param isBatched indicates whether the editing operation will be batched by the collectionView + * + * NOTE: when isBatched, used in combination with -collectionViewWillPerformBatchUpdates + */ +- (void)collectionViewWillEditCellsAtIndexPaths:(NSArray *)indexPaths batched:(BOOL)isBatched; + +/** + * Inform that the collectionView is editing the sections at a set of indexes + * + * @param indexes an NSIndexSet of section indexes being/will be edited. + * @param batched indicates whether the editing operation will be batched by the collectionView + * + * NOTE: when batched, used in combination with -collectionViewWillPerformBatchUpdates + */ +- (void)collectionViewWillEditSectionsAtIndexSet:(NSIndexSet *)indexes batched:(BOOL)batched; + +/** + * Informs the delegate that the collectionView is about to call performBatchUpdates + */ +- (void)collectionViewWillPerformBatchUpdates; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollectionViewProtocols.h b/submodules/AsyncDisplayKit/Source/ASCollectionViewProtocols.h new file mode 100644 index 0000000000..459e12efcd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollectionViewProtocols.h @@ -0,0 +1,103 @@ +// +// ASCollectionViewProtocols.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +typedef NS_OPTIONS(NSUInteger, ASCellLayoutMode) { + /** + * No options set. If cell layout mode is set to ASCellLayoutModeNone, the default values for + * each flag listed below is used. + */ + ASCellLayoutModeNone = 0, + /** + * If ASCellLayoutModeAlwaysSync is enabled it will cause the ASDataController to wait on the + * background queue, and this ensures that any new / changed cells are in the hierarchy by the + * very next CATransaction / frame draw. + * + * Note: Sync & Async flags force the behavior to be always one or the other, regardless of the + * items. Default: If neither ASCellLayoutModeAlwaysSync or ASCellLayoutModeAlwaysAsync is set, + * default behavior is synchronous when there are 0 or 1 ASCellNodes in the data source, and + * asynchronous when there are 2 or more. + */ + ASCellLayoutModeAlwaysSync = 1 << 1, // Default OFF + ASCellLayoutModeAlwaysAsync = 1 << 2, // Default OFF + ASCellLayoutModeForceIfNeeded = 1 << 3, // Deprecated, default OFF. + ASCellLayoutModeAlwaysPassthroughDelegate = 1 << 4, // Deprecated, default ON. + /** Instead of using performBatchUpdates: prefer using reloadData for changes for collection view */ + ASCellLayoutModeAlwaysReloadData = 1 << 5, // Default OFF + /** If flag is enabled nodes are *not* gonna be range managed. */ + ASCellLayoutModeDisableRangeController = 1 << 6, // Default OFF + ASCellLayoutModeAlwaysLazy = 1 << 7, // Deprecated, default OFF. + /** + * Defines if the node creation should happen serialized and not in parallel within the + * data controller + */ + ASCellLayoutModeSerializeNodeCreation = 1 << 8, // Default OFF + /** + * When set, the performBatchUpdates: API (including animation) is used when handling Section + * Reload operations. This is useful only when ASCellLayoutModeAlwaysReloadData is enabled and + * cell height animations are desired. + */ + ASCellLayoutModeAlwaysBatchUpdateSectionReload = 1 << 9, // Default OFF +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + * This is a subset of UICollectionViewDataSource. + * + * @see ASCollectionDataSource + */ +@protocol ASCommonCollectionDataSource + +@optional + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section ASDISPLAYNODE_DEPRECATED_MSG("Implement -collectionNode:numberOfItemsInSection: instead."); + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Implement -numberOfSectionsInCollectionNode: instead."); + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement - collectionNode:nodeForSupplementaryElementOfKind:atIndexPath: instead."); + +@end + + +/** + * This is a subset of UICollectionViewDelegate. + * + * @see ASCollectionDelegate + */ +@protocol ASCommonCollectionDelegate + +@optional + +- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout; + +- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -collectionNode:willDisplaySupplementaryView:forElementKind:atIndexPath: instead."); +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -collectionNode:didEndDisplayingSupplementaryView:forElementKind:atIndexPath: instead."); + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:shouldHighlightItemAtIndexPath: instead."); +- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:didHighlightItemAtIndexPath: instead."); +- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:didUnhighlightItemAtIndexPath: instead."); + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:shouldSelectItemAtIndexPath: instead."); +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:didSelectItemAtIndexPath: instead."); +- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:shouldDeselectItemAtIndexPath: instead."); +- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:didDeselectItemAtIndexPath: instead."); + +- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:shouldShowMenuForItemAtIndexPath: instead."); +- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:canPerformAction:forItemAtIndexPath:withSender: instead."); +- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender ASDISPLAYNODE_DEPRECATED_MSG("Implement collectionNode:performAction:forItemAtIndexPath:withSender: instead."); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASCollections.h b/submodules/AsyncDisplayKit/Source/ASCollections.h new file mode 100644 index 0000000000..ca3b31c65c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollections.h @@ -0,0 +1,38 @@ +// +// ASCollections.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray<__covariant ObjectType> (ASCollections) + +/** + * Create an immutable NSArray from a C-array of strong pointers. + * + * Note: The memory for the array you pass in will be zero'd (to prevent ARC from releasing + * the references when the array goes out of scope.) + * + * Can be combined with vector like: + * vector vec; + * vec.push_back(@"foo"); + * vec.push_back(@"bar"); + * NSArray *arr = [NSArray arrayTransferring:vec.data() count:vec.size()] + * ** vec is now { nil, nil } ** + * + * Unfortunately making a convenience method to do this is currently impossible because + * vector can't be converted to vector by the compiler (silly). + * + * See the private __CFArrayCreateTransfer function. + */ ++ (NSArray *)arrayByTransferring:(ObjectType _Nonnull __strong * _Nonnull)pointers + count:(NSUInteger)count NS_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASCollections.mm b/submodules/AsyncDisplayKit/Source/ASCollections.mm new file mode 100644 index 0000000000..592dee2e88 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollections.mm @@ -0,0 +1,61 @@ +// +// ASCollections.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * A private allocator that signals to our retain callback to skip the retain. + * It behaves the same as the default allocator, but acts as a signal that we + * are creating a transfer array so we should skip the retain. + */ +static CFAllocatorRef gTransferAllocator; + +static const void *ASTransferRetain(CFAllocatorRef allocator, const void *val) { + if (allocator == gTransferAllocator) { + // Transfer allocator. Ignore retain and pass through. + return val; + } else { + // Other allocator. Retain like normal. + // This happens when they make a mutable copy. + return (&kCFTypeArrayCallBacks)->retain(allocator, val); + } +} + +@implementation NSArray (ASCollections) + ++ (NSArray *)arrayByTransferring:(__strong id *)pointers count:(NSUInteger)count NS_RETURNS_RETAINED +{ + // Custom callbacks that point to our ASTransferRetain callback. + static CFArrayCallBacks callbacks; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + callbacks = kCFTypeArrayCallBacks; + callbacks.retain = ASTransferRetain; + CFAllocatorContext ctx; + CFAllocatorGetContext(NULL, &ctx); + gTransferAllocator = CFAllocatorCreate(NULL, &ctx); + }); + + // NSZeroArray fast path. + if (count == 0) { + return @[]; // Does not actually call +array when optimized. + } + + // NSSingleObjectArray fast path. Retain/release here is worth it. + if (count == 1) { + NSArray *result = [[NSArray alloc] initWithObjects:pointers count:1]; + pointers[0] = nil; + return result; + } + + NSArray *result = (__bridge_transfer NSArray *)CFArrayCreate(gTransferAllocator, (const void **)(void *)pointers, count, &callbacks); + memset(pointers, 0, count * sizeof(id)); + return result; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASConfiguration.h b/submodules/AsyncDisplayKit/Source/ASConfiguration.h new file mode 100644 index 0000000000..c529dad801 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfiguration.h @@ -0,0 +1,58 @@ +// +// ASConfiguration.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@protocol ASConfigurationDelegate; + +NS_ASSUME_NONNULL_BEGIN + +static NSInteger const ASConfigurationSchemaCurrentVersion = 1; + +AS_SUBCLASSING_RESTRICTED +@interface ASConfiguration : NSObject + +/** + * Initialize this configuration with the provided dictionary, + * or nil to create an empty configuration. + * + * The schema is located in `schemas/configuration.json`. + */ +- (instancetype)initWithDictionary:(nullable NSDictionary *)dictionary; + +/** + * The delegate for configuration-related events. + * Delegate methods are called from a serial queue. + */ +@property (nonatomic, nullable) id delegate; + +/** + * The experimental features to enable in Texture. + * See ASExperimentalFeatures for functions to convert to/from a string array. + */ +@property (nonatomic) ASExperimentalFeatures experimentalFeatures; + +@end + +/** + * Implement this method in a category to make your + * configuration available to Texture. It will be read + * only once and copied. + * + * NOTE: To specify your configuration at compile-time, you can + * define AS_FIXED_CONFIG_JSON as a C-string of JSON. This method + * will then be implemented to parse that string and generate + * a configuration. + */ +@interface ASConfiguration (UserProvided) ++ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASConfiguration.mm b/submodules/AsyncDisplayKit/Source/ASConfiguration.mm new file mode 100644 index 0000000000..34f11bd366 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfiguration.mm @@ -0,0 +1,64 @@ +// +// ASConfiguration.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +/// Not too performance-sensitive here. + +@implementation ASConfiguration + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ + if (self = [super init]) { + if (dictionary != nil) { + const auto featureStrings = ASDynamicCast(dictionary[@"experimental_features"], NSArray); + const auto version = ASDynamicCast(dictionary[@"version"], NSNumber).integerValue; + if (version != ASConfigurationSchemaCurrentVersion) { + NSLog(@"Texture warning: configuration schema is old version (%ld vs %ld)", (long)version, (long)ASConfigurationSchemaCurrentVersion); + } + self.experimentalFeatures = ASExperimentalFeaturesFromArray(featureStrings); + } else { + self.experimentalFeatures = kNilOptions; + } + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = self.experimentalFeatures; + config.delegate = self.delegate; + return config; +} + +@end + +//#define AS_FIXED_CONFIG_JSON "{ \"version\" : 1, \"experimental_features\": [ \"exp_text_node\" ] }" + +#ifdef AS_FIXED_CONFIG_JSON + +@implementation ASConfiguration (UserProvided) + ++ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED +{ + NSData *data = [@AS_FIXED_CONFIG_JSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSDictionary *d = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; + if (!d) { + NSAssert(NO, @"Error parsing fixed config string '%s': %@", AS_FIXED_CONFIG_JSON, error); + return nil; + } else { + return [[ASConfiguration alloc] initWithDictionary:d]; + } +} + +@end + +#endif // AS_FIXED_CONFIG_JSON diff --git a/submodules/AsyncDisplayKit/Source/ASConfigurationDelegate.h b/submodules/AsyncDisplayKit/Source/ASConfigurationDelegate.h new file mode 100644 index 0000000000..127ec3eb14 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfigurationDelegate.h @@ -0,0 +1,37 @@ +// +// ASConfigurationDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Used to communicate configuration-related events to the client. + */ +@protocol ASConfigurationDelegate + +/** + * Texture performed its first behavior related to the feature(s). + * This can be useful for tracking the impact of the behavior (A/B testing). + */ +- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)features; + +@optional + +/** + * Texture framework initialized. This method is called synchronously + * on the main thread from ASInitializeFrameworkMainThread if you defined + * AS_INITIALIZE_FRAMEWORK_MANUALLY or from the default initialization point + * (currently +load) otherwise. + */ +- (void)textureDidInitialize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.h b/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.h new file mode 100644 index 0000000000..eb639c2243 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.h @@ -0,0 +1,57 @@ +// +// ASConfigurationInternal.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +/// Note this has to be public because it's imported by public header ASThread.h =/ +/// It will be private again after exp_unfair_lock ends. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Quickly check if an experiment is enabled and notify the delegate + * that it's been activated. + * + * The delegate will be notified asynchronously. + */ +#if DEBUG +#define ASActivateExperimentalFeature(opt) _ASActivateExperimentalFeature(opt) +#else +#define ASActivateExperimentalFeature(opt) ({\ + static BOOL result;\ + static dispatch_once_t onceToken;\ + dispatch_once(&onceToken, ^{ result = _ASActivateExperimentalFeature(opt); });\ + result;\ +}) +#endif + +/** + * Internal function. Use the macro without the underbar. + */ +AS_EXTERN BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures option); + +/** + * Notify the configuration delegate that the framework initialized, if needed. + */ +AS_EXTERN void ASNotifyInitialized(void); + +AS_SUBCLASSING_RESTRICTED +@interface ASConfigurationManager : NSObject + +/** + * No API for now. + * Just use ASActivateExperimentalFeature to access this efficiently. + */ + +/* Exposed for testing purposes only */ ++ (void)test_resetWithConfiguration:(nullable ASConfiguration *)configuration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm b/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm new file mode 100644 index 0000000000..485fef0c58 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm @@ -0,0 +1,111 @@ +// +// ASConfigurationInternal.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASConfigurationInternal.h" +#import +#import +#import +#import + +static ASConfigurationManager *ASSharedConfigurationManager; +static dispatch_once_t ASSharedConfigurationManagerOnceToken; + +NS_INLINE ASConfigurationManager *ASConfigurationManagerGet() { + dispatch_once(&ASSharedConfigurationManagerOnceToken, ^{ + ASSharedConfigurationManager = [[ASConfigurationManager alloc] init]; + }); + return ASSharedConfigurationManager; +} + +@implementation ASConfigurationManager { + ASConfiguration *_config; + dispatch_queue_t _delegateQueue; + BOOL _frameworkInitialized; + _Atomic(ASExperimentalFeatures) _activatedExperiments; +} + ++ (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED +{ + ASConfiguration *config = [[ASConfiguration alloc] init]; + // TODO(wsdwsd0829): Fix #788 before enabling it. + // config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; + return config; +} + +- (instancetype)init +{ + if (self = [super init]) { + _delegateQueue = dispatch_queue_create("org.TextureGroup.Texture.ConfigNotifyQueue", DISPATCH_QUEUE_SERIAL); + if ([ASConfiguration respondsToSelector:@selector(textureConfiguration)]) { + _config = [[ASConfiguration textureConfiguration] copy]; + } else { + _config = [ASConfigurationManager defaultConfiguration]; + } + } + return self; +} + +- (void)frameworkDidInitialize +{ + ASDisplayNodeAssertMainThread(); + if (_frameworkInitialized) { + ASDisplayNodeFailAssert(@"Framework initialized twice."); + return; + } + _frameworkInitialized = YES; + + const auto delegate = _config.delegate; + if ([delegate respondsToSelector:@selector(textureDidInitialize)]) { + [delegate textureDidInitialize]; + } +} + +- (BOOL)activateExperimentalFeature:(ASExperimentalFeatures)requested +{ + if (_config == nil) { + return NO; + } + + NSAssert(__builtin_popcountl(requested) == 1, @"Cannot activate multiple features at once with this method."); + + // We need to call out, whether it's enabled or not. + // A/B testing requires even "control" users to be activated. + ASExperimentalFeatures enabled = requested & _config.experimentalFeatures; + ASExperimentalFeatures prevTriggered = atomic_fetch_or(&_activatedExperiments, requested); + ASExperimentalFeatures newlyTriggered = requested & ~prevTriggered; + + // Notify delegate if needed. + if (newlyTriggered != 0) { + __unsafe_unretained id del = _config.delegate; + dispatch_async(_delegateQueue, ^{ + [del textureDidActivateExperimentalFeatures:newlyTriggered]; + }); + } + + return (enabled != 0); +} + +// Define this even when !DEBUG, since we may run our tests in release mode. ++ (void)test_resetWithConfiguration:(ASConfiguration *)configuration +{ + ASConfigurationManager *inst = ASConfigurationManagerGet(); + inst->_config = configuration ?: [self defaultConfiguration]; + atomic_store(&inst->_activatedExperiments, 0); +} + +@end + +BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures feature) +{ + return [ASConfigurationManagerGet() activateExperimentalFeature:feature]; +} + +void ASNotifyInitialized() +{ + [ASConfigurationManagerGet() frameworkDidInitialize]; +} diff --git a/submodules/AsyncDisplayKit/Source/ASContextTransitioning.h b/submodules/AsyncDisplayKit/Source/ASContextTransitioning.h new file mode 100644 index 0000000000..9802ecefc4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASContextTransitioning.h @@ -0,0 +1,72 @@ +// +// ASContextTransitioning.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASDisplayNode; +@class ASLayout; + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN NSString * const ASTransitionContextFromLayoutKey; +AS_EXTERN NSString * const ASTransitionContextToLayoutKey; + +@protocol ASContextTransitioning + +/** + @abstract Defines if the given transition is animated + */ +- (BOOL)isAnimated; + +/** + * @abstract Retrieve either the "from" or "to" layout + */ +- (nullable ASLayout *)layoutForKey:(NSString *)key; + +/** + * @abstract Retrieve either the "from" or "to" constrainedSize + */ +- (ASSizeRange)constrainedSizeForKey:(NSString *)key; + +/** + * @abstract Retrieve the subnodes from either the "from" or "to" layout + */ +- (NSArray *)subnodesForKey:(NSString *)key; + +/** + * @abstract Subnodes that have been inserted in the layout transition + */ +- (NSArray *)insertedSubnodes; + +/** + * @abstract Subnodes that will be removed in the layout transition + */ +- (NSArray *)removedSubnodes; + +/** + @abstract The frame for the given node before the transition began. + @discussion Returns CGRectNull if the node was not in the hierarchy before the transition. + */ +- (CGRect)initialFrameForNode:(ASDisplayNode *)node; + +/** + @abstract The frame for the given node when the transition completes. + @discussion Returns CGRectNull if the node is no longer in the hierarchy after the transition. + */ +- (CGRect)finalFrameForNode:(ASDisplayNode *)node; + +/** + @abstract Invoke this method when the transition is completed in `animateLayoutTransition:` + @discussion Passing NO to `didComplete` will set the original layout as the new layout. + */ +- (void)completeTransition:(BOOL)didComplete; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASControlNode+Subclasses.h b/submodules/AsyncDisplayKit/Source/ASControlNode+Subclasses.h new file mode 100644 index 0000000000..2e9cdb2849 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASControlNode+Subclasses.h @@ -0,0 +1,68 @@ +// +// ASControlNode+Subclasses.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The subclass header _ASControlNode+Subclasses_ defines methods to be + * overridden by custom nodes that subclass ASControlNode. + * + * These methods should never be called directly by other classes. + */ + +@interface ASControlNode (Subclassing) + +/** + @abstract Sends action messages for the given control events. + @param controlEvents A bitmask whose set flags specify the control events for which action messages are sent. See "Control Events" in ASControlNode.h for bitmask constants. + @param touchEvent An event object encapsulating the information specific to the user event. + @discussion ASControlNode implements this method to send all action messages associated with controlEvents. The list of targets is constructed from prior invocations of addTarget:action:forControlEvents:. + */ +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)touchEvent; + +/** + @abstract Sent to the control when tracking begins. + @param touch The touch on the receiving control. + @param touchEvent An event object encapsulating the information specific to the user event. + @result YES if the receiver should respond continuously (respond when touch is dragged); NO otherwise. + */ +- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)touchEvent; + +/** + @abstract Sent continuously to the control as it tracks a touch within the control's bounds. + @param touch The touch on the receiving control. + @param touchEvent An event object encapsulating the information specific to the user event. + @result YES if touch tracking should continue; NO otherwise. + */ +- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)touchEvent; + +/** + @abstract Sent to the control when tracking should be cancelled. + @param touchEvent An event object encapsulating the information specific to the user event. This parameter may be nil, indicating that the cancelation was caused by something other than an event, such as the display node being removed from its supernode. + */ +- (void)cancelTrackingWithEvent:(nullable UIEvent *)touchEvent; + +/** + @abstract Sent to the control when the last touch completely ends, telling it to stop tracking. + @param touch The touch that ended. + @param touchEvent An event object encapsulating the information specific to the user event. + */ +- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)touchEvent; + +/** + @abstract Settable version of highlighted property. + */ +@property (getter=isHighlighted) BOOL highlighted; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASControlNode.h b/submodules/AsyncDisplayKit/Source/ASControlNode.h new file mode 100644 index 0000000000..0918fdb0b0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASControlNode.h @@ -0,0 +1,149 @@ +// +// ASControlNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#pragma once + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract Kinds of events possible for control nodes. + @discussion These events are identical to their UIControl counterparts. + */ +typedef NS_OPTIONS(NSUInteger, ASControlNodeEvent) +{ + /** A touch-down event in the control node. */ + ASControlNodeEventTouchDown = 1 << 0, + /** A repeated touch-down event in the control node; for this event the value of the UITouch tapCount method is greater than one. */ + ASControlNodeEventTouchDownRepeat = 1 << 1, + /** An event where a finger is dragged inside the bounds of the control node. */ + ASControlNodeEventTouchDragInside = 1 << 2, + /** An event where a finger is dragged just outside the bounds of the control. */ + ASControlNodeEventTouchDragOutside = 1 << 3, + /** A touch-up event in the control node where the finger is inside the bounds of the node. */ + ASControlNodeEventTouchUpInside = 1 << 4, + /** A touch-up event in the control node where the finger is outside the bounds of the node. */ + ASControlNodeEventTouchUpOutside = 1 << 5, + /** A system event canceling the current touches for the control node. */ + ASControlNodeEventTouchCancel = 1 << 6, + /** A system event triggered when controls like switches, slides, etc change state. */ + ASControlNodeEventValueChanged = 1 << 12, + /** A system event when the Play/Pause button on the Apple TV remote is pressed. */ + ASControlNodeEventPrimaryActionTriggered = 1 << 13, + + /** All events, including system events. */ + ASControlNodeEventAllEvents = 0xFFFFFFFF +}; + +/** + * Compatibility aliases for @c ASControlState enum. + * We previously provided our own enum, but when it was imported + * into Swift, the @c normal (0) option disappeared. + * + * Apple's UIControlState enum gets special treatment here, and + * UIControlStateNormal is available in Swift. + */ +typedef UIControlState ASControlState ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlState."); +static UIControlState const ASControlStateNormal ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateNormal.") = UIControlStateNormal; +static UIControlState const ASControlStateDisabled ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateDisabled.") = UIControlStateDisabled; +static UIControlState const ASControlStateHighlighted ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateHighlighted.") = UIControlStateHighlighted; +static UIControlState const ASControlStateSelected ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateSelected.") = UIControlStateSelected; + +/** + @abstract ASControlNode is the base class for control nodes (such as buttons), or nodes that track touches to invoke targets with action messages. + @discussion ASControlNode cannot be used directly. It instead defines the common interface and behavior structure for all its subclasses. Subclasses should import "ASControlNode+Subclasses.h" for information on methods intended to be overriden. + */ +@interface ASControlNode : ASDisplayNode + +#pragma mark - Control State + +/** + @abstract Indicates whether or not the receiver is enabled. + @discussion Specify YES to make the control enabled; otherwise, specify NO to make it disabled. The default value is YES. If the enabled state is NO, the control ignores touch events and subclasses may draw differently. + */ +@property (getter=isEnabled) BOOL enabled; + +/** + @abstract Indicates whether or not the receiver is highlighted. + @discussion This is set automatically when the there is a touch inside the control and removed on exit or touch up. This is different from touchInside in that it includes an area around the control, rather than just for touches inside the control. + */ +@property (getter=isHighlighted) BOOL highlighted; + +/** + @abstract Indicates whether or not the receiver is highlighted. + @discussion This is set automatically when the receiver is tapped. + */ +@property (getter=isSelected) BOOL selected; + +#pragma mark - Tracking Touches +/** + @abstract Indicates whether or not the receiver is currently tracking touches related to an event. + @discussion YES if the receiver is tracking touches; NO otherwise. + */ +@property (readonly, getter=isTracking) BOOL tracking; + +/** + @abstract Indicates whether or not a touch is inside the bounds of the receiver. + @discussion YES if a touch is inside the receiver's bounds; NO otherwise. + */ +@property (readonly, getter=isTouchInside) BOOL touchInside; + +#pragma mark - Action Messages +/** + @abstract Adds a target-action pair for a particular event (or events). + @param target The object to which the action message is sent. If this is nil, the responder chain is searched for an object willing to respond to the action message. target is not retained. + @param action A selector identifying an action message. May optionally include the sender and the event as parameters, in that order. May not be NULL. + @param controlEvents A bitmask specifying the control events for which the action message is sent. May not be 0. See "Control Events" for bitmask constants. + @discussion You may call this method multiple times, and you may specify multiple target-action pairs for a particular event. Targets are held weakly. + */ +- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEvents; + +/** + @abstract Returns the actions that are associated with a target and a particular control event. + @param target The target object. May not be nil. + @param controlEvent A single constant of type ASControlNodeEvent that specifies a particular user action on the control; for a list of these constants, see "Control Events". May not be 0 or ASControlNodeEventAllEvents. + @result An array of selector names as NSString objects, or nil if there are no action selectors associated with controlEvent. + */ +- (nullable NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns all target objects associated with the receiver. + @result A set of all targets for the receiver. The set may include NSNull to indicate at least one nil target (meaning, the responder chain is searched for a target.) + */ +- (NSSet *)allTargets AS_WARN_UNUSED_RESULT; + +/** + @abstract Removes a target-action pair for a particular event. + @param target The target object. Pass nil to remove all targets paired with action and the specified control events. + @param action A selector identifying an action message. Pass NULL to remove all action messages paired with target. + @param controlEvents A bitmask specifying the control events associated with target and action. See "Control Events" for bitmask constants. May not be 0. + */ +- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(ASControlNodeEvent)controlEvents; + +/** + @abstract Sends the actions for the control events for a particular event. + @param controlEvents A bitmask specifying the control events for which to send actions. See "Control Events" for bitmask constants. May not be 0. + @param event The event which triggered these control actions. May be nil. + */ +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)event; +@end + +#if TARGET_OS_TV +@interface ASControlNode (tvOS) + +/** + @abstract How the node looks when it isn't focused. Exposed here so that subclasses can override. + */ +- (void)setDefaultFocusAppearance; + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASControlNode.mm b/submodules/AsyncDisplayKit/Source/ASControlNode.mm new file mode 100644 index 0000000000..1256d2d629 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASControlNode.mm @@ -0,0 +1,516 @@ +// +// ASControlNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// UIControl allows dragging some distance outside of the control itself during +// tracking. This value depends on the device idiom (25 or 70 points), so +// so replicate that effect with the same values here for our own controls. +#define kASControlNodeExpandedInset (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? -25.0f : -70.0f) + +// Initial capacities for dispatch tables. +#define kASControlNodeEventDispatchTableInitialCapacity 4 +#define kASControlNodeActionDispatchTableInitialCapacity 4 + +@interface ASControlNode () +{ +@private + // Control Attributes + BOOL _enabled; + BOOL _highlighted; + + // Tracking + BOOL _tracking; + BOOL _touchInside; + + // Target action pairs stored in an array for each event type + // ASControlEvent -> [ASTargetAction0, ASTargetAction1] + NSMutableDictionary, NSMutableArray *> *_controlEventDispatchTable; +} + +// Read-write overrides. +@property (getter=isTracking) BOOL tracking; +@property (getter=isTouchInside) BOOL touchInside; + +/** + @abstract Returns a key to be used in _controlEventDispatchTable that identifies the control event. + @param controlEvent A control event. + @result A key for use in _controlEventDispatchTable. + */ +id _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent); + +/** + @abstract Enumerates the ASControlNode events included mask, invoking the block for each event. + @param mask An ASControlNodeEvent mask. + @param block The block to be invoked for each ASControlNodeEvent included in mask. + */ +void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent)); + +/** + @abstract Returns the expanded bounds used to determine if a touch is considered 'inside' during tracking. + @param controlNode A control node. + @result The expanded bounds of the node. + */ +CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode); + + +@end + +@implementation ASControlNode +{ + ASImageNode *_debugHighlightOverlay; +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _enabled = YES; + + // As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on. + self.userInteractionEnabled = NO; + + return self; +} + +#if TARGET_OS_TV +- (void)didLoad +{ + [super didLoad]; + + // On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them. + // Here we add our own internal tap gesture to handle this behaviour. + self.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_pressDown)]; + tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)]; + [self.view addGestureRecognizer:tapGestureRec]; +} +#endif + +- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled +{ + [super setUserInteractionEnabled:userInteractionEnabled]; + self.isAccessibilityElement = userInteractionEnabled; +} + +- (void)__exitHierarchy +{ + [super __exitHierarchy]; + + // If a control node is exit the hierarchy and is tracking we have to cancel it + if (self.tracking) { + [self _cancelTrackingWithEvent:nil]; + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" + +#pragma mark - ASDisplayNode Overrides + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + // Check if the tracking should start + UITouch *theTouch = [touches anyObject]; + if (![self beginTrackingWithTouch:theTouch withEvent:event]) { + return; + } + + // If we get more than one touch down on us, cancel. + // Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation. + if (touches.count > 1 || self.tracking) { + [self _cancelTrackingWithEvent:event]; + } else { + // Otherwise, begin tracking. + self.tracking = YES; + + // No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds. + self.touchInside = YES; + self.highlighted = YES; + + // Send the appropriate touch-down control event depending on how many times we've been tapped. + ASControlNodeEvent controlEventMask = (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat; + [self sendActionsForControlEvents:controlEventMask withEvent:event]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + NSParameterAssert(touches.count == 1); + UITouch *theTouch = [touches anyObject]; + + // Check if tracking should continue + if (!self.tracking || ![self continueTrackingWithTouch:theTouch withEvent:event]) { + self.tracking = NO; + return; + } + + CGPoint touchLocation = [theTouch locationInView:self.view]; + + // Update our touchInside state. + BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil]; + + // Update our highlighted state. + CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self); + BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); + self.touchInside = dragIsInsideExpandedBounds; + self.highlighted = dragIsInsideExpandedBounds; + + [self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside) + withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + // Note that we've cancelled tracking. + [self _cancelTrackingWithEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + // On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent: + // twice on the view for one call to -touchesBegan:withEvent:. On ASControlNode, it used to + // trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking + // state in order to have a correct behavior. + // It might be related to that issue: http://www.openradar.me/22910171 + if (!self.tracking) { + return; + } + + NSParameterAssert([touches count] == 1); + UITouch *theTouch = [touches anyObject]; + CGPoint touchLocation = [theTouch locationInView:self.view]; + + // Update state. + self.tracking = NO; + self.touchInside = NO; + self.highlighted = NO; + + // Note that we've ended tracking. + [self endTrackingWithTouch:theTouch withEvent:event]; + + // Send the appropriate touch-up control event. + CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self); + BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); + + [self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside) + withEvent:event]; +} + +- (void)_cancelTrackingWithEvent:(UIEvent *)event +{ + // We're no longer tracking and there is no touch to be inside. + self.tracking = NO; + self.touchInside = NO; + self.highlighted = NO; + + // Send the cancel event. + [self sendActionsForControlEvents:ASControlNodeEventTouchCancel withEvent:event]; +} + +#pragma clang diagnostic pop + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + // If not enabled we should not care about receving touches + if (! self.enabled) { + return nil; + } + + return [super hitTest:point withEvent:event]; +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir. + if (self.enabled && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && gestureRecognizer.view != self.view) { + UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *)gestureRecognizer; + // Allow double-tap gestures + return tapRecognizer.numberOfTapsRequired != 1; + } + + // Otherwise, go ahead. :] + return YES; +} + +- (BOOL)supportsLayerBacking +{ + return super.supportsLayerBacking && !self.userInteractionEnabled; +} + +#pragma mark - Action Messages + +- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask +{ + NSParameterAssert(action); + NSParameterAssert(controlEventMask != 0); + + // ASControlNode cannot be layer backed if adding a target + ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair."); + + ASLockScopeSelf(); + + if (!_controlEventDispatchTable) { + _controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries. + + // only show tap-able areas for views with 1 or more addTarget:action: pairs + if ([ASControlNode enableHitTestDebug] && _debugHighlightOverlay == nil) { + // do not use ASPerformBlockOnMainThread here, if it performs the block synchronously it will continue + // holding the lock while calling addSubnode. + dispatch_async(dispatch_get_main_queue(), ^{ + // add a highlight overlay node with area of ASControlNode + UIEdgeInsets + self.clipsToBounds = NO; + _debugHighlightOverlay = [[ASImageNode alloc] init]; + _debugHighlightOverlay.zPosition = 1000; // ensure we're over the top of any siblings + _debugHighlightOverlay.layerBacked = YES; + [self addSubnode:_debugHighlightOverlay]; + }); + } + } + + // Create new target action pair + ASControlTargetAction *targetAction = [[ASControlTargetAction alloc] init]; + targetAction.action = action; + targetAction.target = target; + + // Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ + (ASControlNodeEvent controlEvent) + { + // Do we already have an event table for this control event? + id eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent); + NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey]; + + if (!eventTargetActionArray) { + eventTargetActionArray = [[NSMutableArray alloc] init]; + } + + // Remove any prior target-action pair for this event, as UIKit does. + [eventTargetActionArray removeObject:targetAction]; + + // Register the new target-action as the last one to be sent. + [eventTargetActionArray addObject:targetAction]; + + if (eventKey) { + [_controlEventDispatchTable setObject:eventTargetActionArray forKey:eventKey]; + } + }); + + self.userInteractionEnabled = YES; +} + +- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent +{ + NSParameterAssert(target); + NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents); + + ASLockScopeSelf(); + + // Grab the event target action array for this event. + NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]; + if (!eventTargetActionArray) { + return nil; + } + + NSMutableArray *actions = [[NSMutableArray alloc] init]; + + // Collect all actions for this target. + for (ASControlTargetAction *targetAction in eventTargetActionArray) { + if ((target == nil && targetAction.createdWithNoTarget) || (target != nil && target == targetAction.target)) { + [actions addObject:NSStringFromSelector(targetAction.action)]; + } + } + + return actions; +} + +- (NSSet *)allTargets +{ + ASLockScopeSelf(); + + NSMutableSet *targets = [[NSMutableSet alloc] init]; + + // Look at each event... + for (NSMutableArray *eventTargetActionArray in [_controlEventDispatchTable objectEnumerator]) { + // and each event's targets... + for (ASControlTargetAction *targetAction in eventTargetActionArray) { + [targets addObject:targetAction.target]; + } + } + + return targets; +} + +- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask +{ + NSParameterAssert(controlEventMask != 0); + + ASLockScopeSelf(); + + // Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask. + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ + (ASControlNodeEvent controlEvent) + { + // Grab the dispatch table for this event (if we have it). + id eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent); + NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey]; + if (!eventTargetActionArray) { + return; + } + + NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + if (!target || evaluatedObject.target == target) { + if (!action) { + return NO; + } else if (evaluatedObject.action == action) { + return NO; + } + } + + return YES; + }]; + [eventTargetActionArray filterUsingPredicate:filterPredicate]; + + if (eventTargetActionArray.count == 0) { + // If there are no targets for this event anymore, remove it. + [_controlEventDispatchTable removeObjectForKey:eventKey]; + } + }); +} + +#pragma mark - + +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); //We access self.view below, it's not safe to call this off of main. + NSParameterAssert(controlEvents != 0); + + NSMutableArray *resolvedEventTargetActionArray = [[NSMutableArray alloc] init]; + + { + ASLockScopeSelf(); + + // Enumerate the events in the mask, invoking the target-action pairs for each. + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^ + (ASControlNodeEvent controlEvent) + { + // Iterate on each target action pair + for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) { + ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init]; + resolvedTargetAction.action = targetAction.action; + resolvedTargetAction.target = targetAction.target; + + // NSNull means that a nil target was set, so start at self and travel the responder chain + if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) { + // if the target cannot perform the action, travel the responder chain to try to find something that does + resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self]; + } + + if (resolvedTargetAction.target) { + [resolvedEventTargetActionArray addObject:resolvedTargetAction]; + } + } + }); + } + + //We don't want to hold the lock while calling out, we could potentially walk up the ownership tree causing a deadlock. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + for (ASControlTargetAction *targetAction in resolvedEventTargetActionArray) { + [targetAction.target performSelector:targetAction.action withObject:self withObject:event]; + } +#pragma clang diagnostic pop +} + +#pragma mark - Convenience + +id _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent) +{ + return @(controlEvent); +} + +void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent)) +{ + if (block == nil) { + return; + } + // Start with our first event (touch down) and work our way up to the last event (PrimaryActionTriggered) + for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventPrimaryActionTriggered; thisEvent <<= 1) { + // If it's included in the mask, invoke the block. + if ((mask & thisEvent) == thisEvent) + block(thisEvent); + } +} + +CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode) { + return CGRectInset(UIEdgeInsetsInsetRect(controlNode.view.bounds, controlNode.hitTestSlop), kASControlNodeExpandedInset, kASControlNodeExpandedInset); +} + +#pragma mark - For Subclasses + +- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ + return YES; +} + +- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ + return YES; +} + +- (void)cancelTrackingWithEvent:(UIEvent *)touchEvent +{ + // Subclass hook +} + +- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ + // Subclass hook +} + +#pragma mark - Debug +- (ASImageNode *)debugHighlightOverlay +{ + return _debugHighlightOverlay; +} +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Beta.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Beta.h new file mode 100644 index 0000000000..2bb2a4c932 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Beta.h @@ -0,0 +1,191 @@ +// +// ASDisplayNode+Beta.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#if YOGA + #import YOGA_HEADER_PATH + #import + #import +#endif + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN void ASPerformBlockOnMainThread(void (^block)(void)); +AS_EXTERN void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT + +#if ASEVENTLOG_ENABLE + #define ASDisplayNodeLogEvent(node, ...) [node.eventLog logEventWithBacktrace:(AS_SAVE_EVENT_BACKTRACES ? [NSThread callStackSymbols] : nil) format:__VA_ARGS__] +#else + #define ASDisplayNodeLogEvent(node, ...) +#endif + +#if ASEVENTLOG_ENABLE + #define ASDisplayNodeGetEventLog(node) node.eventLog +#else + #define ASDisplayNodeGetEventLog(node) nil +#endif + +/** + * Bitmask to indicate what performance measurements the cell should record. + */ +typedef NS_OPTIONS(NSUInteger, ASDisplayNodePerformanceMeasurementOptions) { + ASDisplayNodePerformanceMeasurementOptionLayoutSpec = 1 << 0, + ASDisplayNodePerformanceMeasurementOptionLayoutComputation = 1 << 1 +}; + +typedef struct { + CFTimeInterval layoutSpecTotalTime; + NSInteger layoutSpecNumberOfPasses; + CFTimeInterval layoutComputationTotalTime; + NSInteger layoutComputationNumberOfPasses; +} ASDisplayNodePerformanceMeasurements; + +@interface ASDisplayNode (Beta) + +/** + * ASTableView and ASCollectionView now throw exceptions on invalid updates + * like their UIKit counterparts. If YES, these classes will log messages + * on invalid updates rather than throwing exceptions. + * + * Note that even if AsyncDisplayKit's exception is suppressed, the app may still crash + * as it proceeds with an invalid update. + * + * This property defaults to NO. It will be removed in a future release. + */ ++ (BOOL)suppressesInvalidCollectionUpdateExceptions AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled."); ++ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses; + +/** + * @abstract Recursively ensures node and all subnodes are displayed. + * @see Full documentation in ASDisplayNode+FrameworkPrivate.h + */ +- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously; + +/** + * @abstract allow modification of a context before the node's content is drawn + * + * @discussion Set the block to be called after the context has been created and before the node's content is drawn. + * You can override this to modify the context before the content is drawn. You are responsible for saving and + * restoring context if necessary. Restoring can be done in contextDidDisplayNodeContent + * This block can be called from *any* thread and it is unsafe to access any UIKit main thread properties from it. + */ +@property (nullable) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext; + +/** + * @abstract allow modification of a context after the node's content is drawn + */ +@property (nullable) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext; + +/** + * @abstract A bitmask representing which actions (layout spec, layout generation) should be measured. + */ +@property ASDisplayNodePerformanceMeasurementOptions measurementOptions; + +/** + * @abstract A simple struct representing performance measurements collected. + */ +@property (readonly) ASDisplayNodePerformanceMeasurements performanceMeasurements; + +#if ASEVENTLOG_ENABLE +/* + * @abstract The primitive event tracing object. You shouldn't directly use it to log event. Use the ASDisplayNodeLogEvent macro instead. + */ +@property (nonatomic, readonly) ASEventLog *eventLog; +#endif + +/** + * @abstract Whether this node acts as an accessibility container. If set to YES, then this node's accessibility label will represent + * an aggregation of all child nodes' accessibility labels. Nodes in this node's subtree that are also accessibility containers will + * not be included in this aggregation, and will be exposed as separate accessibility elements to UIKit. + */ +@property BOOL isAccessibilityContainer; + +/** + * @abstract Returns the default accessibility property values set by Texture on this node. For + * example, the default accessibility label for a text node may be its text content, while most + * other nodes would have nil default labels. + */ +@property (nullable, readonly, copy) NSString *defaultAccessibilityLabel; +@property (nullable, readonly, copy) NSString *defaultAccessibilityHint; +@property (nullable, readonly, copy) NSString *defaultAccessibilityValue; +@property (nullable, readonly, copy) NSString *defaultAccessibilityIdentifier; +@property (readonly) UIAccessibilityTraits defaultAccessibilityTraits; + +/** + * @abstract Invoked when a user performs a custom action on an accessible node. Nodes that are children of accessibility containers, have + * an accessibity label and have an interactive UIAccessibilityTrait will automatically receive custom-action handling. + * + * @return Return a boolean value that determine whether to propagate through the responder chain. + * To halt propagation, return YES; otherwise, return NO. + */ +- (BOOL)performAccessibilityCustomAction:(UIAccessibilityCustomAction *)action; + +/** + * @abstract Currently used by ASNetworkImageNode and ASMultiplexImageNode to allow their placeholders to stay if they are loading an image from the network. + * Otherwise, a display pass is scheduled and completes, but does not actually draw anything - and ASDisplayNode considers the element finished. + */ +- (BOOL)placeholderShouldPersist AS_WARN_UNUSED_RESULT; + +/** + * @abstract Indicates that the receiver and all subnodes have finished displaying. May be called more than once, for example if the receiver has + * a network image node. This is called after the first display pass even if network image nodes have not downloaded anything (text would be done, + * and other nodes that are ready to do their final display). Each render of every progressive jpeg network node would cause this to be called, so + * this hook could be called up to 1 + (pJPEGcount * pJPEGrenderCount) times. The render count depends on how many times the downloader calls the + * progressImage block. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)hierarchyDisplayDidFinish NS_REQUIRES_SUPER; + +/** + * Only called on the root during yoga layout. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)willCalculateLayout:(ASSizeRange)constrainedSize NS_REQUIRES_SUPER; + +/** + * Only ASLayoutRangeModeVisibleOnly or ASLayoutRangeModeLowMemory are recommended. Default is ASLayoutRangeModeVisibleOnly, + * because this is the only way to ensure an application will not have blank / flashing views as the user navigates back after + * a memory warning. Apps that wish to use the more effective / aggressive ASLayoutRangeModeLowMemory may need to take steps + * to mitigate this behavior, including: restoring a larger range mode to the next controller before the user navigates there, + * enabling .neverShowPlaceholders on ASCellNodes so that the navigation operation is blocked on redisplay completing, etc. + */ ++ (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode; + +/** + * @abstract Whether to draw all descendent nodes' contents into this node's layer's backing store. + * + * @discussion + * When called, causes all descendent nodes' contents to be drawn directly into this node's layer's backing + * store. + * + * If a node's descendants are static (never animated or never change attributes after creation) then that node is a + * good candidate for rasterization. Rasterizing descendants has two main benefits: + * 1) Backing stores for descendant layers are not created. Instead the layers are drawn directly into the rasterized + * container. This can save a great deal of memory. + * 2) Since the entire subtree is drawn into one backing store, compositing and blending are eliminated in that subtree + * which can help improve animation/scrolling/etc performance. + * + * Rasterization does not currently support descendants with transform, sublayerTransform, or alpha. Those properties + * will be ignored when rasterizing descendants. + * + * Note: this has nothing to do with -[CALayer shouldRasterize], which doesn't work with ASDisplayNode's asynchronous + * rendering model. + * + * Note: You cannot add subnodes whose layers/views are already loaded to a rasterized node. + * Note: You cannot call this method after the receiver's layer/view is loaded. + */ +- (void)enableSubtreeRasterization; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.h new file mode 100644 index 0000000000..3c00f67213 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.h @@ -0,0 +1,28 @@ +// +// ASDisplayNode+Convenience.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UIViewController; + +@interface ASDisplayNode (Convenience) + +/** + * @abstract Returns the view controller nearest to this node in the view hierarchy. + * + * @warning This property may only be accessed on the main thread. This property may + * be @c nil until the node's view is actually hosted in the view hierarchy. + */ +@property (nonatomic, nullable, readonly) __kindof UIViewController *closestViewController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm new file mode 100644 index 0000000000..42ae4e4a72 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm @@ -0,0 +1,40 @@ +// +// ASDisplayNode+Convenience.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASDisplayNode+Convenience.h" + +#import + +#import +#import + +@implementation ASDisplayNode (Convenience) + +- (__kindof UIViewController *)closestViewController +{ + ASDisplayNodeAssertMainThread(); + + // Careful not to trigger node loading here. + if (!self.nodeLoaded) { + return nil; + } + + // Get the closest view. + UIView *view = ASFindClosestViewOfLayer(self.layer); + // Travel up the responder chain to find a view controller. + for (UIResponder *responder in [view asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc != nil) { + return vc; + } + } + return nil; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h new file mode 100644 index 0000000000..b05390ea47 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h @@ -0,0 +1,142 @@ +// +// ASDisplayNode+Deprecated.h +// Texture +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import + +@interface ASDisplayNode (Deprecated) + +/** + * @abstract The name of this node, which will be displayed in `description`. The default value is nil. + * + * @deprecated Deprecated in version 2.0: Use .debugName instead. This value will display in + * results of the -asciiArtString method (@see ASLayoutElementAsciiArtProtocol). + */ +@property (nullable, nonatomic, copy) NSString *name ASDISPLAYNODE_DEPRECATED_MSG("Use .debugName instead."); + +/** + * @abstract Provides a default intrinsic content size for calculateSizeThatFits:. This is useful when laying out + * a node that either has no intrinsic content size or should be laid out at a different size than its intrinsic content + * size. For example, this property could be set on an ASImageNode to display at a size different from the underlying + * image size. + * + * @return Try to create a CGSize for preferredFrameSize of this node from the width and height property of this node. It will return CGSizeZero if width and height dimensions are not of type ASDimensionUnitPoints. + * + * @deprecated Deprecated in version 2.0: Just calls through to set the height and width property of the node. Convert to use sizing properties instead: height, minHeight, maxHeight, width, minWidth, maxWidth. + */ +@property (nonatomic, assign, readwrite) CGSize preferredFrameSize ASDISPLAYNODE_DEPRECATED_MSG("Use .style.preferredSize instead OR set individual values with .style.height and .style.width."); + +/** + * @abstract Asks the node to measure and return the size that best fits its subnodes. + * + * @param constrainedSize The maximum size the receiver should fit in. + * + * @return A new size that fits the receiver's subviews. + * + * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the + * constraint and the result. + * + * @warning Subclasses must not override this; it calls -measureWithSizeRange: with zero min size. + * -measureWithSizeRange: caches results from -calculateLayoutThatFits:. Calling this method may + * be expensive if result is not cached. + * + * @see measureWithSizeRange: + * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] + * + * @deprecated Deprecated in version 2.0: Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout + */ +- (CGSize)measure:(CGSize)constrainedSize/* ASDISPLAYNODE_DEPRECATED_MSG("Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout.")*/; + +ASLayoutElementStyleForwardingDeclaration + +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + * + * @deprecated @see didEnterVisibleState @see didExitVisibleState + */ +- (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead."); + +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + * + * @deprecated @see didEnterVisibleState @see didExitVisibleState + */ +- (void)visibleStateDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead."); + +/** + * @abstract Called whenever the the node has entered or exited the display state. + * + * @discussion Subclasses may use this to monitor when a node should be rendering its content. + * + * @note This method can be called from any thread and should therefore be thread safe. + * + * @deprecated @see didEnterDisplayState @see didExitDisplayState + */ +- (void)displayStateDidChange:(BOOL)inDisplayState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterDisplayState / -didExitDisplayState instead."); + +/** + * @abstract Called whenever the the node has entered or left the load state. + * + * @discussion Subclasses may use this to monitor data for a node should be loaded, either from a local or remote source. + * + * @note This method can be called from any thread and should therefore be thread safe. + * + * @deprecated @see didEnterPreloadState @see didExitPreloadState + */ +- (void)loadStateDidChange:(BOOL)inLoadState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState / -didExitPreloadState instead."); + +/** + * @abstract Cancels all performing layout transitions. Can be called on any thread. + * + * @deprecated Deprecated in version 2.0: Use cancelLayoutTransition + */ +- (void)cancelLayoutTransitionsInProgress ASDISPLAYNODE_DEPRECATED_MSG("Use -cancelLayoutTransition instead."); + +/** + * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or + * absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method. + * + * @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence + * or absence of subnodes is completely determined in its layoutSpecThatFits: method. + * + * @deprecated Deprecated in version 2.0: Use automaticallyManagesSubnodes + */ +@property (nonatomic, assign) BOOL usesImplicitHierarchyManagement ASDISPLAYNODE_DEPRECATED_MSG("Set .automaticallyManagesSubnodes instead."); + +/** + * @abstract Indicates that the node should fetch any external data, such as images. + * + * @discussion Subclasses may override this method to be notified when they should begin to preload. Fetching + * should be done asynchronously. The node is also responsible for managing the memory of any data. + * The data may be remote and accessed via the network, but could also be a local database query. + */ +- (void)fetchData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState instead."); + +/** + * Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node. + * + * @discussion This will not clear data recursively for all subnodes. Either call -recursivelyClearPreloadedData or + * selectively clear fetched data. + */ +- (void)clearFetchedData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didExitPreloadState instead."); + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+InterfaceState.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+InterfaceState.h new file mode 100644 index 0000000000..1de83700ac --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+InterfaceState.h @@ -0,0 +1,130 @@ +// +// ASDisplayNode+InterfaceState.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Interface state is available on ASDisplayNode and ASViewController, and + * allows checking whether a node is in an interface situation where it is prudent to trigger certain + * actions: measurement, data loading, display, and visibility (the latter for animations or other onscreen-only effects). + * + * The defualt state, ASInterfaceStateNone, means that the element is not predicted to be onscreen soon and + * preloading should not be performed. Swift: use [] for the default behavior. + */ +typedef NS_OPTIONS(NSUInteger, ASInterfaceState) +{ + /** The element is not predicted to be onscreen soon and preloading should not be performed */ + ASInterfaceStateNone = 0, + /** The element may be added to a view soon that could become visible. Measure the layout, including size calculation. */ + ASInterfaceStateMeasureLayout = 1 << 0, + /** The element is likely enough to come onscreen that disk and/or network data required for display should be fetched. */ + ASInterfaceStatePreload = 1 << 1, + /** The element is very likely to become visible, and concurrent rendering should be executed for any -setNeedsDisplay. */ + ASInterfaceStateDisplay = 1 << 2, + /** The element is physically onscreen by at least 1 pixel. + In practice, all other bit fields should also be set when this flag is set. */ + ASInterfaceStateVisible = 1 << 3, + + /** + * The node is not contained in a cell but it is in a window. + * + * Currently we only set `interfaceState` to other values for + * nodes contained in table views or collection views. + */ + ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible, +}; + +@protocol ASInterfaceStateDelegate + +/** + * @abstract Called whenever any bit in the ASInterfaceState bitfield is changed. + * @discussion Subclasses may use this to monitor when they become visible, should free cached data, and much more. + * @see ASInterfaceState + */ +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState; + +/** + * @abstract Called whenever the node becomes visible. + * @discussion Subclasses may use this to monitor when they become visible. + * @note This method is guaranteed to be called on main. + */ +- (void)didEnterVisibleState; + +/** + * @abstract Called whenever the node is no longer visible. + * @discussion Subclasses may use this to monitor when they are no longer visible. + * @note This method is guaranteed to be called on main. + */ +- (void)didExitVisibleState; + +/** + * @abstract Called whenever the the node has entered the display state. + * @discussion Subclasses may use this to monitor when a node should be rendering its content. + * @note This method is guaranteed to be called on main. + */ +- (void)didEnterDisplayState; + +/** + * @abstract Called whenever the the node has exited the display state. + * @discussion Subclasses may use this to monitor when a node should no longer be rendering its content. + * @note This method is guaranteed to be called on main. + */ +- (void)didExitDisplayState; + +/** + * @abstract Called whenever the the node has entered the preload state. + * @discussion Subclasses may use this to monitor data for a node should be preloaded, either from a local or remote source. + * @note This method is guaranteed to be called on main. + */ +- (void)didEnterPreloadState; + +/** + * @abstract Called whenever the the node has exited the preload state. + * @discussion Subclasses may use this to monitor whether preloading data for a node should be canceled. + * @note This method is guaranteed to be called on main. + */ +- (void)didExitPreloadState; + +/** + * @abstract Called when the node has completed applying the layout. + * @discussion Can be used for operations that are performed after layout has completed. + * @note This method is guaranteed to be called on main. + */ +- (void)nodeDidLayout; + +/** + * @abstract Called when the node loads. + * @discussion Can be used for operations that are performed after the node's view is available. + * @note This method is guaranteed to be called on main. + */ +- (void)nodeDidLoad; + +/** + * @abstract Indicates that the receiver and all subnodes have finished displaying. + * @discussion May be called more than once, for example if the receiver has a network image node. + * This is called after the first display pass even if network image nodes have not downloaded anything + * (text would be done, and other nodes that are ready to do their final display). Each render of + * every progressive jpeg network node would cause this to be called, so this hook could be called up to + * 1 + (pJPEGcount * pJPEGrenderCount) times. The render count depends on how many times the downloader calls + * the progressImage block. + * @note This method is guaranteed to be called on main. + */ +- (void)hierarchyDisplayDidFinish; + +@optional +/** + * @abstract Called when the node is about to calculate layout. This is only called before + * Yoga-driven layouts. + * @discussion Can be used for operations that are performed after the node's view is available. + * @note This method is guaranteed to be called on main, but implementations should be careful not + * to attempt to ascend the node tree when handling this, as the root node is locked when this is + * called. + */ +- (void)nodeWillCalculateLayout:(ASSizeRange)constrainedSize; + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm new file mode 100644 index 0000000000..b7956541b4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm @@ -0,0 +1,1063 @@ +// +// ASDisplayNode+Layout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +using AS::MutexLocker; + +#pragma mark - ASDisplayNode (ASLayoutElement) + +@implementation ASDisplayNode (ASLayoutElement) + +#pragma mark + +- (BOOL)implementsLayoutMethod +{ + MutexLocker l(__instanceLock__); + return (_methodOverrides & (ASDisplayNodeMethodOverrideLayoutSpecThatFits | + ASDisplayNodeMethodOverrideCalcLayoutThatFits | + ASDisplayNodeMethodOverrideCalcSizeThatFits)) != 0 || _layoutSpecBlock != nil; +} + + +- (ASLayoutElementStyle *)style +{ + MutexLocker l(__instanceLock__); + return [self _locked_style]; +} + +- (ASLayoutElementStyle *)_locked_style +{ + if (_style == nil) { + _style = [[ASLayoutElementStyle alloc] init]; + } + return _style; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeDisplayNode; +} + +- (NSArray> *)sublayoutElements +{ + return self.subnodes; +} + +#pragma mark Measurement Pass + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize +{ + return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; +} + +- (CGSize)measure:(CGSize)constrainedSize { + return [self layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; +} + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize +{ + ASScopedLockSelfOrToRoot(); + + // If one or multiple layout transitions are in flight it still can happen that layout information is requested + // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a + // layout calculation wil be performed without side effect + if ([self _isLayoutTransitionInvalid]) { + return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; + } + + ASLayout *layout = nil; + NSUInteger version = _layoutVersion; + if (_calculatedDisplayNodeLayout.isValid(constrainedSize, parentSize, version)) { + ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout.layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _calculatedDisplayNodeLayout.layout should not be nil! %@", self); + layout = _calculatedDisplayNodeLayout.layout; + } else if (_pendingDisplayNodeLayout.isValid(constrainedSize, parentSize, version)) { + ASDisplayNodeAssertNotNil(_pendingDisplayNodeLayout.layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _pendingDisplayNodeLayout.layout should not be nil! %@", self); + layout = _pendingDisplayNodeLayout.layout; + } else { + // Create a pending display node layout for the layout pass + layout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:parentSize]; + as_log_verbose(ASLayoutLog(), "Established pending layout for %@ in %s", self, sel_getName(_cmd)); + _pendingDisplayNodeLayout = ASDisplayNodeLayout(layout, constrainedSize, parentSize,version); + ASDisplayNodeAssertNotNil(layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newly calculated layout should not be nil! %@", self); + } + + return layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; +} + +#pragma mark ASLayoutElementStyleExtensibility + +ASLayoutElementStyleExtensibilityForwarding + +#pragma mark ASPrimitiveTraitCollection + +- (ASPrimitiveTraitCollection)primitiveTraitCollection +{ + return _primitiveTraitCollection.load(); +} + +- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection +{ + if (ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, _primitiveTraitCollection.load()) == NO) { + _primitiveTraitCollection = traitCollection; + ASDisplayNodeLogEvent(self, @"asyncTraitCollectionDidChange: %@", NSStringFromASPrimitiveTraitCollection(traitCollection)); + + [self asyncTraitCollectionDidChange]; + } +} + +- (ASTraitCollection *)asyncTraitCollection +{ + return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + NSMutableString *result = [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; + if (_debugName) { + [result appendFormat:@" (%@)", _debugName]; + } + return result; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayout) + +@implementation ASDisplayNode (ASLayout) + +- (ASLayoutEngineType)layoutEngineType +{ +#if YOGA + MutexLocker l(__instanceLock__); + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + if (yogaNode != NULL && (hasYogaParent || hasYogaChildren)) { + return ASLayoutEngineTypeYoga; + } +#endif + + return ASLayoutEngineTypeLayoutSpec; +} + +- (ASLayout *)calculatedLayout +{ + MutexLocker l(__instanceLock__); + return _calculatedDisplayNodeLayout.layout; +} + +- (CGSize)calculatedSize +{ + MutexLocker l(__instanceLock__); + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { + return _pendingDisplayNodeLayout.layout.size; + } + return _calculatedDisplayNodeLayout.layout.size; +} + +- (ASSizeRange)constrainedSizeForCalculatedLayout +{ + MutexLocker l(__instanceLock__); + return [self _locked_constrainedSizeForCalculatedLayout]; +} + +- (ASSizeRange)_locked_constrainedSizeForCalculatedLayout +{ + ASAssertLocked(__instanceLock__); + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { + return _pendingDisplayNodeLayout.constrainedSize; + } + return _calculatedDisplayNodeLayout.constrainedSize; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutElementStylability) + +@implementation ASDisplayNode (ASLayoutElementStylability) + +- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock +{ + styleBlock(self.style); + return self; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutInternal) + +@implementation ASDisplayNode (ASLayoutInternal) + +/** + * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. + * + * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know + * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. + */ +- (void)_u_setNeedsLayoutFromAbove +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + as_activity_create_for_scope("Set needs layout from above"); + + // Mark the node for layout in the next layout pass + [self setNeedsLayout]; + + __instanceLock__.lock(); + // Escalate to the root; entire tree must allow adjustments so the layout fits the new child. + // Much of the layout will be re-used as cached (e.g. other items in an unconstrained stack) + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + if (supernode) { + // Threading model requires that we unlock before calling a method on our parent. + [supernode _u_setNeedsLayoutFromAbove]; + } else { + // Let the root node method know that the size was invalidated + [self _rootNodeDidInvalidateSize]; + } +} + +// TODO It would be easier to work with if we could `ASAssertUnlocked` here, but we +// cannot due to locking to root in `_u_measureNodeWithBoundsIfNecessary`. +- (void)_rootNodeDidInvalidateSize +{ + ASDisplayNodeAssertThreadAffinity(self); + __instanceLock__.lock(); + + // We are the root node and need to re-flow the layout; at least one child needs a new size. + CGSize boundsSizeForLayout = ASCeilSizeValues(self.bounds.size); + + // Figure out constrainedSize to use + ASSizeRange constrainedSize = ASSizeRangeMake(boundsSizeForLayout); + if (_pendingDisplayNodeLayout.layout != nil) { + constrainedSize = _pendingDisplayNodeLayout.constrainedSize; + } else if (_calculatedDisplayNodeLayout.layout != nil) { + constrainedSize = _calculatedDisplayNodeLayout.constrainedSize; + } + + __instanceLock__.unlock(); + + // Perform a measurement pass to get the full tree layout, adapting to the child's new size. + ASLayout *layout = [self layoutThatFits:constrainedSize]; + + // Check if the returned layout has a different size than our current bounds. + if (CGSizeEqualToSize(boundsSizeForLayout, layout.size) == NO) { + // If so, inform our container we need an update (e.g Table, Collection, ViewController, etc). + [self displayNodeDidInvalidateSizeNewSize:layout.size]; + } +} + +// TODO +// We should remove this logic, which is relatively new, and instead +// rely on the parent / host of the root node to do this size change. That's always been the +// expectation with other node containers like ASTableView, ASCollectionView, ASViewController, etc. +// E.g. in ASCellNode the _interactionDelegate is a Table or Collection that will resize in this +// case. By resizing without participating with the parent, we could get cases where our parent size +// does not match, especially if there is a size constraint that is applied at that level. +// +// In general a node should never need to set its own size, instead allowing its parent to do so - +// even in the root case. Anyhow this is a separate / pre-existing issue, but I think it could be +// causing real issues in cases of resizing nodes. +- (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size +{ + ASDisplayNodeAssertThreadAffinity(self); + + // The default implementation of display node changes the size of itself to the new size + CGRect oldBounds = self.bounds; + CGSize oldSize = oldBounds.size; + CGSize newSize = size; + + if (! CGSizeEqualToSize(oldSize, newSize)) { + self.bounds = (CGRect){ oldBounds.origin, newSize }; + + // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint + // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. + CGPoint anchorPoint = self.anchorPoint; + CGPoint oldPosition = self.position; + CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; + CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; + self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); + } +} + +- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds +{ + // ASAssertUnlocked(__instanceLock__); + ASScopedLockSelfOrToRoot(); + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as it's part of the layout transition + if ([self _locked_isLayoutTransitionInvalid]) { + return; + } + + CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); + + // Prefer a newer and not yet applied _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout + // If there is no such _pending, check if _calculated is valid to reuse (avoiding recalculation below). + BOOL pendingLayoutIsPreferred = NO; + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { + NSUInteger calculatedVersion = _calculatedDisplayNodeLayout.version; + NSUInteger pendingVersion = _pendingDisplayNodeLayout.version; + if (pendingVersion > calculatedVersion) { + pendingLayoutIsPreferred = YES; // Newer _pending + } else if (pendingVersion == calculatedVersion + && !ASSizeRangeEqualToSizeRange(_pendingDisplayNodeLayout.constrainedSize, + _calculatedDisplayNodeLayout.constrainedSize)) { + pendingLayoutIsPreferred = YES; // _pending with a different constrained size + } + } + BOOL calculatedLayoutIsReusable = (_calculatedDisplayNodeLayout.isValid(_layoutVersion) + && (_calculatedDisplayNodeLayout.requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout.layout.size, boundsSizeForLayout))); + if (!pendingLayoutIsPreferred && calculatedLayoutIsReusable) { + return; + } + + as_activity_create_for_scope("Update node layout for current bounds"); + as_log_verbose(ASLayoutLog(), "Node %@, bounds size %@, calculatedSize %@, calculatedIsDirty %d", + self, + NSStringFromCGSize(boundsSizeForLayout), + NSStringFromCGSize(_calculatedDisplayNodeLayout.layout.size), + _calculatedDisplayNodeLayout.version < _layoutVersion); + // _calculatedDisplayNodeLayout is not reusable we need to transition to a new one + [self cancelLayoutTransition]; + + BOOL didCreateNewContext = NO; + ASLayoutElementContext *context = ASLayoutElementGetCurrentContext(); + if (context == nil) { + context = [[ASLayoutElementContext alloc] init]; + ASLayoutElementPushContext(context); + didCreateNewContext = YES; + } + + // Figure out previous and pending layouts for layout transition + ASDisplayNodeLayout nextLayout = _pendingDisplayNodeLayout; + #define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout.layout.size, boundsSizeForLayout) + + // nextLayout was likely created by a call to layoutThatFits:, check if it is valid and can be applied. + // If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr-> + BOOL pendingLayoutApplicable = NO; + if (nextLayout.layout == nil) { + as_log_verbose(ASLayoutLog(), "No pending layout."); + } else if (!nextLayout.isValid(_layoutVersion)) { + as_log_verbose(ASLayoutLog(), "Pending layout is stale."); + } else if (layoutSizeDifferentFromBounds) { + as_log_verbose(ASLayoutLog(), "Pending layout size %@ doesn't match bounds size.", NSStringFromCGSize(nextLayout.layout.size)); + } else { + as_log_verbose(ASLayoutLog(), "Using pending layout %@.", nextLayout.layout); + pendingLayoutApplicable = YES; + } + + if (!pendingLayoutApplicable) { + as_log_verbose(ASLayoutLog(), "Measuring with previous constrained size."); + // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). + NSUInteger version = _layoutVersion; + ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; + ASLayout *layout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:boundsSizeForLayout]; + nextLayout = ASDisplayNodeLayout(layout, constrainedSize, boundsSizeForLayout, version); + // Now that the constrained size of pending layout might have been reused, the layout is useless + // Release it and any orphaned subnodes it retains + _pendingDisplayNodeLayout.layout = nil; + } + + if (didCreateNewContext) { + ASLayoutElementPopContext(); + } + + // If our new layout's desired size for self doesn't match current size, ask our parent to update it. + // This can occur for either pre-calculated or newly-calculated layouts. + if (nextLayout.requestedLayoutFromAbove == NO + && CGSizeEqualToSize(boundsSizeForLayout, nextLayout.layout.size) == NO) { + as_log_verbose(ASLayoutLog(), "Layout size doesn't match bounds size. Requesting layout from above."); + // The layout that we have specifies that this node (self) would like to be a different size + // than it currently is. Because that size has been computed within the constrainedSize, we + // expect that calling setNeedsLayoutFromAbove will result in our parent resizing us to this. + // However, in some cases apps may manually interfere with this (setting a different bounds). + // In this case, we need to detect that we've already asked to be resized to match this + // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. + nextLayout.requestedLayoutFromAbove = YES; + + { + __instanceLock__.unlock(); + [self _u_setNeedsLayoutFromAbove]; + __instanceLock__.lock(); + } + + // Update the layout's version here because _u_setNeedsLayoutFromAbove calls __setNeedsLayout which in turn increases _layoutVersion + // Failing to do this will cause the layout to be invalid immediately + nextLayout.version = _layoutVersion; + } + + // Prepare to transition to nextLayout + ASDisplayNodeAssertNotNil(nextLayout.layout, @"nextLayout.layout should not be nil! %@", self); + _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:nextLayout + previousLayout:_calculatedDisplayNodeLayout]; + + // If a parent is currently executing a layout transition, perform our layout application after it. + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { + // If no transition, apply our new layout immediately (common case). + [self _completePendingLayoutTransition]; + } +} + +- (ASSizeRange)_constrainedSizeForLayoutPass +{ + MutexLocker l(__instanceLock__); + return [self _locked_constrainedSizeForLayoutPass]; +} + +- (ASSizeRange)_locked_constrainedSizeForLayoutPass +{ + // TODO: The logic in -_u_setNeedsLayoutFromAbove seems correct and doesn't use this method. + // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? + // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK + + ASAssertLocked(__instanceLock__); + + CGSize boundsSizeForLayout = ASCeilSizeValues(self.threadSafeBounds.size); + + // Checkout if constrained size of pending or calculated display node layout can be used + if (_pendingDisplayNodeLayout.requestedLayoutFromAbove + || CGSizeEqualToSize(_pendingDisplayNodeLayout.layout.size, boundsSizeForLayout)) { + // We assume the size from the last returned layoutThatFits: layout was applied so use the pending display node + // layout constrained size + return _pendingDisplayNodeLayout.constrainedSize; + } else if (_calculatedDisplayNodeLayout.layout != nil + && (_calculatedDisplayNodeLayout.requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout.layout.size, boundsSizeForLayout))) { + // We assume the _calculatedDisplayNodeLayout is still valid and the frame is not different + return _calculatedDisplayNodeLayout.constrainedSize; + } else { + // In this case neither the _pendingDisplayNodeLayout or the _calculatedDisplayNodeLayout constrained size can + // be reused, so the current bounds is used. This is usual the case if a frame was set manually that differs to + // the one returned from layoutThatFits: or layoutThatFits: was never called + return ASSizeRangeMake(boundsSizeForLayout); + } +} + +- (void)_layoutSublayouts +{ + ASDisplayNodeAssertThreadAffinity(self); + // ASAssertUnlocked(__instanceLock__); + + ASLayout *layout; + { + MutexLocker l(__instanceLock__); + if (_calculatedDisplayNodeLayout.version < _layoutVersion) { + return; + } + layout = _calculatedDisplayNodeLayout.layout; + } + + for (ASDisplayNode *node in self.subnodes) { + CGRect frame = [layout frameForElement:node]; + if (CGRectIsNull(frame)) { + // There is no frame for this node in our layout. + // This currently can happen if we get a CA layout pass + // while waiting for the client to run animateLayoutTransition: + } else { + node.frame = frame; + } + } +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASAutomatic Subnode Management) + +@implementation ASDisplayNode (ASAutomaticSubnodeManagement) + +#pragma mark Automatically Manages Subnodes + +- (BOOL)automaticallyManagesSubnodes +{ + MutexLocker l(__instanceLock__); + return _automaticallyManagesSubnodes; +} + +- (void)setAutomaticallyManagesSubnodes:(BOOL)automaticallyManagesSubnodes +{ + MutexLocker l(__instanceLock__); + _automaticallyManagesSubnodes = automaticallyManagesSubnodes; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutTransition) + +@implementation ASDisplayNode (ASLayoutTransition) + +- (BOOL)_isLayoutTransitionInvalid +{ + MutexLocker l(__instanceLock__); + return [self _locked_isLayoutTransitionInvalid]; +} + +- (BOOL)_locked_isLayoutTransitionInvalid +{ + ASAssertLocked(__instanceLock__); + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASLayoutElementContext *context = ASLayoutElementGetCurrentContext(); + if (context == nil || _pendingTransitionID != context.transitionID) { + return YES; + } + } + return NO; +} + +/// Starts a new transition and returns the transition id +- (int32_t)_startNewTransition +{ + static std::atomic gNextTransitionID; + int32_t newTransitionID = gNextTransitionID.fetch_add(1) + 1; + _transitionID = newTransitionID; + return newTransitionID; +} + +/// Returns NO if there was no transition to cancel/finish. +- (BOOL)_finishOrCancelTransition +{ + int32_t oldValue = _transitionID.exchange(ASLayoutElementContextInvalidTransitionID); + return oldValue != ASLayoutElementContextInvalidTransitionID; +} + +#pragma mark Layout Transition + +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + [self transitionLayoutWithSizeRange:[self _constrainedSizeForLayoutPass] + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:completion]; +} + +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + as_activity_create_for_scope("Transition node layout"); + as_log_debug(ASLayoutLog(), "Transition layout for %@ sizeRange %@ anim %d asyncMeasure %d", self, NSStringFromASSizeRange(constrainedSize), animated, shouldMeasureAsync); + + if (constrainedSize.max.width <= 0.0 || constrainedSize.max.height <= 0.0) { + // Using CGSizeZero for the sizeRange can cause negative values in client layout code. + // Most likely called transitionLayout: without providing a size, before first layout pass. + as_log_verbose(ASLayoutLog(), "Ignoring transition due to bad size range."); + return; + } + + { + MutexLocker l(__instanceLock__); + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as we're part of the layout transition. + if ([self _locked_isLayoutTransitionInvalid]) { + return; + } + + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASDisplayNodeAssert(NO, @"Can't start a transition when one of the supernodes is performing one."); + return; + } + } + + // Invalidate calculated layout because this method acts as an animated "setNeedsLayout" for nodes. + // If the user has reconfigured the node and calls this, we should never return a stale layout + // for subsequent calls to layoutThatFits: regardless of size range. We choose this method rather than + // -setNeedsLayout because that method also triggers a CA layout invalidation, which isn't necessary at this time. + // See https://github.com/TextureGroup/Texture/issues/463 + [self invalidateCalculatedLayout]; + + // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling + int32_t transitionID = [self _startNewTransition]; + as_log_verbose(ASLayoutLog(), "Transition ID is %d", transitionID); + // NOTE: This block captures self. It's cheaper than hitting the weak table. + asdisplaynode_iscancelled_block_t isCancelled = ^{ + BOOL result = (_transitionID != transitionID); + if (result) { + as_log_verbose(ASLayoutLog(), "Transition %d canceled, superseded by %d", transitionID, _transitionID.load()); + } + return result; + }; + + // Move all subnodes in layout pending state for this transition + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + ASDisplayNodeAssert(node->_transitionID == ASLayoutElementContextInvalidTransitionID, @"Can't start a transition when one of the subnodes is performing one."); + node.hierarchyState |= ASHierarchyStateLayoutPending; + node->_pendingTransitionID = transitionID; + }); + + // Transition block that executes the layout transition + void (^transitionBlock)(void) = ^{ + if (isCancelled()) { + return; + } + + // Perform a full layout creation pass with passed in constrained size to create the new layout for the transition + NSUInteger newLayoutVersion = _layoutVersion; + ASLayout *newLayout; + { + ASScopedLockSelfOrToRoot(); + + ASLayoutElementContext *ctx = [[ASLayoutElementContext alloc] init]; + ctx.transitionID = transitionID; + ASLayoutElementPushContext(ctx); + + BOOL automaticallyManagesSubnodesDisabled = (self.automaticallyManagesSubnodes == NO); + self.automaticallyManagesSubnodes = YES; // Temporary flag for 1.9.x + newLayout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:constrainedSize.max]; + if (automaticallyManagesSubnodesDisabled) { + self.automaticallyManagesSubnodes = NO; // Temporary flag for 1.9.x + } + + ASLayoutElementPopContext(); + } + + if (isCancelled()) { + return; + } + + ASPerformBlockOnMainThread(^{ + if (isCancelled()) { + return; + } + as_activity_create_for_scope("Commit layout transition"); + ASLayoutTransition *pendingLayoutTransition; + _ASTransitionContext *pendingLayoutTransitionContext; + { + // Grab __instanceLock__ here to make sure this transition isn't invalidated + // right after it passed the validation test and before it proceeds + MutexLocker l(__instanceLock__); + + // Update calculated layout + const auto previousLayout = _calculatedDisplayNodeLayout; + const auto pendingLayout = ASDisplayNodeLayout(newLayout, + constrainedSize, + constrainedSize.max, + newLayoutVersion); + [self _locked_setCalculatedDisplayNodeLayout:pendingLayout]; + + // Setup pending layout transition for animation + _pendingLayoutTransition = pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:pendingLayout + previousLayout:previousLayout]; + // Setup context for pending layout transition. we need to hold a strong reference to the context + _pendingLayoutTransitionContext = pendingLayoutTransitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated + layoutDelegate:_pendingLayoutTransition + completionDelegate:self]; + } + + // Apply complete layout transitions for all subnodes + { + as_activity_create_for_scope("Complete pending layout transitions for subtree"); + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node _completePendingLayoutTransition]; + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } + + // Measurement pass completion + // Give the subclass a change to hook into before calling the completion block + [self _layoutTransitionMeasurementDidFinish]; + if (completion) { + completion(); + } + + // Apply the subnode insertion immediately to be able to animate the nodes + [pendingLayoutTransition applySubnodeInsertionsAndMoves]; + + // Kick off animating the layout transition + { + as_activity_create_for_scope("Animate layout transition"); + [self animateLayoutTransition:pendingLayoutTransitionContext]; + } + + // Mark transaction as finished + [self _finishOrCancelTransition]; + }); + }; + + // Start transition based on flag on current or background thread + if (shouldMeasureAsync) { + ASPerformBlockOnBackgroundThread(transitionBlock); + } else { + transitionBlock(); + } +} + +- (void)cancelLayoutTransition +{ + if ([self _finishOrCancelTransition]) { + // Tell subnodes to exit layout pending state and clear related properties + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } +} + +- (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration +{ + MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDuration = defaultLayoutTransitionDuration; +} + +- (NSTimeInterval)defaultLayoutTransitionDuration +{ + MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDuration; +} + +- (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay +{ + MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDelay = defaultLayoutTransitionDelay; +} + +- (NSTimeInterval)defaultLayoutTransitionDelay +{ + MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDelay; +} + +- (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + MutexLocker l(__instanceLock__); + _defaultLayoutTransitionOptions = defaultLayoutTransitionOptions; +} + +- (UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionOptions; +} + +#pragma mark + +/* + * Hook for subclasses to perform an animation based on the given ASContextTransitioning. By default a fade in and out + * animation is provided. + */ +- (void)animateLayoutTransition:(id)context +{ + if ([context isAnimated] == NO) { + [self _layoutSublayouts]; + [context completeTransition:YES]; + return; + } + + ASDisplayNode *node = self; + + NSAssert(node.isNodeLoaded == YES, @"Invalid node state"); + + NSArray *removedSubnodes = [context removedSubnodes]; + NSMutableArray *insertedSubnodes = [[context insertedSubnodes] mutableCopy]; + const auto movedSubnodes = [[NSMutableArray alloc] init]; + + const auto insertedSubnodeContexts = [[NSMutableArray<_ASAnimatedTransitionContext *> alloc] init]; + const auto removedSubnodeContexts = [[NSMutableArray<_ASAnimatedTransitionContext *> alloc] init]; + + for (ASDisplayNode *subnode in [context subnodesForKey:ASTransitionContextToLayoutKey]) { + if ([insertedSubnodes containsObject:subnode] == NO) { + // This is an existing subnode, check if it is resized, moved or both + CGRect fromFrame = [context initialFrameForNode:subnode]; + CGRect toFrame = [context finalFrameForNode:subnode]; + if (CGSizeEqualToSize(fromFrame.size, toFrame.size) == NO) { + [insertedSubnodes addObject:subnode]; + } + if (CGPointEqualToPoint(fromFrame.origin, toFrame.origin) == NO) { + [movedSubnodes addObject:subnode]; + } + } + } + + // Create contexts for inserted and removed subnodes + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + [insertedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:insertedSubnode alpha:insertedSubnode.alpha]]; + } + for (ASDisplayNode *removedSubnode in removedSubnodes) { + [removedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:removedSubnode alpha:removedSubnode.alpha]]; + } + + // Fade out inserted subnodes + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + insertedSubnode.frame = [context finalFrameForNode:insertedSubnode]; + insertedSubnode.alpha = 0; + } + + // Adjust groupOpacity for animation + BOOL originAllowsGroupOpacity = node.allowsGroupOpacity; + node.allowsGroupOpacity = YES; + + [UIView animateWithDuration:self.defaultLayoutTransitionDuration delay:self.defaultLayoutTransitionDelay options:self.defaultLayoutTransitionOptions animations:^{ + // Fade removed subnodes and views out + for (ASDisplayNode *removedSubnode in removedSubnodes) { + removedSubnode.alpha = 0; + } + + // Fade inserted subnodes in + for (_ASAnimatedTransitionContext *insertedSubnodeContext in insertedSubnodeContexts) { + insertedSubnodeContext.node.alpha = insertedSubnodeContext.alpha; + } + + // Update frame of self and moved subnodes + CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; + CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; + BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); + if (isResized == YES) { + CGPoint position = node.frame.origin; + node.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); + } + for (ASDisplayNode *movedSubnode in movedSubnodes) { + movedSubnode.frame = [context finalFrameForNode:movedSubnode]; + } + } completion:^(BOOL finished) { + // Restore all removed subnode alpha values + for (_ASAnimatedTransitionContext *removedSubnodeContext in removedSubnodeContexts) { + removedSubnodeContext.node.alpha = removedSubnodeContext.alpha; + } + + // Restore group opacity + node.allowsGroupOpacity = originAllowsGroupOpacity; + + // Subnode removals are automatically performed + [context completeTransition:finished]; + }]; +} + +/** + * Hook for subclasses to clean up nodes after the transition happened. Furthermore this can be used from subclasses + * to manually perform deletions. + */ +- (void)didCompleteLayoutTransition:(id)context +{ + ASDisplayNodeAssertMainThread(); + + __instanceLock__.lock(); + ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; + __instanceLock__.unlock(); + + [pendingLayoutTransition applySubnodeRemovals]; +} + +/** + * Completes the pending layout transition immediately without going through the the Layout Transition Animation API + */ +- (void)_completePendingLayoutTransition +{ + __instanceLock__.lock(); + ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; + __instanceLock__.unlock(); + + if (pendingLayoutTransition != nil) { + [self _setCalculatedDisplayNodeLayout:pendingLayoutTransition.pendingLayout]; + [self _completeLayoutTransition:pendingLayoutTransition]; + [self _pendingLayoutTransitionDidComplete]; + } +} + +/** + * Can be directly called to commit the given layout transition immediately to complete without calling through to the + * Layout Transition Animation API + */ +- (void)_completeLayoutTransition:(ASLayoutTransition *)layoutTransition +{ + // Layout transition is not supported for nodes that do not have automatic subnode management enabled + if (layoutTransition == nil || self.automaticallyManagesSubnodes == NO) { + return; + } + + // Trampoline to the main thread if necessary + if (ASDisplayNodeThreadIsMain() || layoutTransition.isSynchronous == NO) { + // Committing the layout transition will result in subnode insertions and removals, both of which must be called without the lock held + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + [layoutTransition commitTransition]; + } else { + // Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded + ASPerformBlockOnMainThread(^{ + [layoutTransition commitTransition]; + }); + } +} + +- (void)_assertSubnodeState +{ + // Verify that any orphaned nodes are removed. + // This can occur in rare cases if main thread layout is flushed while a background layout is calculating. + + if (self.automaticallyManagesSubnodes == NO) { + return; + } + + MutexLocker l(__instanceLock__); + NSArray *sublayouts = _calculatedDisplayNodeLayout.layout.sublayouts; + unowned ASLayout *cSublayouts[sublayouts.count]; + [sublayouts getObjects:cSublayouts range:NSMakeRange(0, AS_ARRAY_SIZE(cSublayouts))]; + + // Fast-path if we are in the correct state (likely). + if (_subnodes.count == AS_ARRAY_SIZE(cSublayouts)) { + NSUInteger i = 0; + BOOL matches = YES; + for (ASDisplayNode *subnode in _subnodes) { + if (subnode != cSublayouts[i].layoutElement) { + matches = NO; + } + i++; + } + if (matches) { + return; + } + } + + NSArray *layoutNodes = ASArrayByFlatMapping(sublayouts, ASLayout *layout, (ASDisplayNode *)layout.layoutElement); + NSIndexSet *insertions, *deletions; + [_subnodes asdk_diffWithArray:layoutNodes insertions:&insertions deletions:&deletions]; + if (insertions.count > 0) { + NSLog(@"Warning: node's layout includes subnode that has not been added: node = %@, subnodes = %@, subnodes in layout = %@", self, _subnodes, layoutNodes); + } + + // Remove any nodes that are in the tree but should not be. + // Go in reverse order so we don't shift our indexes. + if (deletions) { + for (NSUInteger i = deletions.lastIndex; i != NSNotFound; i = [deletions indexLessThanIndex:i]) { + NSLog(@"Automatically removing orphaned subnode %@, from parent %@", _subnodes[i], self); + [_subnodes[i] removeFromSupernode]; + } + } +} + +- (void)_pendingLayoutTransitionDidComplete +{ + // This assertion introduces a breaking behavior for nodes that has ASM enabled but also manually manage some subnodes. + // Let's gate it behind YOGA flag. +#if YOGA + [self _assertSubnodeState]; +#endif + + // Subclass hook + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + [self calculatedLayoutDidChange]; + + // Grab lock after calling out to subclass + MutexLocker l(__instanceLock__); + + // We generate placeholders at -layoutThatFits: time so that a node is guaranteed to have a placeholder ready to go. + // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. + // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. + if (_placeholderEnabled && !_placeholderImage && [self _locked_displaysAsynchronously]) { + + // Zero-sized nodes do not require a placeholder. + CGSize layoutSize = _calculatedDisplayNodeLayout.layout.size; + if (layoutSize.width * layoutSize.height <= 0.0) { + return; + } + + // If we've displayed our contents, we don't need a placeholder. + // Contents is a thread-affined property and can't be read off main after loading. + if (self.isNodeLoaded) { + ASPerformBlockOnMainThread(^{ + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } + }); + } else { + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } + } + } + + // Cleanup pending layout transition + _pendingLayoutTransition = nil; +} + +- (void)_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)displayNodeLayout +{ + MutexLocker l(__instanceLock__); + [self _locked_setCalculatedDisplayNodeLayout:displayNodeLayout]; +} + +- (void)_locked_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)displayNodeLayout +{ + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssertTrue(displayNodeLayout.layout.layoutElement == self); + ASDisplayNodeAssertTrue(displayNodeLayout.layout.size.width >= 0.0); + ASDisplayNodeAssertTrue(displayNodeLayout.layout.size.height >= 0.0); + + _calculatedDisplayNodeLayout = displayNodeLayout; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (YogaLayout) + +@implementation ASDisplayNode (YogaLayout) + +- (BOOL)locked_shouldLayoutFromYogaRoot { +#if YOGA + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); + if (usesYoga) { + if ([self shouldHaveYogaMeasureFunc] == NO) { + return YES; + } else { + return NO; + } + } else { + return NO; + } +#else + return NO; +#endif +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.h new file mode 100644 index 0000000000..d7cd862047 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.h @@ -0,0 +1,66 @@ +// +// ASDisplayNode+LayoutSpec.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@class ASLayout; + +NS_ASSUME_NONNULL_BEGIN + +@interface ASDisplayNode (ASLayoutSpec) + +/** + * @abstract Provides a way to declare a block to provide an ASLayoutSpec without having to subclass ASDisplayNode and + * implement layoutSpecThatFits: + * + * @return A block that takes a constrainedSize ASSizeRange argument, and must return an ASLayoutSpec that includes all + * of the subnodes to position in the layout. This input-output relationship is identical to the subclass override + * method -layoutSpecThatFits: + * + * @warning Subclasses that implement -layoutSpecThatFits: must not also use .layoutSpecBlock. Doing so will trigger + * an exception. A future version of the framework may support using both, calling them serially, with the + * .layoutSpecBlock superseding any values set by the method override. + * + * @code ^ASLayoutSpec *(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {}; + */ +@property (nullable) ASLayoutSpecBlock layoutSpecBlock; + +@end + +// These methods are intended to be used internally to Texture, and should not be called directly. +@interface ASDisplayNode (ASLayoutSpecPrivate) + +/// For internal usage only +- (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize; + +@end + +@interface ASDisplayNode (ASLayoutSpecSubclasses) + +/** + * @abstract Return a layout spec that describes the layout of the receiver and its children. + * + * @param constrainedSize The minimum and maximum sizes the receiver should fit in. + * + * @discussion Subclasses that override should expect this method to be called on a non-main thread. The returned layout spec + * is used to calculate an ASLayout and cached by ASDisplayNode for quick access during -layout. Other expensive work that needs to + * be done before display can be performed here, and using ivars to cache any valuable intermediate results is + * encouraged. + * + * @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: instead. + * + * @warning Subclasses that implement -layoutSpecThatFits: must not use .layoutSpecBlock. Doing so will trigger an + * exception. A future version of the framework may support using both, calling them serially, with the .layoutSpecBlock + * superseding any values set by the method override. + */ +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm new file mode 100644 index 0000000000..d79073ffdd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm @@ -0,0 +1,146 @@ +// +// ASDisplayNode+LayoutSpec.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import + + +@implementation ASDisplayNode (ASLayoutSpec) + +- (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock +{ + // For now there should never be an override of layoutSpecThatFits: and a layoutSpecBlock together. + ASDisplayNodeAssert(!(_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits), + @"Nodes with a .layoutSpecBlock must not also implement -layoutSpecThatFits:"); + AS::MutexLocker l(__instanceLock__); + _layoutSpecBlock = layoutSpecBlock; +} + +- (ASLayoutSpecBlock)layoutSpecBlock +{ + AS::MutexLocker l(__instanceLock__); + return _layoutSpecBlock; +} + +- (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize +{ + AS::UniqueLock l(__instanceLock__); + + // Manual size calculation via calculateSizeThatFits: + if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) { + CGSize size = [self calculateSizeThatFits:constrainedSize.max]; + ASDisplayNodeLogEvent(self, @"calculatedSize: %@", NSStringFromCGSize(size)); + return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil]; + } + + // Size calcualtion with layout elements + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + if (measureLayoutSpec) { + _layoutSpecNumberOfPasses++; + } + + // Get layout element from the node + id layoutElement = [self _locked_layoutElementThatFits:constrainedSize]; +#if ASEnableVerboseLogging + for (NSString *asciiLine in [[layoutElement asciiArtString] componentsSeparatedByString:@"\n"]) { + as_log_verbose(ASLayoutLog(), "%@", asciiLine); + } +#endif + + + // Certain properties are necessary to set on an element of type ASLayoutSpec + if (layoutElement.layoutElementType == ASLayoutElementTypeLayoutSpec) { + ASLayoutSpec *layoutSpec = (ASLayoutSpec *)layoutElement; + +#if AS_DEDUPE_LAYOUT_SPEC_TREE + NSHashTable *duplicateElements = [layoutSpec findDuplicatedElementsInSubtree]; + if (duplicateElements.count > 0) { + ASDisplayNodeFailAssert(@"Node %@ returned a layout spec that contains the same elements in multiple positions. Elements: %@", self, duplicateElements); + // Use an empty layout spec to avoid crashes + layoutSpec = [[ASLayoutSpec alloc] init]; + } +#endif + + ASDisplayNodeAssert(layoutSpec.isMutable, @"Node %@ returned layout spec %@ that has already been used. Layout specs should always be regenerated.", self, layoutSpec); + + layoutSpec.isMutable = NO; + } + + // Manually propagate the trait collection here so that any layoutSpec children of layoutSpec will get a traitCollection + { + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + ASTraitCollectionPropagateDown(layoutElement, self.primitiveTraitCollection); + } + + BOOL measureLayoutComputation = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation; + if (measureLayoutComputation) { + _layoutComputationNumberOfPasses++; + } + + // Layout element layout creation + ASLayout *layout = ({ + AS::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation); + [layoutElement layoutThatFits:constrainedSize]; + }); + ASDisplayNodeAssertNotNil(layout, @"[ASLayoutElement layoutThatFits:] should never return nil! %@, %@", self, layout); + + // Make sure layoutElementObject of the root layout is `self`, so that the flattened layout will be structurally correct. + BOOL isFinalLayoutElement = (layout.layoutElement != self); + if (isFinalLayoutElement) { + layout.position = CGPointZero; + layout = [ASLayout layoutWithLayoutElement:self size:layout.size sublayouts:@[layout]]; + } + ASDisplayNodeLogEvent(self, @"computedLayout: %@", layout); + + // PR #1157: Reduces accuracy of _unflattenedLayout for debugging/Weaver + if ([ASDisplayNode shouldStoreUnflattenedLayouts]) { + _unflattenedLayout = layout; + } + layout = [layout filteredNodeLayoutTree]; + + return layout; +} + +- (id)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize +{ + ASAssertLocked(__instanceLock__); + + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + + if (_layoutSpecBlock != NULL) { + return ({ + AS::MutexLocker l(__instanceLock__); + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + _layoutSpecBlock(self, constrainedSize); + }); + } else { + return ({ + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + [self layoutSpecThatFits:constrainedSize]; + }); + } +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeAssert(NO, @"-[ASDisplayNode layoutSpecThatFits:] should never return an empty value. One way this is caused is by calling -[super layoutSpecThatFits:] which is not currently supported."); + return [[ASLayoutSpec alloc] init]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Subclasses.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Subclasses.h new file mode 100644 index 0000000000..d3c9575fbb --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Subclasses.h @@ -0,0 +1,493 @@ +// +// ASDisplayNode+Subclasses.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@class ASLayoutSpec, _ASDisplayLayer; + +NS_ASSUME_NONNULL_BEGIN + +/** + * The subclass header _ASDisplayNode+Subclasses_ defines the following methods that either must or can be overriden by + * subclasses of ASDisplayNode. + * + * These methods should never be called directly by other classes. + * + * ## Drawing + * + * Implement one of +displayWithParameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide + * drawing for your node. + * + * Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for + * use on the display queue. The display and drawRect implementations *MUST* be thread-safe, as they can be called on + * the displayQueue (asynchronously) or the main thread (synchronously/displayImmediately). + * + * Class methods that require passing in copies of the values are used to minimize the need for locking around instance + * variable access, and the possibility of the asynchronous display pass grabbing an inconsistent state across multiple + * variables. + */ + +@interface ASDisplayNode (Subclassing) + +#pragma mark - Properties +/** @name Properties */ + +/** + * @abstract Return the calculated layout. + * + * @discussion For node subclasses that implement manual layout (e.g., they have a custom -layout method), + * calculatedLayout may be accessed on subnodes to retrieved cached information about their size. + * This allows -layout to be very fast, saving time on the main thread. + * Note: .calculatedLayout will only be set for nodes that have had -layoutThatFits: called on them. + * For manual layout, make sure you call -layoutThatFits: in your implementation of -calculateSizeThatFits:. + * + * For node subclasses that use automatic layout (e.g., they implement -layoutSpecThatFits:), + * it is typically not necessary to use .calculatedLayout at any point. For these nodes, + * the ASLayoutSpec implementation will automatically call -layoutThatFits: on all of the subnodes, + * and the ASDisplayNode base class implementation of -layout will automatically make use of .calculatedLayout on the subnodes. + * + * @return Layout that wraps calculated size returned by -calculateSizeThatFits: (in manual layout mode), + * or layout already calculated from layout spec returned by -layoutSpecThatFits: (in automatic layout mode). + * + * @warning Subclasses must not override this; it returns the last cached layout and is never expensive. + */ +@property (nullable, readonly) ASLayout *calculatedLayout; + +#pragma mark - View Lifecycle +/** @name View Lifecycle */ + +/** + * @abstract Called on the main thread immediately after self.view is created. + * + * @discussion This is the best time to add gesture recognizers to the view. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)didLoad ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * An empty method that you can implement in a category to add global + * node initialization behavior. This method will be called by [ASDisplayNode init]. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)baseDidInit; + +/** + * An empty method that you can implement in a category to add global + * node deallocation behavior. This method will be called by [ASDisplayNode dealloc]. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)baseWillDealloc; + +#pragma mark - Layout +/** @name Layout */ + +/** + * @abstract Called on the main thread by the view's -layoutSubviews. + * + * @discussion Subclasses override this method to layout all subnodes or subviews. + */ +- (void)layout ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Called on the main thread by the view's -layoutSubviews, after -layout. + * + * @discussion Gives a chance for subclasses to perform actions after the subclass and superclass have finished laying + * out. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)layoutDidFinish ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Called on a background thread if !isNodeLoaded - called on the main thread if isNodeLoaded. + * + * @discussion When the .calculatedLayout property is set to a new ASLayout (directly from -calculateLayoutThatFits: or + * calculated via use of -layoutSpecThatFits:), subclasses may inspect it here. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)calculatedLayoutDidChange ASDISPLAYNODE_REQUIRES_SUPER; + + +#pragma mark - Layout calculation +/** @name Layout calculation */ + +/** + * @abstract Calculate a layout based on given size range. + * + * @param constrainedSize The minimum and maximum sizes the receiver should fit in. + * + * @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used). + * + * @discussion This method is called on a non-main thread. The default implementation calls either -layoutSpecThatFits: + * or -calculateSizeThatFits:, whichever method is overriden. Subclasses rarely need to override this method, + * override -layoutSpecThatFits: or -calculateSizeThatFits: instead. + * + * @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or -calculatedLayout instead. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize; +- (CGSize)calculateSizeThatFits:(CGSize)contrainedSize; + +/** + * ASDisplayNode's implementation of -layoutThatFits:parentSize: calls this method to resolve the node's size + * against parentSize, intersect it with constrainedSize, and call -calculateLayoutThatFits: with the result. + * + * In certain advanced cases, you may want to customize this logic. Overriding this method allows you to receive all + * three parameters and do the computation yourself. + * + * @warning Overriding this method should be done VERY rarely. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize; + +/** + * @abstract Return the calculated size. + * + * @param constrainedSize The maximum size the receiver should fit in. + * + * @discussion Subclasses that override should expect this method to be called on a non-main thread. The returned size + * is wrapped in an ASLayout and cached for quick access during -layout. Other expensive work that needs to + * be done before display can be performed here, and using ivars to cache any valuable intermediate results is + * encouraged. + * + * @note Subclasses that override are committed to manual layout. Therefore, -layout: must be overriden to layout all subnodes or subviews. + * + * @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or layoutThatFits:parentSize: instead. + */ +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize; + +/** + * @abstract Invalidate previously measured and cached layout. + * + * @discussion Subclasses should call this method to invalidate the previously measured and cached layout for the display + * node, when the contents of the node change in such a way as to require measuring it again. + */ +- (void)invalidateCalculatedLayout; + +#pragma mark - Observing Node State Changes +/** @name Observing node state changes */ + +/** + * Declare methods as requiring super calls (this can't be required in the protocol). + * For descriptions, see definition. + */ + +AS_CATEGORY_IMPLEMENTABLE +- (void)didEnterVisibleState ASDISPLAYNODE_REQUIRES_SUPER; + +AS_CATEGORY_IMPLEMENTABLE +- (void)didExitVisibleState ASDISPLAYNODE_REQUIRES_SUPER; + +AS_CATEGORY_IMPLEMENTABLE +- (void)didEnterDisplayState ASDISPLAYNODE_REQUIRES_SUPER; + +AS_CATEGORY_IMPLEMENTABLE +- (void)didExitDisplayState ASDISPLAYNODE_REQUIRES_SUPER; + +AS_CATEGORY_IMPLEMENTABLE +- (void)didEnterPreloadState ASDISPLAYNODE_REQUIRES_SUPER; + +AS_CATEGORY_IMPLEMENTABLE +- (void)didExitPreloadState ASDISPLAYNODE_REQUIRES_SUPER; + +AS_CATEGORY_IMPLEMENTABLE +- (void)interfaceStateDidChange:(ASInterfaceState)newState + fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Called when the node's ASTraitCollection changes + * + * @discussion Subclasses can override this method to react to a trait collection change. + */ +AS_CATEGORY_IMPLEMENTABLE +- (void)asyncTraitCollectionDidChange; + +#pragma mark - Drawing +/** @name Drawing */ + +/** + * @summary Delegate method to draw layer contents into a CGBitmapContext. The current UIGraphics context will be set + * to an appropriate context. + * + * @param bounds Region to draw in. + * @param parameters An object describing all of the properties you need to draw. Return this from + * -drawParametersForAsyncLayer: + * @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid + * unnecessary work. A return value of YES means cancel drawing and return. + * @param isRasterizing YES if the layer is being rasterized into another layer, in which case drawRect: probably wants + * to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store. + * + * @note Called on the display queue and/or main queue (MUST BE THREAD SAFE) + */ +/*+ (void)drawRect:(CGRect)bounds withParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock + isRasterizing:(BOOL)isRasterizing;*/ + +/** + * @summary Delegate override to provide new layer contents as a UIImage. + * + * @param parameters An object describing all of the properties you need to draw. Return this from + * -drawParametersForAsyncLayer: + * @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid + * unnecessary work. A return value of YES means cancel drawing and return. + * + * @return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already + * decoded before returning it here. + * + * @note Called on the display queue and/or main queue (MUST BE THREAD SAFE) + */ ++ (nullable UIImage *)displayWithParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; + +/** + * @abstract Delegate override for drawParameters + * + * @param layer The layer that will be drawn into. + * + * @note Called on the main thread only + */ +- (nullable id)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; + +/** + * @abstract Indicates that the receiver is about to display. + * + * @discussion Deprecated in 2.5. + * + * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is + * about to begin. + * + * @note Called on the main thread only + */ +- (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use displayWillStartAsynchronously: instead."); + +/** + * @abstract Indicates that the receiver is about to display. + * + * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is + * about to begin. + * + * @note Called on the main thread only + */ +- (void)displayWillStartAsynchronously:(BOOL)asynchronously ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Indicates that the receiver has finished displaying. + * + * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) has + * completed. + * + * @note Called on the main thread only + */ +- (void)displayDidFinish ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * Called just before the view is added to a window. + */ +- (void)willEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * Called after the view is removed from the window. + */ +- (void)didExitHierarchy ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * Called just after the view is added to a window. + * Note: this may be called multiple times during view controller transitions. To overcome this: use didEnterVisibleState or its equavalents. + */ +- (void)didEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Whether the view or layer of this display node is currently in a window + */ +@property (readonly, getter=isInHierarchy) BOOL inHierarchy; + +/** + * Provides an opportunity to clear backing store and other memory-intensive intermediates, such as text layout managers + * on the current node. + * + * @discussion Called by -recursivelyClearContents. Always called on main thread. Base class implements self.contents = nil, clearing any backing + * store, for asynchronous regeneration when needed. + */ +- (void)clearContents ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Indicates that the receiver is about to display its subnodes. This method is not called if there are no + * subnodes present. + * + * @param subnode The subnode of which display is about to begin. + * + * @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) is + * about to begin. + */ +- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Indicates that the receiver is finished displaying its subnodes. This method is not called if there are + * no subnodes present. + * + * @param subnode The subnode of which display is about to completed. + * + * @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) has + * completed. + */ +- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Marks the receiver's bounds as needing to be redrawn, with a scale value. + * + * @param contentsScale The scale at which the receiver should be drawn. + * + * @discussion Subclasses should override this if they don't want their contentsScale changed. + * + * @note This changes an internal property. + * -setNeedsDisplay is also available to trigger display without changing contentsScaleForDisplay. + * @see -setNeedsDisplay, contentsScaleForDisplay + */ +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale; + +/** + * @abstract Recursively calls setNeedsDisplayAtScale: on subnodes. + * + * @param contentsScale The scale at which the receiver's subnode hierarchy should be drawn. + * + * @discussion Subclasses may override this if they require modifying the scale set on their child nodes. + * + * @note Only the node tree is walked, not the view or layer trees. + * + * @see setNeedsDisplayAtScale: + * @see contentsScaleForDisplay + */ +- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale; + +/** + * @abstract The scale factor to apply to the rendering. + * + * @discussion Use setNeedsDisplayAtScale: to set a value and then after display, the display node will set the layer's + * contentsScale. This is to prevent jumps when re-rasterizing at a different contentsScale. + * Read this property if you need to know the future contentsScale of your layer, eg in drawParameters. + * + * @see setNeedsDisplayAtScale: + */ +@property (readonly) CGFloat contentsScaleForDisplay; + + +#pragma mark - Touch handling +/** @name Touch handling */ + +/** + * @abstract Tells the node when touches began in its view. + * + * @param touches A set of UITouch instances. + * @param event A UIEvent associated with the touch. + */ +- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Tells the node when touches moved in its view. + * + * @param touches A set of UITouch instances. + * @param event A UIEvent associated with the touch. + */ +- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Tells the node when touches ended in its view. + * + * @param touches A set of UITouch instances. + * @param event A UIEvent associated with the touch. + */ +- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Tells the node when touches was cancelled in its view. + * + * @param touches A set of UITouch instances. + * @param event A UIEvent associated with the touch. + */ +- (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; + + +#pragma mark - Managing Gesture Recognizers +/** @name Managing Gesture Recognizers */ + +/** + * @abstract Asks the node if a gesture recognizer should continue tracking touches. + * + * @param gestureRecognizer A gesture recognizer trying to recognize a gesture. + */ +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer; + + +#pragma mark - Hit Testing + +/** @name Hit Testing */ + +/** + * @abstract Returns the view that contains the point. + * + * @discussion Override to make this node respond differently to touches: (e.g. hide touches from subviews, send all + * touches to certain subviews (hit area maximizing), etc.) + * + * @param point A point specified in the node's local coordinate system (bounds). + * @param event The event that warranted a call to this method. + * + * @return Returns a UIView, not ASDisplayNode, for two reasons: + * 1) allows sending events to plain UIViews that don't have attached nodes, + * 2) hitTest: is never called before the views are created. + */ +- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; + + +#pragma mark - Placeholders +/** @name Placeholders */ + +/** + * @abstract Optionally provide an image to serve as the placeholder for the backing store while the contents are being + * displayed. + * + * @discussion + * Subclasses may override this method and return an image to use as the placeholder. Take caution as there may be a + * time and place where this method is called on a background thread. Note that -[UIImage imageNamed:] is not thread + * safe when using image assets. + * + * To retrieve the CGSize to do any image drawing, use the node's calculatedSize property. + * + * Defaults to nil. + * + * @note Called on the display queue and/or main queue (MUST BE THREAD SAFE) + */ +- (nullable UIImage *)placeholderImage; + + +#pragma mark - Description +/** @name Description */ + +/** + * @abstract Return a description of the node + * + * @discussion The function that gets called for each display node in -recursiveDescription + */ +- (NSString *)descriptionForRecursiveDescription; + +@end + + +// Check that at most a layoutSpecBlock or one of the three layout methods is overridden +#define __ASDisplayNodeCheckForLayoutMethodOverrides \ + ASDisplayNodeAssert(_layoutSpecBlock != NULL || \ + ((ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateSizeThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(layoutSpecThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateLayoutThatFits:)) ? 1 : 0)) <= 1, \ + @"Subclass %@ must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:", NSStringFromClass(self.class)) + +#define ASDisplayNodeAssertThreadAffinity(viewNode) ASDisplayNodeAssert(!viewNode || ASMainThreadAssertionsAreDisabled() || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created") +#define ASDisplayNodeCAssertThreadAffinity(viewNode) ASDisplayNodeCAssert(!viewNode || ASMainThreadAssertionsAreDisabled() || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created") + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Yoga.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Yoga.h new file mode 100644 index 0000000000..93cbf7a7c1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Yoga.h @@ -0,0 +1,102 @@ +// +// ASDisplayNode+Yoga.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA + +NS_ASSUME_NONNULL_BEGIN + +@class ASLayout; + +AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable node, void(^block)(ASDisplayNode *node)); + +@interface ASDisplayNode (Yoga) + +@property (copy) NSArray *yogaChildren; + +- (void)addYogaChild:(ASDisplayNode *)child; +- (void)removeYogaChild:(ASDisplayNode *)child; +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index; + +- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; + +@property BOOL yogaLayoutInProgress; +// TODO: Make this atomic (lock). +@property (nullable, nonatomic) ASLayout *yogaCalculatedLayout; + +// Will walk up the Yoga tree and returns the root node +- (ASDisplayNode *)yogaRoot; + + +@end + +@interface ASDisplayNode (YogaLocking) +/** + * @discussion Attempts(spinning) to lock all node up to root node when yoga is enabled. + * This will lock self when yoga is not enabled; + */ +- (ASLockSet)lockToRootIfNeededForLayout; + +@end + + +// These methods are intended to be used internally to Texture, and should not be called directly. +@interface ASDisplayNode (YogaInternal) + +/// For internal usage only +- (BOOL)shouldHaveYogaMeasureFunc; +/// For internal usage only +- (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize; +/// For internal usage only +- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; +/// For internal usage only +- (void)invalidateCalculatedYogaLayout; +/** + * @discussion return true only when yoga enabled and the node is in yoga tree and the node is + * not leaf that implemented measure function. + */ +- (BOOL)locked_shouldLayoutFromYogaRoot; + +@end + +@interface ASDisplayNode (YogaDebugging) + +- (NSString *)yogaTreeDescription; + +@end + +@interface ASLayoutElementStyle (Yoga) + +- (YGNodeRef)yogaNodeCreateIfNeeded; +- (void)destroyYogaNode; + +@property (readonly) YGNodeRef yogaNode; + +@property ASStackLayoutDirection flexDirection; +@property YGDirection direction; +@property ASStackLayoutJustifyContent justifyContent; +@property ASStackLayoutAlignItems alignItems; +@property YGPositionType positionType; +@property ASEdgeInsets position; +@property ASEdgeInsets margin; +@property ASEdgeInsets padding; +@property ASEdgeInsets border; +@property CGFloat aspectRatio; +@property YGWrap flexWrap; + +@end + +NS_ASSUME_NONNULL_END + +// When Yoga is enabled, there are several points where we want to lock the tree to the root but otherwise (without Yoga) +// will want to simply lock self. +#define ASScopedLockSelfOrToRoot() ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout] +#else +#define ASScopedLockSelfOrToRoot() ASLockScopeSelf() +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Yoga.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Yoga.mm new file mode 100644 index 0000000000..dcecda7279 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Yoga.mm @@ -0,0 +1,472 @@ +// +// ASDisplayNode+Yoga.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA /* YOGA */ + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import + +#define YOGA_LAYOUT_LOGGING 0 + +#pragma mark - ASDisplayNode+Yoga + +@interface ASDisplayNode (YogaPrivate) +@property (nonatomic, weak) ASDisplayNode *yogaParent; +- (ASSizeRange)_locked_constrainedSizeForLayoutPass; +@end + +@implementation ASDisplayNode (Yoga) + +- (ASDisplayNode *)yogaRoot +{ + ASDisplayNode *yogaRoot = self; + ASDisplayNode *yogaParent = nil; + while ((yogaParent = yogaRoot.yogaParent)) { + yogaRoot = yogaParent; + } + return yogaRoot; +} + +- (void)setYogaChildren:(NSArray *)yogaChildren +{ + ASScopedLockSelfOrToRoot(); + for (ASDisplayNode *child in [_yogaChildren copy]) { + // Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren + // If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here. + [self _locked_removeYogaChild:child]; + } + _yogaChildren = nil; + for (ASDisplayNode *child in yogaChildren) { + [self _locked_addYogaChild:child]; + } +} + +- (NSArray *)yogaChildren +{ + ASLockScope(self.yogaRoot); + return [_yogaChildren copy] ?: @[]; +} + +- (void)addYogaChild:(ASDisplayNode *)child +{ + ASScopedLockSelfOrToRoot(); + [self _locked_addYogaChild:child]; +} + +- (void)_locked_addYogaChild:(ASDisplayNode *)child +{ + [self insertYogaChild:child atIndex:_yogaChildren.count]; +} + +- (void)removeYogaChild:(ASDisplayNode *)child +{ + ASScopedLockSelfOrToRoot(); + [self _locked_removeYogaChild:child]; +} + +- (void)_locked_removeYogaChild:(ASDisplayNode *)child +{ + if (child == nil) { + return; + } + + [_yogaChildren removeObjectIdenticalTo:child]; + + // YGNodeRef removal is done in setParent: + child.yogaParent = nil; +} + +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index +{ + ASScopedLockSelfOrToRoot(); + [self _locked_insertYogaChild:child atIndex:index]; +} + +- (void)_locked_insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index +{ + if (child == nil) { + return; + } + if (_yogaChildren == nil) { + _yogaChildren = [[NSMutableArray alloc] init]; + } + + // Clean up state in case this child had another parent. + [self _locked_removeYogaChild:child]; + + [_yogaChildren insertObject:child atIndex:index]; + + // YGNodeRef insertion is done in setParent: + child.yogaParent = self; +} + +#pragma mark - Subclass Hooks + +- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute +{ + UIUserInterfaceLayoutDirection layoutDirection = + [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute]; + self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight + ? YGDirectionLTR : YGDirectionRTL); +} + +- (void)setYogaParent:(ASDisplayNode *)yogaParent +{ + ASLockScopeSelf(); + if (_yogaParent == yogaParent) { + return; + } + + YGNodeRef yogaNode = [self.style yogaNodeCreateIfNeeded]; + YGNodeRef oldParentRef = YGNodeGetParent(yogaNode); + if (oldParentRef != NULL) { + YGNodeRemoveChild(oldParentRef, yogaNode); + } + + _yogaParent = yogaParent; + if (yogaParent) { + YGNodeRef newParentRef = [yogaParent.style yogaNodeCreateIfNeeded]; + YGNodeInsertChild(newParentRef, yogaNode, YGNodeGetChildCount(newParentRef)); + } +} + +- (ASDisplayNode *)yogaParent +{ + return _yogaParent; +} + +- (void)setYogaCalculatedLayout:(ASLayout *)yogaCalculatedLayout +{ + _yogaCalculatedLayout = yogaCalculatedLayout; +} + +- (ASLayout *)yogaCalculatedLayout +{ + return _yogaCalculatedLayout; +} + +- (void)setYogaLayoutInProgress:(BOOL)yogaLayoutInProgress +{ + setFlag(YogaLayoutInProgress, yogaLayoutInProgress); + [self updateYogaMeasureFuncIfNeeded]; +} + +- (BOOL)yogaLayoutInProgress +{ + return checkFlag(YogaLayoutInProgress); +} + +- (ASLayout *)layoutForYogaNode +{ + YGNodeRef yogaNode = self.style.yogaNode; + + CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); + CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode)); + + if (!ASIsCGSizeValidForSize(size)) { + size = CGSizeZero; + } + + if (!ASIsCGPositionValidForLayout(position)) { + position = CGPointZero; + } + return [ASLayout layoutWithLayoutElement:self size:size position:position sublayouts:nil]; +} + +- (void)setupYogaCalculatedLayout +{ + ASScopedLockSelfOrToRoot(); + + YGNodeRef yogaNode = self.style.yogaNode; + uint32_t childCount = YGNodeGetChildCount(yogaNode); + ASDisplayNodeAssert(childCount == _yogaChildren.count, + @"Yoga tree should always be in sync with .yogaNodes array! %@", + _yogaChildren); + + ASLayout *rawSublayouts[childCount]; + int i = 0; + for (ASDisplayNode *subnode in _yogaChildren) { + rawSublayouts[i++] = [subnode layoutForYogaNode]; + } + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:childCount]; + + // The layout for self should have position CGPointNull, but include the calculated size. + CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); + if (!ASIsCGSizeValidForSize(size)) { + size = CGSizeZero; + } + ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // Assert that the sublayout is already flattened. + for (ASLayout *sublayout in layout.sublayouts) { + if (sublayout.sublayouts.count > 0 || ASDynamicCast(sublayout.layoutElement, ASDisplayNode) == nil) { + ASDisplayNodeAssert(NO, @"Yoga sublayout is not flattened! %@, %@", self, sublayout); + } + } +#endif + + // Because this layout won't go through the rest of the logic in calculateLayoutThatFits:, flatten it now. + layout = [layout filteredNodeLayoutTree]; + + if ([self.yogaCalculatedLayout isEqual:layout] == NO) { + self.yogaCalculatedLayout = layout; + } else { + layout = self.yogaCalculatedLayout; + ASYogaLog("-setupYogaCalculatedLayout: applying identical ASLayout: %@", layout); + } + + // Setup _pendingDisplayNodeLayout to reference the Yoga-calculated ASLayout, *unless* we are a leaf node. + // Leaf yoga nodes may have their own .sublayouts, if they use a layout spec (such as ASButtonNode). + // Their _pending variable is set after passing the Yoga checks at the start of -calculateLayoutThatFits: + + // For other Yoga nodes, there is no code that will set _pending unless we do it here. Why does it need to be set? + // When CALayer triggers the -[ASDisplayNode __layout] call, we will check if our current _pending layout + // has a size which matches our current bounds size. If it does, that layout will be used without recomputing it. + + // NOTE: Yoga does not make the constrainedSize available to intermediate nodes in the tree (e.g. not root or leaves). + // Although the size range provided here is not accurate, this will only affect caching of calls to layoutThatFits: + // These calls will behave as if they are not cached, starting a new Yoga layout pass, but this will tap into Yoga's + // own internal cache. + + if ([self shouldHaveYogaMeasureFunc] == NO) { + YGNodeRef parentNode = YGNodeGetParent(yogaNode); + CGSize parentSize = CGSizeZero; + if (parentNode) { + parentSize.width = YGNodeLayoutGetWidth(parentNode); + parentSize.height = YGNodeLayoutGetHeight(parentNode); + } + // For the root node in a Yoga tree, make sure to preserve the constrainedSize originally provided. + // This will be used for all relayouts triggered by children, since they escalate to root. + ASSizeRange range = parentNode ? ASSizeRangeUnconstrained : self.constrainedSizeForCalculatedLayout; + _pendingDisplayNodeLayout = ASDisplayNodeLayout(layout, range, parentSize, _layoutVersion); + } +} + +- (BOOL)shouldHaveYogaMeasureFunc +{ + ASLockScopeSelf(); + // Size calculation via calculateSizeThatFits: or layoutSpecThatFits: + // For these nodes, we assume they may need custom Baseline calculation too. + // This will be used for ASTextNode, as well as any other node that has no Yoga children + BOOL isLeafNode = (_yogaChildren.count == 0); + BOOL definesCustomLayout = [self implementsLayoutMethod]; + return (isLeafNode && definesCustomLayout); +} + +- (void)updateYogaMeasureFuncIfNeeded +{ + // We set the measure func only during layout. Otherwise, a cycle is created: + // The YGNodeRef Context will retain the ASDisplayNode, which retains the style, which owns the YGNodeRef. + BOOL shouldHaveMeasureFunc = ([self shouldHaveYogaMeasureFunc] && checkFlag(YogaLayoutInProgress)); + + ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, shouldHaveMeasureFunc ? self : nil); +} + +- (void)invalidateCalculatedYogaLayout +{ + ASLockScopeSelf(); + YGNodeRef yogaNode = self.style.yogaNode; + if (yogaNode && [self shouldHaveYogaMeasureFunc]) { + // Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function. + BOOL needsTemporaryMeasureFunc = (YGNodeGetMeasureFunc(yogaNode) == NULL); + if (needsTemporaryMeasureFunc) { + ASDisplayNodeAssert(self.yogaLayoutInProgress == NO, + @"shouldHaveYogaMeasureFunc == YES, and inside a layout pass, but no measure func pointer! %@", self); + YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); + } + YGNodeMarkDirty(yogaNode); + if (needsTemporaryMeasureFunc) { + YGNodeSetMeasureFunc(yogaNode, NULL); + } + } + self.yogaCalculatedLayout = nil; +} + +- (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize +{ + AS::UniqueLock l(__instanceLock__); + + // There are several cases where Yoga could arrive here: + // - This node is not in a Yoga tree: it has neither a yogaParent nor yogaChildren. + // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. + // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. + // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. + if ([self locked_shouldLayoutFromYogaRoot]) { + // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then + // initiate a new Yoga calculation pass from root. + as_activity_create_for_scope("Yoga layout calculation"); + if (self.yogaLayoutInProgress == NO) { + ASYogaLog("Calculating yoga layout from root %@, %@", self, + NSStringFromASSizeRange(constrainedSize)); + [self calculateLayoutFromYogaRoot:constrainedSize]; + } else { + ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); + } + ASDisplayNodeAssert(_yogaCalculatedLayout, + @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; + } else { + // If we're a yoga leaf node with custom measurement function, proceed with normal layout so + // layoutSpecs can run (e.g. ASButtonNode). + ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); + } + + // Delegate to layout spec layout for nodes that do not support Yoga + return [self calculateLayoutLayoutSpec:constrainedSize]; +} + +- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize +{ + ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout]; + ASDisplayNode *yogaRoot = self.yogaRoot; + + if (self != yogaRoot) { + ASYogaLog("ESCALATING to Yoga root: %@", self); + // TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually. + [yogaRoot calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained]; + return; + } + + if (ASSizeRangeEqualToSizeRange(rootConstrainedSize, ASSizeRangeUnconstrained)) { + rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; + } + + [self willCalculateLayout:rootConstrainedSize]; + [self enumerateInterfaceStateDelegates:^(id _Nonnull delegate) { + if ([delegate respondsToSelector:@selector(nodeWillCalculateLayout:)]) { + [delegate nodeWillCalculateLayout:rootConstrainedSize]; + } + }]; + + // Prepare all children for the layout pass with the current Yoga tree configuration. + ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode *_Nonnull node) { + node.yogaLayoutInProgress = YES; + ASDisplayNode *yogaParent = node.yogaParent; + if (yogaParent) { + node.style.parentAlignStyle = yogaParent.style.alignItems; + } else { + node.style.parentAlignStyle = ASStackLayoutAlignItemsNotSet; + }; + }); + + ASYogaLog("CALCULATING at Yoga root with constraint = {%@, %@}: %@", + NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self); + + YGNodeRef rootYogaNode = self.style.yogaNode; + + // Apply the constrainedSize as a base, known frame of reference. + // If the root node also has style.*Size set, these will be overridden below. + // YGNodeCalculateLayout currently doesn't offer the ability to pass a minimum size (max is passed there). + + // TODO(appleguy): Reconcile the self.style.*Size properties with rootConstrainedSize + YGNodeStyleSetMinWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.width)); + YGNodeStyleSetMinHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.height)); + + // It is crucial to use yogaFloat... to convert CGFLOAT_MAX into YGUndefined here. + YGNodeCalculateLayout(rootYogaNode, + yogaFloatForCGFloat(rootConstrainedSize.max.width), + yogaFloatForCGFloat(rootConstrainedSize.max.height), + YGDirectionInherit); + + // Reset accessible elements, since layout may have changed. + ASPerformBlockOnMainThread(^{ + if (self.nodeLoaded && !self.isSynchronous) { + [(_ASDisplayView *)self.view setAccessibilityElements:nil]; + } + }); + + ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { + [node setupYogaCalculatedLayout]; + node.yogaLayoutInProgress = NO; + }); + +#if YOGA_LAYOUT_LOGGING /* YOGA_LAYOUT_LOGGING */ + // Concurrent layouts will interleave the NSLog messages unless we serialize. + // Use @synchornize rather than trampolining to the main thread so the tree state isn't changed. + @synchronized ([ASDisplayNode class]) { + NSLog(@"****************************************************************************"); + NSLog(@"******************** STARTING YOGA -> ASLAYOUT CREATION ********************"); + NSLog(@"****************************************************************************"); + ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { + NSLog(@"node = %@", node); + YGNodePrint(node.style.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); + NSCAssert(ASIsCGSizeValidForSize(node.yogaCalculatedLayout.size), @"Yoga layout returned an invalid size"); + NSLog(@" "); // Newline + }); + } +#endif /* YOGA_LAYOUT_LOGGING */ +} + +@end + +#pragma mark - ASDisplayNode (YogaLocking) + +@implementation ASDisplayNode (YogaLocking) + +- (ASLockSet)lockToRootIfNeededForLayout { + ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { + if (!addLock(self)) { + return NO; + } +#if YOGA + if (![self locked_shouldLayoutFromYogaRoot]) { + return YES; + } + if (self.nodeController && !addLock(self.nodeController)) { + return NO; + } + ASDisplayNode *parent = _supernode; + while (parent) { + if (!addLock(parent)) { + return NO; + } + if (parent.nodeController && !addLock(parent.nodeController)) { + return NO; + } + parent = parent->_supernode; + } +#endif + return true; + }); + return lockSet; +} + +@end + +@implementation ASDisplayNode (YogaDebugging) + +- (NSString *)yogaTreeDescription { + return [self _yogaTreeDescription:@""]; +} + +- (NSString *)_yogaTreeDescription:(NSString *)indent { + auto subtree = [NSMutableString stringWithFormat:@"%@%@\n", indent, self.description]; + for (ASDisplayNode *n in self.yogaChildren) { + [subtree appendString:[n _yogaTreeDescription:[indent stringByAppendingString:@"| "]]]; + } + return subtree; +} + +@end + +#endif /* YOGA */ diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode.h new file mode 100644 index 0000000000..a3475892df --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.h @@ -0,0 +1,986 @@ +// +// ASDisplayNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#define ASDisplayNodeLoggingEnabled 0 + +#ifndef AS_MAX_INTERFACE_STATE_DELEGATES +#define AS_MAX_INTERFACE_STATE_DELEGATES 4 +#endif + +@class ASDisplayNode; +@protocol ASContextTransitioning; + +/** + * UIView creation block. Used to create the backing view of a new display node. + */ +typedef UIView * _Nonnull(^ASDisplayNodeViewBlock)(void); + +/** + * UIView creation block. Used to create the backing view of a new display node. + */ +typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(void); + +/** + * CALayer creation block. Used to create the backing layer of a new display node. + */ +typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(void); + +/** + * ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread. + */ +typedef void (^ASDisplayNodeDidLoadBlock)(__kindof ASDisplayNode * node); + +/** + * ASDisplayNode will / did render node content in context. + */ +typedef void (^ASDisplayNodeContextModifier)(CGContextRef context, id _Nullable drawParameters); + +/** + * ASDisplayNode layout spec block. This block can be used instead of implementing layoutSpecThatFits: in subclass + */ +typedef ASLayoutSpec * _Nonnull(^ASLayoutSpecBlock)(__kindof ASDisplayNode *node, ASSizeRange constrainedSize); + +/** + * AsyncDisplayKit non-fatal error block. This block can be used for handling non-fatal errors. Useful for reporting + * errors that happens in production. + */ +typedef void (^ASDisplayNodeNonFatalErrorBlock)(NSError *error); + +typedef NS_ENUM(NSInteger, ASCornerRoundingType) { + ASCornerRoundingTypeDefaultSlowCALayer, + ASCornerRoundingTypePrecomposited, + ASCornerRoundingTypeClipping +}; + +/** + * Default drawing priority for display node + */ +AS_EXTERN NSInteger const ASDefaultDrawingPriority; + +/** + * An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view + * hierarchy off the main thread, and could do rendering off the main thread as well. + * + * The node API is designed to be as similar as possible to `UIView`. See the README for examples. + * + * ## Subclassing + * + * `ASDisplayNode` can be subclassed to create a new UI element. The subclass header `ASDisplayNode+Subclasses` provides + * necessary declarations and conveniences. + * + * Commons reasons to subclass includes making a `UIView` property available and receiving a callback after async + * display. + * + */ + +@interface ASDisplayNode : NSObject { +@public + /** + * The _displayNodeContext ivar is unused by Texture, but provided to enable advanced clients to make powerful extensions to base class functionality. + * For example, _displayNodeContext can be used to implement category methods on ASDisplayNode that add functionality to all node subclass types. + * Code demonstrating this technique can be found in the CatDealsCollectionView example. + */ + void *_displayNodeContext; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock + isRasterizing:(BOOL)isRasterizing; ++ (nullable UIImage *)displayWithParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; + +/** @name Initializing a node object */ + + +/** + * @abstract Designated initializer. + * + * @return An ASDisplayNode instance whose view will be a subclass that enables asynchronous rendering, and passes + * through -layout and touch handling methods. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + + +/** + * @abstract Alternative initializer with a block to create the backing view. + * + * @param viewBlock The block that will be used to create the backing view. + * + * @return An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main + * queue. The view will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock; + +/** + * @abstract Alternative initializer with a block to create the backing view. + * + * @param viewBlock The block that will be used to create the backing view. + * @param didLoadBlock The block that will be called after the view created by the viewBlock is loaded + * + * @return An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main + * queue. The view will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_DESIGNATED_INITIALIZER; + +/** + * @abstract Alternative initializer with a block to create the backing layer. + * + * @param layerBlock The block that will be used to create the backing layer. + * + * @return An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main + * queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock; + +/** + * @abstract Alternative initializer with a block to create the backing layer. + * + * @param layerBlock The block that will be used to create the backing layer. + * @param didLoadBlock The block that will be called after the layer created by the layerBlock is loaded + * + * @return An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main + * queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_DESIGNATED_INITIALIZER; + +/** + * @abstract Add a block of work to be performed on the main thread when the node's view or layer is loaded. Thread safe. + * @warning Be careful not to retain self in `body`. Change the block parameter list to `^(MYCustomNode *self) {}` if you + * want to shadow self (e.g. if calling this during `init`). + * + * @param body The work to be performed when the node is loaded. + * + * @precondition The node is not already loaded. + */ +- (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body; + +/** + * Set the block that should be used to load this node's view. + * + * @param viewBlock The block that creates a view for this node. + * + * @precondition The node is not yet loaded. + * + * @note You will usually NOT call this. See the limitations documented in @c initWithViewBlock: + */ +- (void)setViewBlock:(ASDisplayNodeViewBlock)viewBlock; + +/** + * Set the block that should be used to load this node's layer. + * + * @param layerBlock The block that creates a layer for this node. + * + * @precondition The node is not yet loaded. + * + * @note You will usually NOT call this. See the limitations documented in @c initWithLayerBlock: + */ +- (void)setLayerBlock:(ASDisplayNodeLayerBlock)layerBlock; + +/** + * @abstract Returns whether the node is synchronous. + * + * @return NO if the node wraps a _ASDisplayView, YES otherwise. + */ +@property (readonly, getter=isSynchronous) BOOL synchronous; + +/** @name Getting view and layer */ + +/** + * @abstract Returns a view. + * + * @discussion The view property is lazily initialized, similar to UIViewController. + * To go the other direction, use ASViewToDisplayNode() in ASDisplayNodeExtras.h. + * + * @warning The first access to it must be on the main thread, and should only be used on the main thread thereafter as + * well. + */ +@property (readonly) UIView *view; + +/** + * @abstract Returns whether a node's backing view or layer is loaded. + * + * @return YES if a view is loaded, or if layerBacked is YES and layer is not nil; NO otherwise. + */ +@property (readonly, getter=isNodeLoaded) BOOL nodeLoaded; + +/** + * @abstract Returns whether the node rely on a layer instead of a view. + * + * @return YES if the node rely on a layer, NO otherwise. + */ +@property (getter=isLayerBacked) BOOL layerBacked; + +/** + * @abstract Returns a layer. + * + * @discussion The layer property is lazily initialized, similar to the view property. + * To go the other direction, use ASLayerToDisplayNode() in ASDisplayNodeExtras.h. + * + * @warning The first access to it must be on the main thread, and should only be used on the main thread thereafter as + * well. + */ +@property (readonly) CALayer * layer; + +/** + * Returns YES if the node is – at least partially – visible in a window. + * + * @see didEnterVisibleState and didExitVisibleState + */ +@property (readonly, getter=isVisible) BOOL visible; + +/** + * Returns YES if the node is in the preloading interface state. + * + * @see didEnterPreloadState and didExitPreloadState + */ +@property (readonly, getter=isInPreloadState) BOOL inPreloadState; + +/** + * Returns YES if the node is in the displaying interface state. + * + * @see didEnterDisplayState and didExitDisplayState + */ +@property (readonly, getter=isInDisplayState) BOOL inDisplayState; + +/** + * @abstract Returns the Interface State of the node. + * + * @return The current ASInterfaceState of the node, indicating whether it is visible and other situational properties. + * + * @see ASInterfaceState + */ +@property (readonly) ASInterfaceState interfaceState; + +/** + * @abstract Adds a delegate to receive notifications on interfaceState changes. + * + * @warning This must be called from the main thread. + * There is a hard limit on the number of delegates a node can have; see + * AS_MAX_INTERFACE_STATE_DELEGATES above. + * + * @see ASInterfaceState + */ +- (void)addInterfaceStateDelegate:(id )interfaceStateDelegate; + +/** + * @abstract Removes a delegate from receiving notifications on interfaceState changes. + * + * @warning This must be called from the main thread. + * + * @see ASInterfaceState + */ +- (void)removeInterfaceStateDelegate:(id )interfaceStateDelegate; + +/** + * @abstract Class property that allows to set a block that can be called on non-fatal errors. This + * property can be useful for cases when Async Display Kit can recover from an abnormal behavior, but + * still gives the opportunity to use a reporting mechanism to catch occurrences in production. In + * development, Async Display Kit will assert instead of calling this block. + * + * @warning This method is not thread-safe. + */ +@property (class, nonatomic) ASDisplayNodeNonFatalErrorBlock nonFatalErrorBlock; + +/** @name Managing the nodes hierarchy */ + + +/** + * @abstract Add a node as a subnode to this node. + * + * @param subnode The node to be added. + * + * @discussion The subnode's view will automatically be added to this node's view, lazily if the views are not created + * yet. + */ +- (void)addSubnode:(ASDisplayNode *)subnode; + +/** + * @abstract Insert a subnode before a given subnode in the list. + * + * @param subnode The node to insert below another node. + * @param below The sibling node that will be above the inserted node. + * + * @discussion If the views are loaded, the subnode's view will be inserted below the given node's view in the hierarchy + * even if there are other non-displaynode views. + */ +- (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below; + +/** + * @abstract Insert a subnode after a given subnode in the list. + * + * @param subnode The node to insert below another node. + * @param above The sibling node that will be behind the inserted node. + * + * @discussion If the views are loaded, the subnode's view will be inserted above the given node's view in the hierarchy + * even if there are other non-displaynode views. + */ +- (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above; + +/** + * @abstract Insert a subnode at a given index in subnodes. + * + * @param subnode The node to insert. + * @param idx The index in the array of the subnodes property at which to insert the node. Subnodes indices start at 0 + * and cannot be greater than the number of subnodes. + * + * @discussion If this node's view is loaded, ASDisplayNode insert the subnode's view after the subnode at index - 1's + * view even if there are other non-displaynode views. + */ +- (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx; + +/** + * @abstract Replace subnode with replacementSubnode. + * + * @param subnode A subnode of self. + * @param replacementSubnode A node with which to replace subnode. + * + * @discussion Should both subnode and replacementSubnode already be subnodes of self, subnode is removed and + * replacementSubnode inserted in its place. + * If subnode is not a subnode of self, this method will throw an exception. + * If replacementSubnode is nil, this method will throw an exception + */ +- (void)replaceSubnode:(ASDisplayNode *)subnode withSubnode:(ASDisplayNode *)replacementSubnode; + +/** + * @abstract Remove this node from its supernode. + * + * @discussion The node's view will be automatically removed from the supernode's view. + */ +- (void)removeFromSupernode; + +/** + * @abstract The receiver's immediate subnodes. + */ +@property (nullable, readonly, copy) NSArray *subnodes; + +/** + * @abstract The receiver's supernode. + */ +@property (nullable, readonly, weak) ASDisplayNode *supernode; + + +/** @name Drawing and Updating the View */ + +/** + * @abstract Whether this node's view performs asynchronous rendering. + * + * @return Defaults to YES, except for synchronous views (ie, those created with -initWithViewBlock: / + * -initWithLayerBlock:), which are always NO. + * + * @discussion If this flag is set, then the node will participate in the current asyncdisplaykit_async_transaction and + * do its rendering on the displayQueue instead of the main thread. + * + * Asynchronous rendering proceeds as follows: + * + * When the view is initially added to the hierarchy, it has -needsDisplay true. + * After layout, Core Animation will call -display on the _ASDisplayLayer + * -display enqueues a rendering operation on the displayQueue + * When the render block executes, it calls the delegate display method (-drawRect:... or -display) + * The delegate provides contents via this method and an operation is added to the asyncdisplaykit_async_transaction + * Once all rendering is complete for the current asyncdisplaykit_async_transaction, + * the completion for the block sets the contents on all of the layers in the same frame + * + * If asynchronous rendering is disabled: + * + * When the view is initially added to the hierarchy, it has -needsDisplay true. + * After layout, Core Animation will call -display on the _ASDisplayLayer + * -display calls delegate display method (-drawRect:... or -display) immediately + * -display sets the layer contents immediately with the result + * + * Note: this has nothing to do with -[CALayer drawsAsynchronously]. + */ +@property BOOL displaysAsynchronously; + +/** + * @abstract Prevent the node's layer from displaying. + * + * @discussion A subclass may check this flag during -display or -drawInContext: to cancel a display that is already in + * progress. + * + * Defaults to NO. Does not control display for any child or descendant nodes; for that, use + * -recursivelySetDisplaySuspended:. + * + * If a setNeedsDisplay occurs while displaySuspended is YES, and displaySuspended is set to NO, then the + * layer will be automatically displayed. + */ +@property BOOL displaySuspended; + +/** + * @abstract Whether size changes should be animated. Default to YES. + */ +@property BOOL shouldAnimateSizeChanges; + +/** + * @abstract Prevent the node and its descendants' layer from displaying. + * + * @param flag YES if display should be prevented or cancelled; NO otherwise. + * + * @see displaySuspended + */ +- (void)recursivelySetDisplaySuspended:(BOOL)flag; + +/** + * @abstract Calls -clearContents on the receiver and its subnode hierarchy. + * + * @discussion Clears backing stores and other memory-intensive intermediates. + * If the node is removed from a visible hierarchy and then re-added, it will automatically trigger a new asynchronous display, + * as long as displaySuspended is not set. + * If the node remains in the hierarchy throughout, -setNeedsDisplay is required to trigger a new asynchronous display. + * + * @see displaySuspended and setNeedsDisplay + */ +- (void)recursivelyClearContents; + +/** + * @abstract Toggle displaying a placeholder over the node that covers content until the node and all subnodes are + * displayed. + * + * @discussion Defaults to NO. + */ +@property BOOL placeholderEnabled; + +/** + * @abstract Set the time it takes to fade out the placeholder when a node's contents are finished displaying. + * + * @discussion Defaults to 0 seconds. + */ +@property NSTimeInterval placeholderFadeDuration; + +/** + * @abstract Determines drawing priority of the node. Nodes with higher priority will be drawn earlier. + * + * @discussion Defaults to ASDefaultDrawingPriority. There may be multiple drawing threads, and some of them may + * decide to perform operations in queued order (regardless of drawingPriority) + */ +@property NSInteger drawingPriority; + +/** @name Hit Testing */ + + +/** + * @abstract Bounds insets for hit testing. + * + * @discussion When set to a non-zero inset, increases the bounds for hit testing to make it easier to tap or perform + * gestures on this node. Default is UIEdgeInsetsZero. + * + * This affects the default implementation of -hitTest and -pointInside, so subclasses should call super if you override + * it and want hitTestSlop applied. + */ +@property UIEdgeInsets hitTestSlop; + +/** + * @abstract Returns a Boolean value indicating whether the receiver contains the specified point. + * + * @discussion Includes the "slop" factor specified with hitTestSlop. + * + * @param point A point that is in the receiver's local coordinate system (bounds). + * @param event The event that warranted a call to this method. + * + * @return YES if point is inside the receiver's bounds; otherwise, NO. + */ +- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event AS_WARN_UNUSED_RESULT; + + +/** @name Converting Between View Coordinate Systems */ + + +/** + * @abstract Converts a point from the receiver's coordinate system to that of the specified node. + * + * @param point A point specified in the local coordinate system (bounds) of the receiver. + * @param node The node into whose coordinate system point is to be converted. + * + * @return The point converted to the coordinate system of node. + */ +- (CGPoint)convertPoint:(CGPoint)point toNode:(nullable ASDisplayNode *)node AS_WARN_UNUSED_RESULT; + + +/** + * @abstract Converts a point from the coordinate system of a given node to that of the receiver. + * + * @param point A point specified in the local coordinate system (bounds) of node. + * @param node The node with point in its coordinate system. + * + * @return The point converted to the local coordinate system (bounds) of the receiver. + */ +- (CGPoint)convertPoint:(CGPoint)point fromNode:(nullable ASDisplayNode *)node AS_WARN_UNUSED_RESULT; + + +/** + * @abstract Converts a rectangle from the receiver's coordinate system to that of another view. + * + * @param rect A rectangle specified in the local coordinate system (bounds) of the receiver. + * @param node The node that is the target of the conversion operation. + * + * @return The converted rectangle. + */ +- (CGRect)convertRect:(CGRect)rect toNode:(nullable ASDisplayNode *)node AS_WARN_UNUSED_RESULT; + +/** + * @abstract Converts a rectangle from the coordinate system of another node to that of the receiver. + * + * @param rect A rectangle specified in the local coordinate system (bounds) of node. + * @param node The node with rect in its coordinate system. + * + * @return The converted rectangle. + */ +- (CGRect)convertRect:(CGRect)rect fromNode:(nullable ASDisplayNode *)node AS_WARN_UNUSED_RESULT; + +/** + * Whether or not the node would support having .layerBacked = YES. + */ +@property (readonly) BOOL supportsLayerBacking; + +/** + * Whether or not the node layout should be automatically updated when it receives safeAreaInsetsDidChange. + * + * Defaults to NO. + */ +@property BOOL automaticallyRelayoutOnSafeAreaChanges; + +/** + * Whether or not the node layout should be automatically updated when it receives layoutMarginsDidChange. + * + * Defaults to NO. + */ +@property BOOL automaticallyRelayoutOnLayoutMarginsChanges; + +@end + +/** + * Convenience methods for debugging. + */ +@interface ASDisplayNode (Debugging) + +/** + * Whether or not ASDisplayNode instances should store their unflattened layouts. + * + * The layout can be accessed via `-unflattenedCalculatedLayout`. + * + * Flattened layouts use less memory and are faster to lookup. On the other hand, unflattened layouts are useful for debugging + * because they preserve original information. + * + * Defaults to NO. + */ +@property (class) BOOL shouldStoreUnflattenedLayouts; + +@property (nullable, readonly) ASLayout *unflattenedCalculatedLayout; + +/** + * @abstract Return a description of the node hierarchy. + * + * @discussion For debugging: (lldb) po [node displayNodeRecursiveDescription] + */ +- (NSString *)displayNodeRecursiveDescription AS_WARN_UNUSED_RESULT; + +/** + * A detailed description of this node's layout state. This is useful when debugging. + */ +@property (copy, readonly) NSString *detailedLayoutDescription; + +@end + +/** + * ## UIView bridge + * + * ASDisplayNode provides thread-safe access to most of UIView and CALayer properties and methods, traditionally unsafe. + * + * Using them will not cause the actual view/layer to be created, and will be applied when it is created (when the view + * or layer property is accessed). + * + * - NOTE: After the view or layer is created, the properties pass through to the view or layer directly and must be called on the main thread. + * + * See UIView and CALayer for documentation on these common properties. + */ +@interface ASDisplayNode (UIViewBridge) + +/** + * Marks the view as needing display. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. + */ +- (void)setNeedsDisplay; + +/** + * Marks the node as needing layout. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. + * + * If the node determines its own desired layout size will change in the next layout pass, it will propagate this + * information up the tree so its parents can have a chance to consider and apply if necessary the new size onto the node. + * + * Note: ASCellNode has special behavior in that calling this method will automatically notify + * the containing ASTableView / ASCollectionView that the cell should be resized, if necessary. + */ +- (void)setNeedsLayout; + +/** + * Performs a layout pass on the node. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. + */ +- (void)layoutIfNeeded; + +@property CGRect frame; // default=CGRectZero +@property CGRect bounds; // default=CGRectZero +@property CGPoint position; // default=CGPointZero +@property CGFloat alpha; // default=1.0f + +/* @abstract Sets the corner rounding method to use on the ASDisplayNode. + * There are three types of corner rounding provided by Texture: CALayer, Precomposited, and Clipping. + * + * - ASCornerRoundingTypeDefaultSlowCALayer: uses CALayer's inefficient .cornerRadius property. Use + * this type of corner in situations in which there is both movement through and movement underneath + * the corner (very rare). This uses only .cornerRadius. + * + * - ASCornerRoundingTypePrecomposited: corners are drawn using bezier paths to clip the content in a + * CGContext / UIGraphicsContext. This requires .backgroundColor and .cornerRadius to be set. Use opaque + * background colors when possible for optimal efficiency, but transparent colors are supported and much + * more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and + * thus works best for ASImageNodes or containers showing a background around their children. + * + * - ASCornerRoundingTypeClipping: overlays 4 separate opaque corners on top of the content that needs + * corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations + * in which is movement through the corner, with an opaque background (no movement underneath the corner). + * Clipped corners are ideal for animating / resizing views, and still outperform CALayer. + * + * For more information and examples, see http://texturegroup.org/docs/corner-rounding.html + * + * @default ASCornerRoundingTypeDefaultSlowCALayer + */ +@property ASCornerRoundingType cornerRoundingType; // default=ASCornerRoundingTypeDefaultSlowCALayer .cornerRadius (offscreen rendering) + +/** @abstract The radius to use when rounding corners of the ASDisplayNode. + * + * @discussion This property is thread-safe and should always be preferred over CALayer's cornerRadius property, + * even if corner rounding type is ASCornerRoundingTypeDefaultSlowCALayer. + */ +@property CGFloat cornerRadius; // default=0.0 + +@property BOOL clipsToBounds; // default==NO +@property (getter=isHidden) BOOL hidden; // default==NO +@property (getter=isOpaque) BOOL opaque; // default==YES + +@property (nullable) id contents; // default=nil +@property CGRect contentsRect; // default={0,0,1,1}. @see CALayer.h for details. +@property CGRect contentsCenter; // default={0,0,1,1}. @see CALayer.h for details. +@property CGFloat contentsScale; // default=1.0f. See @contentsScaleForDisplay for details. +@property CGFloat rasterizationScale; // default=1.0f. + +@property CGPoint anchorPoint; // default={0.5, 0.5} +@property CGFloat zPosition; // default=0.0 +@property CATransform3D transform; // default=CATransform3DIdentity +@property CATransform3D subnodeTransform; // default=CATransform3DIdentity + +@property (getter=isUserInteractionEnabled) BOOL userInteractionEnabled; // default=YES (NO for layer-backed nodes) +#if TARGET_OS_IOS +@property (getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO +#endif + +/** + * @abstract The node view's background color. + * + * @discussion In contrast to UIView, setting a transparent color will not set opaque = NO. + * This only affects nodes that implement +drawRect like ASTextNode. +*/ +@property (nullable, copy) UIColor *backgroundColor; // default=nil + +@property (null_resettable, copy) UIColor *tintColor; // default=Blue +- (void)tintColorDidChange; // Notifies the node when the tintColor has changed. + +/** + * @abstract A flag used to determine how a node lays out its content when its bounds change. + * + * @discussion This is like UIView's contentMode property, but better. We do our own mapping to layer.contentsGravity in + * _ASDisplayView. You can set needsDisplayOnBoundsChange independently. + * Thus, UIViewContentModeRedraw is not allowed; use needsDisplayOnBoundsChange = YES instead, and pick an appropriate + * contentMode for your content while it's being re-rendered. + */ +@property UIViewContentMode contentMode; // default=UIViewContentModeScaleToFill +@property (copy) NSString *contentsGravity; // Use .contentMode in preference when possible. +@property UISemanticContentAttribute semanticContentAttribute; + +@property (nullable) CGColorRef shadowColor; // default=opaque rgb black +@property CGFloat shadowOpacity; // default=0.0 +@property CGSize shadowOffset; // default=(0, -3) +@property CGFloat shadowRadius; // default=3 +@property CGFloat borderWidth; // default=0 +@property (nullable) CGColorRef borderColor; // default=opaque rgb black + +@property BOOL allowsGroupOpacity; +@property BOOL allowsEdgeAntialiasing; +@property unsigned int edgeAntialiasingMask; // default==all values from CAEdgeAntialiasingMask + +@property BOOL needsDisplayOnBoundsChange; // default==NO +@property BOOL autoresizesSubviews; // default==YES (undefined for layer-backed nodes) +@property UIViewAutoresizing autoresizingMask; // default==UIViewAutoresizingNone (undefined for layer-backed nodes) + +/** + * @abstract Content margins + * + * @discussion This property is bridged to its UIView counterpart. + * + * If your layout depends on this property, you should probably enable automaticallyRelayoutOnLayoutMarginsChanges to ensure + * that the layout gets automatically updated when the value of this property changes. Or you can override layoutMarginsDidChange + * and make all the necessary updates manually. + */ +@property UIEdgeInsets layoutMargins; +@property BOOL preservesSuperviewLayoutMargins; // default is NO - set to enable pass-through or cascading behavior of margins from this view’s parent to its children +- (void)layoutMarginsDidChange; + +/** + * @abstract Safe area insets + * + * @discussion This property is bridged to its UIVIew counterpart. + * + * If your layout depends on this property, you should probably enable automaticallyRelayoutOnSafeAreaChanges to ensure + * that the layout gets automatically updated when the value of this property changes. Or you can override safeAreaInsetsDidChange + * and make all the necessary updates manually. + */ +@property (readonly) UIEdgeInsets safeAreaInsets; +@property BOOL insetsLayoutMarginsFromSafeArea; // Default: YES +- (void)safeAreaInsetsDidChange; + + +// UIResponder methods +// By default these fall through to the underlying view, but can be overridden. +- (BOOL)canBecomeFirstResponder; // default==NO +- (BOOL)becomeFirstResponder; // default==NO (no-op) +- (BOOL)canResignFirstResponder; // default==YES +- (BOOL)resignFirstResponder; // default==NO (no-op) +- (BOOL)isFirstResponder; +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender; + +#if TARGET_OS_TV +//Focus Engine +- (void)setNeedsFocusUpdate; +- (BOOL)canBecomeFocused; +- (void)updateFocusIfNeeded; +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator; +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context; +- (nullable UIView *)preferredFocusedView; +#endif + +@end + +@interface ASDisplayNode (UIViewBridgeAccessibility) + +// Accessibility support +@property BOOL isAccessibilityElement; +@property (nullable, copy) NSString *accessibilityLabel; +@property (nullable, copy) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0)); +@property (nullable, copy) NSString *accessibilityHint; +@property (nullable, copy) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0)); +@property (nullable, copy) NSString *accessibilityValue; +@property (nullable, copy) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0)); +@property UIAccessibilityTraits accessibilityTraits; +@property CGRect accessibilityFrame; +@property (nullable, copy) UIBezierPath *accessibilityPath; +@property CGPoint accessibilityActivationPoint; +@property (nullable, copy) NSString *accessibilityLanguage; +@property BOOL accessibilityElementsHidden; +@property BOOL accessibilityViewIsModal; +@property BOOL shouldGroupAccessibilityChildren; +@property UIAccessibilityNavigationStyle accessibilityNavigationStyle; +#if TARGET_OS_TV +@property (nullable, copy) NSArray *accessibilityHeaderElements; +#endif + +// Accessibility identification support +@property (nullable, copy) NSString *accessibilityIdentifier; + +@end + +@interface ASDisplayNode (ASLayoutElement) + +/** + * @abstract Asks the node to return a layout based on given size range. + * + * @param constrainedSize The minimum and maximum sizes the receiver should fit in. + * + * @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used). + * + * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the + * constraint and the result. + * + * @warning Subclasses must not override this; it caches results from -calculateLayoutThatFits:. Calling this method may + * be expensive if result is not cached. + * + * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] + */ +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize; +- (CGSize)measure:(CGSize)constrainedSize; + +@end + +@interface ASDisplayNode (ASLayoutElementStylability) + +@end + +typedef NS_ENUM(NSInteger, ASLayoutEngineType) { + ASLayoutEngineTypeLayoutSpec, + ASLayoutEngineTypeYoga +}; + +@interface ASDisplayNode (ASLayout) + +/** + * @abstract Returns the current layout type the node uses for layout the subtree. + */ +@property (readonly) ASLayoutEngineType layoutEngineType; + +/** + * @abstract Return the calculated size. + * + * @discussion Ideal for use by subclasses in -layout, having already prompted their subnodes to calculate their size by + * calling -layoutThatFits: on them in -calculateLayoutThatFits. + * + * @return Size already calculated by -calculateLayoutThatFits:. + * + * @warning Subclasses must not override this; it returns the last cached measurement and is never expensive. + */ +@property (readonly) CGSize calculatedSize; + +/** + * @abstract Return the constrained size range used for calculating layout. + * + * @return The minimum and maximum constrained sizes used by calculateLayoutThatFits:. + */ +@property (readonly) ASSizeRange constrainedSizeForCalculatedLayout; + +@end + +@interface ASDisplayNode (ASLayoutTransitioning) + +/** + * @abstract The amount of time it takes to complete the default transition animation. Default is 0.2. + */ +@property NSTimeInterval defaultLayoutTransitionDuration; + +/** + * @abstract The amount of time (measured in seconds) to wait before beginning the default transition animation. + * Default is 0.0. + */ +@property NSTimeInterval defaultLayoutTransitionDelay; + +/** + * @abstract A mask of options indicating how you want to perform the default transition animations. + * For a list of valid constants, see UIViewAnimationOptions. + */ +@property UIViewAnimationOptions defaultLayoutTransitionOptions; + +/** + * @discussion A place to perform your animation. New nodes have been inserted here. You can also use this time to re-order the hierarchy. + */ +- (void)animateLayoutTransition:(nonnull id)context; + +/** + * @discussion A place to clean up your nodes after the transition + */ +- (void)didCompleteLayoutTransition:(nonnull id)context; + +/** + * @abstract Transitions the current layout with a new constrained size. Must be called on main thread. + * + * @param animated Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * @param shouldMeasureAsync Measure the layout asynchronously. + * @param completion Optional completion block called only if a new layout is calculated. + * It is called on main, right after the measurement and before -animateLayoutTransition:. + * + * @discussion If the passed constrainedSize is the the same as the node's current constrained size, this method is noop. If passed YES to shouldMeasureAsync it's guaranteed that measurement is happening on a background thread, otherwise measaurement will happen on the thread that the method was called on. The measurementCompletion callback is always called on the main thread right after the measurement and before -animateLayoutTransition:. + * + * @see animateLayoutTransition: + * + */ +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(nullable void(^)(void))completion; + + +/** + * @abstract Invalidates the layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread. + * + * @discussion It is called right after the measurement and before -animateLayoutTransition:. + * + * @param animated Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * @param shouldMeasureAsync Measure the layout asynchronously. + * @param completion Optional completion block called only if a new layout is calculated. + * + * @see animateLayoutTransition: + * + */ +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(nullable void(^)(void))completion; + +/** + * @abstract Cancels all performing layout transitions. Can be called on any thread. + */ +- (void)cancelLayoutTransition; + +@end + +/* + * ASDisplayNode support for automatic subnode management. + */ +@interface ASDisplayNode (ASAutomaticSubnodeManagement) + +/** + * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or + * absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method. + * + * @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence + * or absence of subnodes is completely determined in its layoutSpecThatFits: method. + */ +@property BOOL automaticallyManagesSubnodes; + +@end + +/* + * ASDisplayNode participates in ASAsyncTransactions, so you can determine when your subnodes are done rendering. + * See: -(void)asyncdisplaykit_asyncTransactionContainerStateDidChange in ASDisplayNodeSubclass.h + */ +@interface ASDisplayNode (ASAsyncTransactionContainer) +@end + +/** UIVIew(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to an UIView. */ +@interface UIView (AsyncDisplayKit) +/** + * Convenience method, equivalent to [view addSubview:node.view] or [view.layer addSublayer:node.layer] if layer-backed. + * + * @param node The node to be added. + */ +- (void)addSubnode:(ASDisplayNode *)node; +@end + +/* + * CALayer(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to a CALayer. + */ +@interface CALayer (AsyncDisplayKit) +/** + * Convenience method, equivalent to [layer addSublayer:node.layer]. + * + * @param node The node to be added. + */ +- (void)addSubnode:(ASDisplayNode *)node; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm new file mode 100644 index 0000000000..f4e3238448 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm @@ -0,0 +1,3861 @@ +// +// ASDisplayNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import +#import + +#import +#include + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) +#if TIME_DISPLAYNODE_OPS + #define TIME_SCOPED(outVar) AS::ScopeTimer t(outVar) +#else + #define TIME_SCOPED(outVar) +#endif +// This is trying to merge non-rangeManaged with rangeManaged, so both range-managed and standalone nodes wait before firing their exit-visibility handlers, as UIViewController transitions now do rehosting at both start & end of animation. +// Enable this will mitigate interface updating state when coalescing disabled. +// TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. +#define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 0 + +using AS::MutexLocker; + +static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; + +// Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol. +// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 +@protocol CALayerDelegate; + +@interface ASDisplayNode () +/** + * See ASDisplayNodeInternal.h for ivars + */ + +@end + +@implementation ASDisplayNode + +@dynamic layoutElementType; + +@synthesize threadSafeBounds = _threadSafeBounds; + +static std::atomic_bool suppressesInvalidCollectionUpdateExceptions = ATOMIC_VAR_INIT(NO); +static std::atomic_bool storesUnflattenedLayouts = ATOMIC_VAR_INIT(NO); + +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 ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLayerBacked) +{ + return isSynchronous && !isLayerBacked; +} + +_ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node) +{ + ASLockScope(node); + _ASPendingState *result = node->_pendingViewState; + if (result == nil) { + result = [[_ASPendingState alloc] init]; + node->_pendingViewState = result; + } + return result; +} + +void StubImplementationWithNoArgs(id receiver) {} +void StubImplementationWithSizeRange(id receiver, ASSizeRange sr) {} +void StubImplementationWithTwoInterfaceStates(id receiver, ASInterfaceState s0, ASInterfaceState s1) {} + +/** + * Returns ASDisplayNodeFlags for the given class/instance. instance MAY BE NIL. + * + * @param c the class, required + * @param instance the instance, which may be nil. (If so, the class is inspected instead) + * @remarks The instance value is used only if we suspect the class may be dynamic (because it overloads + * +respondsToSelector: or -respondsToSelector.) In that case we use our "slow path", calling this + * method on each -init and passing the instance value. While this may seem like an unlikely scenario, + * it turns our our own internal tests use a dynamic class, so it's worth capturing this edge case. + * + * @return ASDisplayNode flags. + */ +static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *instance) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + struct ASDisplayNodeFlags flags = {0}; + + flags.isInHierarchy = NO; + flags.displaysAsynchronously = YES; + flags.shouldAnimateSizeChanges = YES; + flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); + flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); + if (instance) { + flags.implementsDrawParameters = ([instance respondsToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } else { + flags.implementsDrawParameters = ([c instancesRespondToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } + + + return flags; +} + +/** + * Returns ASDisplayNodeMethodOverrides for the given class + * + * @param c the class, required. + * + * @return ASDisplayNodeMethodOverrides. + */ +static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + ASDisplayNodeMethodOverrides overrides = ASDisplayNodeMethodOverrideNone; + + // Handling touches + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesBegan:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesBegan; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesMoved:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesMoved; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesCancelled:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesCancelled; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesEnded:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesEnded; + } + + // Responder chain + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canBecomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(becomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canResignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(resignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(isFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideIsFirstResponder; + } + + // Layout related methods + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(layoutSpecThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideLayoutSpecThatFits; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits:)) || + ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits: + restrictedToSize: + relativeToParentSize:))) { + overrides |= ASDisplayNodeMethodOverrideCalcLayoutThatFits; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideCalcSizeThatFits; + } + + return overrides; +} + ++ (void)initialize +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + if (self != [ASDisplayNode class]) { + + // Subclasses should never override these. Use unused to prevent warnings + __unused NSString *classString = NSStringFromClass(self); + + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:)), @"Subclass %@ must not override layoutThatFits: method. Instead override calculateLayoutThatFits:.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:parentSize:)), @"Subclass %@ must not override layoutThatFits:parentSize method. Instead override calculateLayoutThatFits:.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearPreloadedData)), @"Subclass %@ must not override recursivelyClearFetchedData method.", classString); + } else { + // Check if subnodes where modified during the creation of the layout + __block IMP originalLayoutSpecThatFitsIMP = ASReplaceMethodWithBlock(self, @selector(_locked_layoutElementThatFits:), ^(ASDisplayNode *_self, ASSizeRange sizeRange) { + NSArray *oldSubnodes = _self.subnodes; + ASLayoutSpec *layoutElement = ((ASLayoutSpec *( *)(id, SEL, ASSizeRange))originalLayoutSpecThatFitsIMP)(_self, @selector(_locked_layoutElementThatFits:), sizeRange); + NSArray *subnodes = _self.subnodes; + ASDisplayNodeAssert(oldSubnodes.count == subnodes.count, @"Adding or removing nodes in layoutSpecBlock or layoutSpecThatFits: is not allowed and can cause unexpected behavior."); + for (NSInteger i = 0; i < oldSubnodes.count; i++) { + ASDisplayNodeAssert(oldSubnodes[i] == subnodes[i], @"Adding or removing nodes in layoutSpecBlock or layoutSpecThatFits: is not allowed and can cause unexpected behavior."); + } + return layoutElement; + }); + } +#endif + + // Below we are pre-calculating values per-class and dynamically adding a method (_staticInitialize) to populate these values + // when each instance is constructed. These values don't change for each class, so there is significant performance benefit + // in doing it here. +initialize is guaranteed to be called before any instance method so it is safe to add this method here. + // Note that we take care to detect if the class overrides +respondsToSelector: or -respondsToSelector and take the slow path + // (recalculating for each instance) to make sure we are always correct. + + BOOL classOverridesRespondsToSelector = ASSubclassOverridesClassSelector([NSObject class], self, @selector(respondsToSelector:)); + BOOL instancesOverrideRespondsToSelector = ASSubclassOverridesSelector([NSObject class], self, @selector(respondsToSelector:)); + struct ASDisplayNodeFlags flags = GetASDisplayNodeFlags(self, nil); + ASDisplayNodeMethodOverrides methodOverrides = GetASDisplayNodeMethodOverrides(self); + + __unused Class initializeSelf = self; + + IMP staticInitialize = imp_implementationWithBlock(^(ASDisplayNode *node) { + ASDisplayNodeAssert(node.class == initializeSelf, @"Node class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", node.class, initializeSelf); + node->_flags = (classOverridesRespondsToSelector || instancesOverrideRespondsToSelector) ? GetASDisplayNodeFlags(node.class, node) : flags; + node->_methodOverrides = (classOverridesRespondsToSelector) ? GetASDisplayNodeMethodOverrides(node.class) : methodOverrides; + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); + + // Add stub implementations for global methods that the client didn't + // implement in a category. We do this instead of hard-coding empty + // implementations to avoid a linker warning when it merges categories. + // Note: addMethod will not do anything if a method already exists. + if (self == ASDisplayNode.class) { + IMP noArgsImp = (IMP)StubImplementationWithNoArgs; + class_addMethod(self, @selector(baseDidInit), noArgsImp, "v@:"); + class_addMethod(self, @selector(baseWillDealloc), noArgsImp, "v@:"); + class_addMethod(self, @selector(didLoad), noArgsImp, "v@:"); + class_addMethod(self, @selector(layoutDidFinish), noArgsImp, "v@:"); + class_addMethod(self, @selector(didEnterPreloadState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didExitPreloadState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didEnterDisplayState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didExitDisplayState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didEnterVisibleState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didExitVisibleState), noArgsImp, "v@:"); + class_addMethod(self, @selector(hierarchyDisplayDidFinish), noArgsImp, "v@:"); + class_addMethod(self, @selector(asyncTraitCollectionDidChange), noArgsImp, "v@:"); + class_addMethod(self, @selector(calculatedLayoutDidChange), noArgsImp, "v@:"); + + auto type0 = "v@:" + std::string(@encode(ASSizeRange)); + class_addMethod(self, @selector(willCalculateLayout:), (IMP)StubImplementationWithSizeRange, type0.c_str()); + + auto interfaceStateType = std::string(@encode(ASInterfaceState)); + auto type1 = "v@:" + interfaceStateType + interfaceStateType; + class_addMethod(self, @selector(interfaceStateDidChange:fromState:), (IMP)StubImplementationWithTwoInterfaceStates, type1.c_str()); + } +} + +#if !AS_INITIALIZE_FRAMEWORK_MANUALLY ++ (void)load +{ + ASInitializeFrameworkMainThread(); +} +#endif + ++ (Class)viewClass +{ + return [_ASDisplayView class]; +} + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - Lifecycle + +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + +- (void)_initializeInstance +{ + [self _staticInitialize]; + +#if ASEVENTLOG_ENABLE + _eventLog = [[ASEventLog alloc] initWithObject:self]; +#endif + + _viewClass = [self.class viewClass]; + _layerClass = [self.class layerClass]; + BOOL isSynchronous = ![_viewClass isSubclassOfClass:[_ASDisplayView class]] + || ![_layerClass isSubclassOfClass:[_ASDisplayLayer class]]; + setFlag(Synchronous, isSynchronous); + + + _contentsScaleForDisplay = ASScreenScale(); + _drawingPriority = ASDefaultTransactionPriority; + + _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + + _layoutVersion = 1; + + _defaultLayoutTransitionDuration = 0.2; + _defaultLayoutTransitionDelay = 0.0; + _defaultLayoutTransitionOptions = UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionNone; + + _flags.canClearContentsOfLayer = YES; + _flags.canCallSetNeedsDisplayOfLayer = YES; + + _fallbackSafeAreaInsets = UIEdgeInsetsZero; + _fallbackInsetsLayoutMarginsFromSafeArea = YES; + _isViewControllerRoot = NO; + + _automaticallyRelayoutOnSafeAreaChanges = NO; + _automaticallyRelayoutOnLayoutMarginsChanges = NO; + + [self baseDidInit]; + ASDisplayNodeLogEvent(self, @"init"); +} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + [self _initializeInstance]; + + return self; +} + +- (instancetype)initWithViewClass:(Class)viewClass +{ + if (!(self = [self init])) + return nil; + + ASDisplayNodeAssert([viewClass isSubclassOfClass:[UIView class]], @"should initialize with a subclass of UIView"); + + _viewClass = viewClass; + setFlag(Synchronous, ![viewClass isSubclassOfClass:[_ASDisplayView class]]); + + return self; +} + +- (instancetype)initWithLayerClass:(Class)layerClass +{ + if (!(self = [self init])) { + return nil; + } + + ASDisplayNodeAssert([layerClass isSubclassOfClass:[CALayer class]], @"should initialize with a subclass of CALayer"); + + _layerClass = layerClass; + _flags.layerBacked = YES; + setFlag(Synchronous, ![layerClass isSubclassOfClass:[_ASDisplayLayer class]]); + + return self; +} + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + return [self initWithViewBlock:viewBlock didLoadBlock:nil]; +} + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [self init])) { + return nil; + } + + [self setViewBlock:viewBlock]; + if (didLoadBlock != nil) { + [self onDidLoad:didLoadBlock]; + } + + return self; +} + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + return [self initWithLayerBlock:layerBlock didLoadBlock:nil]; +} + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [self init])) { + return nil; + } + + [self setLayerBlock:layerBlock]; + if (didLoadBlock != nil) { + [self onDidLoad:didLoadBlock]; + } + + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__); + +- (void)setViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + ASDisplayNodeAssertFalse(self.nodeLoaded); + ASDisplayNodeAssertNotNil(viewBlock, @"should initialize with a valid block that returns a UIView"); + + _viewBlock = viewBlock; + setFlag(Synchronous, YES); +} + +- (void)setLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + ASDisplayNodeAssertFalse(self.nodeLoaded); + ASDisplayNodeAssertNotNil(layerBlock, @"should initialize with a valid block that returns a CALayer"); + + _layerBlock = layerBlock; + _flags.layerBacked = YES; + setFlag(Synchronous, YES); +} + +- (ASDisplayNodeMethodOverrides)methodOverrides +{ + return _methodOverrides; +} + +- (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body +{ + AS::UniqueLock l(__instanceLock__); + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeAssertThreadAffinity(self); + l.unlock(); + body(self); + return; + } else if (_onDidLoadBlocks == nil) { + _onDidLoadBlocks = [NSMutableArray arrayWithObject:body]; + } else { + [_onDidLoadBlocks addObject:body]; + } +} + +- (void)dealloc +{ + _flags.isDeallocating = YES; + [self baseWillDealloc]; + + // Synchronous nodes may not be able to call the hierarchy notifications, so only enforce for regular nodes. + ASDisplayNodeAssert(checkFlag(Synchronous) || !ASInterfaceStateIncludesVisible(_interfaceState), @"Node should always be marked invisible before deallocating. Node: %@", self); + + self.asyncLayer.asyncDelegate = nil; + _view.asyncdisplaykit_node = nil; + _layer.asyncdisplaykit_node = nil; + + // Remove any subnodes so they lose their connection to the now deallocated parent. This can happen + // because subnodes do not retain their supernode, but subnodes can legitimately remain alive if another + // thing outside the view hierarchy system (e.g. async display, controller code, etc). keeps a retained + // reference to subnodes. + + for (ASDisplayNode *subnode in _subnodes) + [subnode _setSupernode:nil]; + + [self scheduleIvarsForMainThreadDeallocation]; + + // TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. + [self _setSupernode:nil]; +} + +#pragma mark - Loading + +- (BOOL)_locked_shouldLoadViewOrLayer +{ + ASAssertLocked(__instanceLock__); + return !_flags.isDeallocating && !(_hierarchyState & ASHierarchyStateRasterized); +} + +- (UIView *)_locked_viewToLoad +{ + ASAssertLocked(__instanceLock__); + + UIView *view = nil; + if (_viewBlock) { + view = _viewBlock(); + ASDisplayNodeAssertNotNil(view, @"View block returned nil"); + ASDisplayNodeAssert(![view isKindOfClass:[_ASDisplayView class]], @"View block should return a synchronously displayed view"); + _viewBlock = nil; + _viewClass = [view class]; + } else { + view = [[_viewClass alloc] init]; + } + + // Special handling of wrapping UIKit components + if (checkFlag(Synchronous)) { + [self checkResponderCompatibility]; + + // UIImageView layers. More details on the flags + if ([_viewClass isSubclassOfClass:[UIImageView class]]) { + _flags.canClearContentsOfLayer = NO; + _flags.canCallSetNeedsDisplayOfLayer = NO; + } + + // UIActivityIndicator + if ([_viewClass isSubclassOfClass:[UIActivityIndicatorView class]] + || [_viewClass isSubclassOfClass:[UIVisualEffectView class]]) { + self.opaque = NO; + } + + // CAEAGLLayer + if([[view.layer class] isSubclassOfClass:[CAEAGLLayer class]]){ + _flags.canClearContentsOfLayer = NO; + } + } + + return view; +} + +- (CALayer *)_locked_layerToLoad +{ + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert(_flags.layerBacked, @"_layerToLoad is only for layer-backed nodes"); + + CALayer *layer = nil; + if (_layerBlock) { + layer = _layerBlock(); + ASDisplayNodeAssertNotNil(layer, @"Layer block returned nil"); + ASDisplayNodeAssert(![layer isKindOfClass:[_ASDisplayLayer class]], @"Layer block should return a synchronously displayed layer"); + _layerBlock = nil; + _layerClass = [layer class]; + } else { + layer = [[_layerClass alloc] init]; + } + + return layer; +} + +- (void)_locked_loadViewOrLayer +{ + ASAssertLocked(__instanceLock__); + + if (_flags.layerBacked) { + TIME_SCOPED(_debugTimeToCreateView); + _layer = [self _locked_layerToLoad]; + static int ASLayerDelegateAssociationKey; + + /** + * CALayer's .delegate property is documented to be weak, but the implementation is actually assign. + * Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node + * begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only + * way to avoid a dangling pointer is to use a weak proxy. + */ + ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self]; + _layer.delegate = (id)instance; + objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + TIME_SCOPED(_debugTimeToCreateView); + _view = [self _locked_viewToLoad]; + _view.asyncdisplaykit_node = self; + _layer = _view.layer; + } + _layer.asyncdisplaykit_node = self; + + self._locked_asyncLayer.asyncDelegate = self; +} + +- (void)_didLoad +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"didLoad"); + as_log_verbose(ASNodeLog(), "didLoad %@", self); + TIME_SCOPED(_debugTimeForDidLoad); + + [self didLoad]; + + __instanceLock__.lock(); + const auto onDidLoadBlocks = ASTransferStrong(_onDidLoadBlocks); + __instanceLock__.unlock(); + + for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { + block(self); + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLoad]; + }]; +} + +- (BOOL)isNodeLoaded +{ + if (ASDisplayNodeThreadIsMain()) { + // Because the view and layer can only be created and destroyed on Main, that is also the only thread + // where the state of this property can change. As an optimization, we can avoid locking. + return _loaded(self); + } else { + MutexLocker l(__instanceLock__); + return [self _locked_isNodeLoaded]; + } +} + +- (BOOL)_locked_isNodeLoaded +{ + ASAssertLocked(__instanceLock__); + return _loaded(self); +} + +#pragma mark - Misc Setter / Getter + +- (UIView *)view +{ + AS::UniqueLock l(__instanceLock__); + + ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes"); + BOOL isLayerBacked = _flags.layerBacked; + if (isLayerBacked) { + return nil; + } + + if (_view != nil) { + return _view; + } + + if (![self _locked_shouldLoadViewOrLayer]) { + return nil; + } + + // Loading a view needs to happen on the main thread + ASDisplayNodeAssertMainThread(); + [self _locked_loadViewOrLayer]; + + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + l.unlock(); + [self __setNeedsLayout]; + l.lock(); + } + + [self _locked_applyPendingStateToViewOrLayer]; + + // The following methods should not be called with a lock + l.unlock(); + + // No need for the lock as accessing the subviews or layers are always happening on main + [self _addSubnodeViewsAndLayers]; + + // A subclass hook should never be called with a lock + [self _didLoad]; + + return _view; +} + +- (CALayer *)layer +{ + AS::UniqueLock l(__instanceLock__); + if (_layer != nil) { + return _layer; + } + + if (![self _locked_shouldLoadViewOrLayer]) { + return nil; + } + + // Loading a layer needs to happen on the main thread + ASDisplayNodeAssertMainThread(); + [self _locked_loadViewOrLayer]; + CALayer *layer = _layer; + + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + l.unlock(); + [self __setNeedsLayout]; + l.lock(); + } + + [self _locked_applyPendingStateToViewOrLayer]; + + // The following methods should not be called with a lock + l.unlock(); + + // No need for the lock as accessing the subviews or layers are always happening on main + [self _addSubnodeViewsAndLayers]; + + // A subclass hook should never be called with a lock + [self _didLoad]; + + return layer; +} + +// Returns nil if the layer is not an _ASDisplayLayer; will not create the layer if nil. +- (_ASDisplayLayer *)asyncLayer +{ + MutexLocker l(__instanceLock__); + return [self _locked_asyncLayer]; +} + +- (_ASDisplayLayer *)_locked_asyncLayer +{ + ASAssertLocked(__instanceLock__); + return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil; +} + +- (BOOL)isSynchronous +{ + return checkFlag(Synchronous); +} + +- (void)setLayerBacked:(BOOL)isLayerBacked +{ + // Only call this if assertions are enabled – it could be expensive. + ASDisplayNodeAssert(!isLayerBacked || self.supportsLayerBacking, @"Node %@ does not support layer backing.", self); + + MutexLocker l(__instanceLock__); + if (_flags.layerBacked == isLayerBacked) { + return; + } + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeFailAssert(@"Cannot change layerBacked after view/layer has loaded."); + return; + } + + _flags.layerBacked = isLayerBacked; +} + +- (BOOL)isLayerBacked +{ + MutexLocker l(__instanceLock__); + return _flags.layerBacked; +} + +- (BOOL)supportsLayerBacking +{ + MutexLocker l(__instanceLock__); + return !checkFlag(Synchronous) && !_flags.viewEverHadAGestureRecognizerAttached && _viewClass == [_ASDisplayView class] && _layerClass == [_ASDisplayLayer class]; +} + +- (BOOL)shouldAnimateSizeChanges +{ + MutexLocker l(__instanceLock__); + return _flags.shouldAnimateSizeChanges; +} + +- (void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + MutexLocker l(__instanceLock__); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + +- (CGRect)threadSafeBounds +{ + MutexLocker l(__instanceLock__); + return [self _locked_threadSafeBounds]; +} + +- (CGRect)_locked_threadSafeBounds +{ + ASAssertLocked(__instanceLock__); + return _threadSafeBounds; +} + +- (void)setThreadSafeBounds:(CGRect)newBounds +{ + MutexLocker l(__instanceLock__); + _threadSafeBounds = newBounds; +} + +- (void)nodeViewDidAddGestureRecognizer +{ + MutexLocker l(__instanceLock__); + _flags.viewEverHadAGestureRecognizerAttached = YES; +} + +- (UIEdgeInsets)fallbackSafeAreaInsets +{ + MutexLocker l(__instanceLock__); + return _fallbackSafeAreaInsets; +} + +- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets +{ + BOOL needsManualUpdate; + BOOL updatesLayoutMargins; + + { + MutexLocker l(__instanceLock__); + ASDisplayNodeAssertThreadAffinity(self); + + if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) { + return; + } + + _fallbackSafeAreaInsets = insets; + needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked; + updatesLayoutMargins = needsManualUpdate && [self _locked_insetsLayoutMarginsFromSafeArea]; + } + + if (needsManualUpdate) { + [self safeAreaInsetsDidChange]; + } + + if (updatesLayoutMargins) { + [self layoutMarginsDidChange]; + } +} + +- (void)_fallbackUpdateSafeAreaOnChildren +{ + ASDisplayNodeAssertThreadAffinity(self); + + UIEdgeInsets insets = self.safeAreaInsets; + CGRect bounds = self.bounds; + + for (ASDisplayNode *child in self.subnodes) { + if (AS_AT_LEAST_IOS11 && !child.layerBacked) { + // In iOS 11 view-backed nodes already know what their safe area is. + continue; + } + + if (child.viewControllerRoot) { + // Its safe area is controlled by a view controller. Don't override it. + continue; + } + + CGRect childFrame = child.frame; + UIEdgeInsets childInsets = UIEdgeInsetsMake(MAX(insets.top - (CGRectGetMinY(childFrame) - CGRectGetMinY(bounds)), 0), + MAX(insets.left - (CGRectGetMinX(childFrame) - CGRectGetMinX(bounds)), 0), + MAX(insets.bottom - (CGRectGetMaxY(bounds) - CGRectGetMaxY(childFrame)), 0), + MAX(insets.right - (CGRectGetMaxX(bounds) - CGRectGetMaxX(childFrame)), 0)); + + child.fallbackSafeAreaInsets = childInsets; + } +} + +- (BOOL)isViewControllerRoot +{ + MutexLocker l(__instanceLock__); + return _isViewControllerRoot; +} + +- (void)setViewControllerRoot:(BOOL)flag +{ + MutexLocker l(__instanceLock__); + _isViewControllerRoot = flag; +} + +- (BOOL)automaticallyRelayoutOnSafeAreaChanges +{ + MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnSafeAreaChanges; +} + +- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag +{ + MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnSafeAreaChanges = flag; +} + +- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges +{ + MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnLayoutMarginsChanges; +} + +- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag +{ + MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnLayoutMarginsChanges = flag; +} + +- (void)__setNodeController:(ASNodeController *)controller +{ + // See docs for why we don't lock. + if (controller.shouldInvertStrongReference) { + _strongNodeController = controller; + _weakNodeController = nil; + } else { + _weakNodeController = controller; + _strongNodeController = nil; + } +} + +#pragma mark - UIResponder + +#define HANDLE_NODE_RESPONDER_METHOD(__sel) \ + /* All responder methods should be called on the main thread */ \ + ASDisplayNodeAssertMainThread(); \ + if (checkFlag(Synchronous)) { \ + /* If the view is not a _ASDisplayView subclass (Synchronous) just call through to the view as we + expect it's a non _ASDisplayView subclass that will respond */ \ + return [_view __sel]; \ + } else { \ + if (ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(__sel))) { \ + /* If the subclass overwrites canBecomeFirstResponder just call through + to it as we expect it will handle it */ \ + return [_view __sel]; \ + } else { \ + /* Call through to _ASDisplayView's superclass to get it handled */ \ + return [(_ASDisplayView *)_view __##__sel]; \ + } \ + } \ + +- (void)checkResponderCompatibility +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // There are certain cases we cannot handle and are not supported: + // 1. If the _view class is not a subclass of _ASDisplayView + if (checkFlag(Synchronous)) { + // 2. At least one UIResponder methods are overwritten in the node subclass + NSString *message = @"Overwritting %@ and having a backing view that is not an _ASDisplayView is not supported."; + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canBecomeFirstResponder)), ([NSString stringWithFormat:message, @"canBecomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(becomeFirstResponder)), ([NSString stringWithFormat:message, @"becomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canResignFirstResponder)), ([NSString stringWithFormat:message, @"canResignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(resignFirstResponder)), ([NSString stringWithFormat:message, @"resignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(isFirstResponder)), ([NSString stringWithFormat:message, @"isFirstResponder"])); + } +#endif +} + +- (BOOL)__canBecomeFirstResponder +{ + if (_view == nil) { + // By default we return NO if not view is created yet + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(canBecomeFirstResponder); +} + +- (BOOL)__becomeFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canBecomeFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(becomeFirstResponder); +} + +- (BOOL)__canResignFirstResponder +{ + if (_view == nil) { + // By default we return YES if no view is created yet + return YES; + } + + HANDLE_NODE_RESPONDER_METHOD(canResignFirstResponder); +} + +- (BOOL)__resignFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canResignFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(resignFirstResponder); +} + +- (BOOL)__isFirstResponder +{ + if (_view == nil) { + // If no view is created yet we can just return NO as it's unlikely it's the first responder + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(isFirstResponder); +} + +#pragma mark + +- (NSString *)debugName +{ + MutexLocker l(__instanceLock__); + return _debugName; +} + +- (void)setDebugName:(NSString *)debugName +{ + MutexLocker l(__instanceLock__); + if (!ASObjectIsEqual(_debugName, debugName)) { + _debugName = [debugName copy]; + } +} + +#pragma mark - Layout + +#pragma mark + +- (BOOL)canLayoutAsynchronous +{ + return !self.isNodeLoaded; +} + +#pragma mark Layout Pass + +- (void)__setNeedsLayout +{ + [self invalidateCalculatedLayout]; +} + +- (void)invalidateCalculatedLayout +{ + MutexLocker l(__instanceLock__); + + _layoutVersion++; + + _unflattenedLayout = nil; + +#if YOGA + [self invalidateCalculatedYogaLayout]; +#endif +} + +- (void)__layout +{ + ASDisplayNodeAssertThreadAffinity(self); + // ASAssertUnlocked(__instanceLock__); + + BOOL loaded = NO; + { + AS::UniqueLock l(__instanceLock__); + loaded = [self _locked_isNodeLoaded]; + CGRect bounds = _threadSafeBounds; + + if (CGRectEqualToRect(bounds, CGRectZero)) { + // Performing layout on a zero-bounds view often results in frame calculations + // with negative sizes after applying margins, which will cause + // layoutThatFits: on subnodes to assert. + as_log_debug(OS_LOG_DISABLED, "Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); + return; + } + + // If a current layout transition is in progress there is no need to do a measurement and layout pass in here as + // this is supposed to happen within the layout transition process + if (_transitionID != ASLayoutElementContextInvalidTransitionID) { + return; + } + + as_activity_create_for_scope("-[ASDisplayNode __layout]"); + + // This method will confirm that the layout is up to date (and update if needed). + // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). + l.unlock(); + [self _u_measureNodeWithBoundsIfNecessary:bounds]; + l.lock(); + + [self _locked_layoutPlaceholderIfNecessary]; + } + + [self _layoutSublayouts]; + + // Per API contract, `-layout` and `-layoutDidFinish` are called only if the node is loaded. + if (loaded) { + ASPerformBlockOnMainThread(^{ + [self layout]; + [self _layoutClipCornersIfNeeded]; + [self _layoutDidFinish]; + }); + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + +- (void)_layoutDidFinish +{ + ASDisplayNodeAssertMainThread(); + // ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); + [self layoutDidFinish]; +} + +#pragma mark Calculation + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ + as_activity_scope_verbose(as_activity_create("Calculate node layout", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASLayoutLog(), "Calculating layout for %@ sizeRange %@", self, NSStringFromASSizeRange(constrainedSize)); + +#if AS_KDEBUG_ENABLE + // We only want one calculateLayout signpost interval per thread. + // Currently there is no fallback for profiling i386, since it's not useful. + static _Thread_local NSInteger tls_callDepth; + if (tls_callDepth++ == 0) { + ASSignpostStart(ASSignpostCalculateLayout); + } +#endif + + ASSizeRange styleAndParentSize = ASLayoutElementSizeResolve(self.style.size, parentSize); + const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, styleAndParentSize); + ASLayout *result = [self calculateLayoutThatFits:resolvedRange]; + as_log_verbose(ASLayoutLog(), "Calculated layout %@", result); + +#if AS_KDEBUG_ENABLE + if (--tls_callDepth == 0) { + ASSignpostEnd(ASSignpostCalculateLayout); + } +#endif + + return result; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + switch (self.layoutEngineType) { + case ASLayoutEngineTypeLayoutSpec: + return [self calculateLayoutLayoutSpec:constrainedSize]; +#if YOGA + case ASLayoutEngineTypeYoga: + return [self calculateLayoutYoga:constrainedSize]; +#endif + // If YOGA is not defined but for some reason the layout type engine is Yoga + // we explicitly fallthrough here + default: + break; + } + + // If this case is reached a layout type engine was defined for a node that is currently + // not supported. + ASDisplayNodeAssert(NO, @"No layout type determined"); + return nil; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeLogEvent(self, @"calculateSizeThatFits: with constrainedSize: %@", NSStringFromCGSize(constrainedSize)); + + return ASIsCGSizeValidForSize(constrainedSize) ? constrainedSize : CGSizeZero; +} + +- (void)layout +{ + // Hook for subclasses + ASDisplayNodeAssertMainThread(); + // ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLayout]; + }]; +} + +#pragma mark Layout Transition + +- (void)_layoutTransitionMeasurementDidFinish +{ + // Hook for subclasses - No-Op in ASDisplayNode +} + +#pragma mark <_ASTransitionContextCompletionDelegate> + +/** + * After completeTransition: is called on the ASContextTransitioning object in animateLayoutTransition: this + * delegate method will be called that start the completion process of the transition + */ +- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete +{ + ASDisplayNodeAssertMainThread(); + + [self didCompleteLayoutTransition:context]; + + _pendingLayoutTransitionContext = nil; + + [self _pendingLayoutTransitionDidComplete]; +} + +#pragma mark - Display + +NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes"; +NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp"; + +- (BOOL)displaysAsynchronously +{ + MutexLocker l(__instanceLock__); + return [self _locked_displaysAsynchronously]; +} + +/** + * Core implementation of -displaysAsynchronously. + */ +- (BOOL)_locked_displaysAsynchronously +{ + ASAssertLocked(__instanceLock__); + return checkFlag(Synchronous) == NO && _flags.displaysAsynchronously; +} + +- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously +{ + ASDisplayNodeAssertThreadAffinity(self); + + MutexLocker l(__instanceLock__); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (checkFlag(Synchronous)) { + return; + } + + if (_flags.displaysAsynchronously == displaysAsynchronously) { + return; + } + + _flags.displaysAsynchronously = displaysAsynchronously; + + self._locked_asyncLayer.displaysAsynchronously = displaysAsynchronously; +} + +- (BOOL)rasterizesSubtree +{ + MutexLocker l(__instanceLock__); + return _flags.rasterizesSubtree; +} + +- (void)enableSubtreeRasterization +{ + MutexLocker l(__instanceLock__); + // Already rasterized from self. + if (_flags.rasterizesSubtree) { + return; + } + + // If rasterized from above, bail. + if (ASHierarchyStateIncludesRasterized(_hierarchyState)) { + ASDisplayNodeFailAssert(@"Subnode of a rasterized node should not have redundant -enableSubtreeRasterization."); + return; + } + + // Ensure not loaded. + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeFailAssert(@"Cannot call %@ on loaded node: %@", NSStringFromSelector(_cmd), self); + return; + } + + // Ensure no loaded subnodes + ASDisplayNode *loadedSubnode = ASDisplayNodeFindFirstSubnode(self, ^BOOL(ASDisplayNode * _Nonnull node) { + return node.nodeLoaded; + }); + if (loadedSubnode != nil) { + ASDisplayNodeFailAssert(@"Cannot call %@ on node %@ with loaded subnode %@", NSStringFromSelector(_cmd), self, loadedSubnode); + return; + } + + _flags.rasterizesSubtree = YES; + + // Tell subnodes that now they're in a rasterized hierarchy (while holding lock!) + for (ASDisplayNode *subnode in _subnodes) { + [subnode enterHierarchyState:ASHierarchyStateRasterized]; + } +} + +- (CGFloat)contentsScaleForDisplay +{ + MutexLocker l(__instanceLock__); + + return _contentsScaleForDisplay; +} + +- (void)setContentsScaleForDisplay:(CGFloat)contentsScaleForDisplay +{ + MutexLocker l(__instanceLock__); + + if (_contentsScaleForDisplay == contentsScaleForDisplay) { + return; + } + + _contentsScaleForDisplay = contentsScaleForDisplay; +} + +- (void)displayImmediately +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!checkFlag(Synchronous), @"this method is designed for asynchronous mode only"); + + [self.asyncLayer displayImmediately]; +} + +- (void)recursivelyDisplayImmediately +{ + for (ASDisplayNode *child in self.subnodes) { + [child recursivelyDisplayImmediately]; + } + [self displayImmediately]; +} + +- (void)__setNeedsDisplay +{ + BOOL shouldScheduleForDisplay = NO; + { + MutexLocker l(__instanceLock__); + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState); + // FIXME: This should not need to recursively display, so create a non-recursive variant. + // The semantics of setNeedsDisplay (as defined by CALayer behavior) are not recursive. + if (_layer != nil && !checkFlag(Synchronous) && nowDisplay && [self _implementsDisplay]) { + shouldScheduleForDisplay = YES; + } + } + + if (shouldScheduleForDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } +} + ++ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node +{ + static dispatch_once_t onceToken; + static ASRunLoopQueue *renderQueue; + dispatch_once(&onceToken, ^{ + renderQueue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() + retainObjects:NO + handler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { + [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; + if (isQueueDrained) { + CFTimeInterval timestamp = CACurrentMediaTime(); + [[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification + object:nil + userInfo:@{ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp: @(timestamp)}]; + } + }]; + }); + + as_log_verbose(ASDisplayLog(), "%s %@", sel_getName(_cmd), node); + [renderQueue enqueue:node]; +} + +/// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay +{ + MutexLocker l(__instanceLock__); + + return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.rasterizesSubtree; +} + +// Track that a node will be displayed as part of the current node hierarchy. +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeWillDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + + // No lock needed as _pendingDisplayNodes is main thread only + if (!_pendingDisplayNodes) { + _pendingDisplayNodes = [[ASWeakSet alloc] init]; + } + + [_pendingDisplayNodes addObject:node]; +} + +// Notify that a node that was pending display finished +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeDidDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + + // No lock for _pendingDisplayNodes needed as it's main thread only + [_pendingDisplayNodes removeObject:node]; + + if (_pendingDisplayNodes.isEmpty) { + + [self hierarchyDisplayDidFinish]; + [self enumerateInterfaceStateDelegates:^(id delegate) { + [delegate hierarchyDisplayDidFinish]; + }]; + + BOOL placeholderShouldPersist = [self placeholderShouldPersist]; + + __instanceLock__.lock(); + if (_placeholderLayer.superlayer && !placeholderShouldPersist) { + void (^cleanupBlock)() = ^{ + [_placeholderLayer removeFromSuperlayer]; + }; + + if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [CATransaction begin]; + [CATransaction setCompletionBlock:cleanupBlock]; + [CATransaction setAnimationDuration:_placeholderFadeDuration]; + _placeholderLayer.opacity = 0.0; + [CATransaction commit]; + } else { + cleanupBlock(); + } + } + __instanceLock__.unlock(); + } +} + +// Helper method to determine if it's safe to call setNeedsDisplay on a layer without throwing away the content. +// For details look at the comment on the canCallSetNeedsDisplayOfLayer flag +- (BOOL)_canCallSetNeedsDisplayOfLayer +{ + MutexLocker l(__instanceLock__); + return _flags.canCallSetNeedsDisplayOfLayer; +} + +void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) +{ + // This recursion must handle layers in various states: + // 1. Just added to hierarchy, CA hasn't yet called -display + // 2. Previously in a hierarchy (such as a working window owned by an Intelligent Preloading class, like ASTableView / ASCollectionView / ASViewController) + // 3. Has no content to display at all + // Specifically for case 1), we need to explicitly trigger a -display call now. + // Otherwise, there is no opportunity to block the main thread after CoreAnimation's transaction commit + // (even a runloop observer at a late call order will not stop the next frame from compositing, showing placeholders). + + ASDisplayNode *node = [layer asyncdisplaykit_node]; + + if (node.isSynchronous && [node _canCallSetNeedsDisplayOfLayer]) { + // Layers for UIKit components that are wrapped within a node needs to be set to be displayed as the contents of + // the layer get's cleared and would not be recreated otherwise. + // We do not call this for _ASDisplayLayer as an optimization. + [layer setNeedsDisplay]; + } + + if ([node _implementsDisplay]) { + // For layers that do get displayed here, this immediately kicks off the work on the concurrent -[_ASDisplayLayer displayQueue]. + // At the same time, it creates an associated _ASAsyncTransaction, which we can use to block on display completion. See ASDisplayNode+AsyncDisplay.mm. + [layer displayIfNeeded]; + } + + // Kick off the recursion first, so that all necessary display calls are sent and the displayQueue is full of parallelizable work. + // NOTE: The docs report that `sublayers` returns a copy but it actually doesn't. + for (CALayer *sublayer in [layer.sublayers copy]) { + recursivelyTriggerDisplayForLayer(sublayer, shouldBlock); + } + + if (shouldBlock) { + // As the recursion unwinds, verify each transaction is complete and block if it is not. + // While blocking on one transaction, others may be completing concurrently, so it doesn't matter which blocks first. + BOOL waitUntilComplete = (!node.shouldBypassEnsureDisplay); + if (waitUntilComplete) { + for (_ASAsyncTransaction *transaction in [layer.asyncdisplaykit_asyncLayerTransactions copy]) { + // Even if none of the layers have had a chance to start display earlier, they will still be allowed to saturate a multicore CPU while blocking main. + // This significantly reduces time on the main thread relative to UIKit. + [transaction waitUntilComplete]; + } + } + } +} + +- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock +{ + ASDisplayNodeAssertMainThread(); + + CALayer *layer = self.layer; + // -layoutIfNeeded is recursive, and even walks up to superlayers to check if they need layout, + // so we should call it outside of starting the recursion below. If our own layer is not marked + // as dirty, we can assume layout has run on this subtree before. + if ([layer needsLayout]) { + [layer layoutIfNeeded]; + } + recursivelyTriggerDisplayForLayer(layer, shouldBlock); +} + +- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously +{ + [self _recursivelyTriggerDisplayAndBlock:synchronously]; +} + +- (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay +{ + MutexLocker l(__instanceLock__); + _flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay; +} + +- (BOOL)shouldBypassEnsureDisplay +{ + MutexLocker l(__instanceLock__); + return _flags.shouldBypassEnsureDisplay; +} + +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale +{ + { + MutexLocker l(__instanceLock__); + if (contentsScale == _contentsScaleForDisplay) { + return; + } + + _contentsScaleForDisplay = contentsScale; + } + + [self setNeedsDisplay]; +} + +- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + [node setNeedsDisplayAtScale:contentsScale]; + }); +} + +- (void)_layoutClipCornersIfNeeded +{ + ASDisplayNodeAssertMainThread(); + if (_clipCornerLayers[0] == nil) { + return; + } + + CGSize boundsSize = self.bounds.size; + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + if (_clipCornerLayers[idx]) { + // Note the Core Animation coordinates are reversed for y; 0 is at the bottom. + _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0); + [_layer addSublayer:_clipCornerLayers[idx]]; + } + } +} + +- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor +{ + ASPerformBlockOnMainThread(^{ + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + // Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left. + // anchorPoint is Bottom Left at 0,0 and Top Right at 1,1. + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + + CGSize size = CGSizeMake(radius + 1, radius + 1); + ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + if (isRight == YES) { + CGContextTranslateCTM(ctx, -radius + 1, 0); + } + if (isTop == YES) { + CGContextTranslateCTM(ctx, 0, -radius + 1); + } + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; + [roundedRect setUsesEvenOddFillRule:YES]; + [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; + [backgroundColor setFill]; + [roundedRect fill]; + + // No lock needed, as _clipCornerLayers is only modified on the main thread. + CALayer *clipCornerLayer = _clipCornerLayers[idx]; + clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage); + clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height); + clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + } + [self _layoutClipCornersIfNeeded]; + }); +} + +- (void)_setClipCornerLayersVisible:(BOOL)visible +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + if (visible) { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + if (_clipCornerLayers[idx] == nil) { + static ASDisplayNodeCornerLayerDelegate *clipCornerLayers; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init]; + }); + _clipCornerLayers[idx] = [[CALayer alloc] init]; + _clipCornerLayers[idx].zPosition = 99999; + _clipCornerLayers[idx].delegate = clipCornerLayers; + } + } + [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; + } else { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + [_clipCornerLayers[idx] removeFromSuperlayer]; + _clipCornerLayers[idx] = nil; + } + } + }); +} + +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius +{ + __instanceLock__.lock(); + CGFloat oldCornerRadius = _cornerRadius; + ASCornerRoundingType oldRoundingType = _cornerRoundingType; + + _cornerRadius = newCornerRadius; + _cornerRoundingType = newRoundingType; + __instanceLock__.unlock(); + + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + if (newRoundingType == ASCornerRoundingTypePrecomposited) { + self.layerCornerRadius = 0.0; + if (oldCornerRadius > 0.0) { + [self displayImmediately]; + } else { + [self setNeedsDisplay]; // Async display is OK if we aren't replacing an existing .cornerRadius. + } + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + self.layerCornerRadius = 0.0; + [self _setClipCornerLayersVisible:YES]; + } else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + } + } + else if (oldRoundingType == ASCornerRoundingTypePrecomposited) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + // Corners are already precomposited, but the radius has changed. + // Default to async re-display. The user may force a synchronous display if desired. + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + [self _setClipCornerLayersVisible:YES]; + [self setNeedsDisplay]; + } + } + else if (oldRoundingType == ASCornerRoundingTypeClipping) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self _setClipCornerLayersVisible:NO]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + [self _setClipCornerLayersVisible:NO]; + [self displayImmediately]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + // Clip corners already exist, but the radius has changed. + [self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor]; + } + } + } + }); +} + +- (void)recursivelySetDisplaySuspended:(BOOL)flag +{ + _recursivelySetDisplaySuspended(self, nil, flag); +} + +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or a variant with a condition / test block. +static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, BOOL flag) +{ + // If there is no layer, but node whose its view is loaded, then we can traverse down its layer hierarchy. Otherwise we must stick to the node hierarchy to avoid loading views prematurely. Note that for nodes that haven't loaded their views, they can't possibly have subviews/sublayers, so we don't need to traverse the layer hierarchy for them. + if (!layer && node && node.nodeLoaded) { + layer = node.layer; + } + + // If we don't know the node, but the layer is an async layer, get the node from the layer. + if (!node && layer && [layer isKindOfClass:[_ASDisplayLayer class]]) { + node = layer.asyncdisplaykit_node; + } + + // Set the flag on the node. If this is a pure layer (no node) then this has no effect (plain layers don't support preventing/cancelling display). + node.displaySuspended = flag; + + if (layer && !node.rasterizesSubtree) { + // If there is a layer, recurse down the layer hierarchy to set the flag on descendants. This will cover both layer-based and node-based children. + for (CALayer *sublayer in layer.sublayers) { + _recursivelySetDisplaySuspended(nil, sublayer, flag); + } + } else { + // If there is no layer (view not loaded yet) or this node rasterizes descendants (there won't be a layer tree to traverse), recurse down the subnode hierarchy to set the flag on descendants. This covers only node-based children, but for a node whose view is not loaded it can't possibly have nodeless children. + for (ASDisplayNode *subnode in node.subnodes) { + _recursivelySetDisplaySuspended(subnode, nil, flag); + } + } +} + +- (BOOL)displaySuspended +{ + MutexLocker l(__instanceLock__); + return _flags.displaySuspended; +} + +- (void)setDisplaySuspended:(BOOL)flag +{ + ASDisplayNodeAssertThreadAffinity(self); + __instanceLock__.lock(); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (checkFlag(Synchronous) || _flags.displaySuspended == flag) { + __instanceLock__.unlock(); + return; + } + + _flags.displaySuspended = flag; + + self._locked_asyncLayer.displaySuspended = flag; + + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + if ([self _implementsDisplay]) { + // Display start and finish methods needs to happen on the main thread + ASPerformBlockOnMainThread(^{ + if (flag) { + [supernode subnodeDisplayDidFinish:self]; + } else { + [supernode subnodeDisplayWillStart:self]; + } + }); + } +} + +#pragma mark <_ASDisplayLayerDelegate> + +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer asynchronously:(BOOL)asynchronously +{ + // Subclass hook. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self displayWillStart]; +#pragma clang diagnostic pop + + [self displayWillStartAsynchronously:asynchronously]; +} + +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + // Subclass hook. + [self displayDidFinish]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)displayWillStart {} +#pragma clang diagnostic pop +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNodeLogEvent(self, @"displayWillStart"); + // in case current node takes longer to display than it's subnodes, treat it as a dependent node + [self _pendingNodeWillDisplay:self]; + + __instanceLock__.lock(); + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + [supernode subnodeDisplayWillStart:self]; +} + +- (void)displayDidFinish +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNodeLogEvent(self, @"displayDidFinish"); + [self _pendingNodeDidDisplay:self]; + + __instanceLock__.lock(); + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + [supernode subnodeDisplayDidFinish:self]; +} + +- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode +{ + // Subclass hook + [self _pendingNodeWillDisplay:subnode]; +} + +- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode +{ + // Subclass hook + [self _pendingNodeDidDisplay:subnode]; +} + +#pragma mark + +// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + if (event == kCAOnOrderIn) { + [self __enterHierarchy]; + } else if (event == kCAOnOrderOut) { + [self __exitHierarchy]; + } + + ASDisplayNodeAssert(_flags.layerBacked, @"We shouldn't get called back here unless we are layer-backed."); + return (id)kCFNull; +} + +#pragma mark - Error Handling + ++ (void)setNonFatalErrorBlock:(ASDisplayNodeNonFatalErrorBlock)nonFatalErrorBlock +{ + if (_nonFatalErrorBlock != nonFatalErrorBlock) { + _nonFatalErrorBlock = [nonFatalErrorBlock copy]; + } +} + ++ (ASDisplayNodeNonFatalErrorBlock)nonFatalErrorBlock +{ + return _nonFatalErrorBlock; +} + +#pragma mark - Converting to and from the Node's Coordinate System + +- (CATransform3D)_transformToAncestor:(ASDisplayNode *)ancestor +{ + CATransform3D transform = CATransform3DIdentity; + ASDisplayNode *currentNode = self; + while (currentNode.supernode) { + if (currentNode == ancestor) { + return transform; + } + + CGPoint anchorPoint = currentNode.anchorPoint; + CGRect bounds = currentNode.bounds; + CGPoint position = currentNode.position; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + + transform = CATransform3DTranslate(transform, origin.x, origin.y, 0); + transform = CATransform3DTranslate(transform, -bounds.origin.x, -bounds.origin.y, 0); + currentNode = currentNode.supernode; + } + return transform; +} + +static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNode *referenceNode, ASDisplayNode *targetNode) +{ + ASDisplayNode *ancestor = ASDisplayNodeFindClosestCommonAncestor(referenceNode, targetNode); + + // Transform into global (away from reference coordinate space) + CATransform3D transformToGlobal = [referenceNode _transformToAncestor:ancestor]; + + // Transform into local (via inverse transform from target to ancestor) + CATransform3D transformToLocal = CATransform3DInvert([targetNode _transformToAncestor:ancestor]); + + return CATransform3DConcat(transformToGlobal, transformToLocal); +} + +- (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + /** + * When passed node=nil, all methods in this family use the UIView-style + * behavior – that is, convert from/to window coordinates if there's a window, + * otherwise return the point untransformed. + */ + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertPoint:point fromLayer:window.layer]; + } else { + return point; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertPoint:point toLayer:window.layer]; + } else { + return point; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertRect:rect fromLayer:window.layer]; + } else { + return rect; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertRect:rect toLayer:window.layer]; + } else { + return rect; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +#pragma mark - Managing the Node Hierarchy + +ASDISPLAYNODE_INLINE bool shouldDisableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASDisplayNode *to) { + if (!from || !to) return NO; + if (from.isSynchronous) return NO; + if (to.isSynchronous) return NO; + if (from.isInHierarchy != to.isInHierarchy) return NO; + return YES; +} + +/// Returns incremented value of i if i is not NSNotFound +ASDISPLAYNODE_INLINE NSInteger incrementIfFound(NSInteger i) { + return i == NSNotFound ? NSNotFound : i + 1; +} + +/// Returns if a node is a member of a rasterized tree +ASDISPLAYNODE_INLINE BOOL canUseViewAPI(ASDisplayNode *node, ASDisplayNode *subnode) { + return (subnode.isLayerBacked == NO && node.isLayerBacked == NO); +} + +/// Returns if node is a member of a rasterized tree +ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { + return (node.rasterizesSubtree || (node.hierarchyState & ASHierarchyStateRasterized)); +} + +// NOTE: This method must be dealloc-safe (should not retain self). +- (ASDisplayNode *)supernode +{ + MutexLocker l(__instanceLock__); + return _supernode; +} + +- (void)_setSupernode:(ASDisplayNode *)newSupernode +{ + BOOL supernodeDidChange = NO; + ASDisplayNode *oldSupernode = nil; + { + MutexLocker l(__instanceLock__); + if (_supernode != newSupernode) { + oldSupernode = _supernode; // Access supernode properties outside of lock to avoid remote chance of deadlock, + // in case supernode implementation must access one of our properties. + _supernode = newSupernode; + supernodeDidChange = YES; + } + } + + if (supernodeDidChange) { + ASDisplayNodeLogEvent(self, @"supernodeDidChange: %@, oldValue = %@", ASObjectDescriptionMakeTiny(newSupernode), ASObjectDescriptionMakeTiny(oldSupernode)); + // Hierarchy state + ASHierarchyState stateToEnterOrExit = (newSupernode ? newSupernode.hierarchyState + : oldSupernode.hierarchyState); + + // Rasterized state + BOOL parentWasOrIsRasterized = (newSupernode ? newSupernode.rasterizesSubtree + : oldSupernode.rasterizesSubtree); + if (parentWasOrIsRasterized) { + stateToEnterOrExit |= ASHierarchyStateRasterized; + } + if (newSupernode) { + + // Now that we have a supernode, propagate its traits to self. + // This should be done before possibly forcing self to load so we have traits in -didLoad + ASTraitCollectionPropagateDown(self, newSupernode.primitiveTraitCollection); + + if (!parentWasOrIsRasterized && newSupernode.nodeLoaded) { + // Trigger the subnode to load its layer, which will create its view if it needs one. + // By doing this prior to the downward propagation of newSupernode's interface state, + // we can guarantee that -didEnterVisibleState is only called with .isNodeLoaded = YES. + [self layer]; + } + + [self enterHierarchyState:stateToEnterOrExit]; + + // If a node was added to a supernode, the supernode could be in a layout pending state. All of the hierarchy state + // properties related to the transition need to be copied over as well as propagated down the subtree. + // This is especially important as with automatic subnode management, adding subnodes can happen while a transition + // is in fly + if (ASHierarchyStateIncludesLayoutPending(stateToEnterOrExit)) { + int32_t pendingTransitionId = newSupernode->_pendingTransitionID; + if (pendingTransitionId != ASLayoutElementContextInvalidTransitionID) { + { + _pendingTransitionID = pendingTransitionId; + + // Propagate down the new pending transition id + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + node->_pendingTransitionID = pendingTransitionId; + }); + } + } + } + } else { + // If a node will be removed from the supernode it should go out from the layout pending state to remove all + // layout pending state related properties on the node + stateToEnterOrExit |= ASHierarchyStateLayoutPending; + + [self exitHierarchyState:stateToEnterOrExit]; + + // We only need to explicitly exit hierarchy here if we were rasterized. + // Otherwise we will exit the hierarchy when our view/layer does so + // which has some nice carry-over machinery to handle cases where we are removed from a hierarchy + // and then added into it again shortly after. + __instanceLock__.lock(); + BOOL isInHierarchy = _flags.isInHierarchy; + __instanceLock__.unlock(); + + if (parentWasOrIsRasterized && isInHierarchy) { + [self __exitHierarchy]; + } + } + } +} + +- (NSArray *)subnodes +{ + MutexLocker l(__instanceLock__); + if (_cachedSubnodes == nil) { + _cachedSubnodes = [_subnodes copy]; + } else { + ASDisplayNodeAssert(ASObjectIsEqual(_cachedSubnodes, _subnodes), @"Expected _subnodes and _cachedSubnodes to have the same contents."); + } + return _cachedSubnodes ?: @[]; +} + +/* + * Central private helper method that should eventually be called if submethods add, insert or replace subnodes + * This method is called with thread affinity and without lock held. + * + * @param subnode The subnode to insert + * @param subnodeIndex The index in _subnodes to insert it + * @param viewSublayerIndex The index in layer.sublayers (not view.subviews) at which to insert the view (use if we can use the view API) otherwise pass NSNotFound + * @param sublayerIndex The index in layer.sublayers at which to insert the layer (use if either parent or subnode is layer-backed) otherwise pass NSNotFound + * @param oldSubnode Remove this subnode before inserting; ok to be nil if no removal is desired + */ +- (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnodeIndex sublayerIndex:(NSInteger)sublayerIndex andRemoveSubnode:(ASDisplayNode *)oldSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + as_log_verbose(ASNodeLog(), "Insert subnode %@ at index %zd of %@ and remove subnode %@", subnode, subnodeIndex, self, oldSubnode); + + if (subnode == nil || subnode == self) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode or self as subnode"); + return; + } + + if (subnodeIndex == NSNotFound) { + ASDisplayNodeFailAssert(@"Try to insert node on an index that was not found"); + return; + } + + if (self.layerBacked && !subnode.layerBacked) { + ASDisplayNodeFailAssert(@"Cannot add a view-backed node as a subnode of a layer-backed node. Supernode: %@, subnode: %@", self, subnode); + return; + } + + BOOL isRasterized = subtreeIsRasterized(self); + if (isRasterized && subnode.nodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot add loaded node %@ to rasterized subtree of node %@", ASObjectDescriptionMakeTiny(subnode), ASObjectDescriptionMakeTiny(self)); + return; + } + + __instanceLock__.lock(); + NSUInteger subnodesCount = _subnodes.count; + __instanceLock__.unlock(); + if (subnodeIndex > subnodesCount || subnodeIndex < 0) { + ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)subnodeIndex, (long)subnodesCount); + return; + } + + // Disable appearance methods during move between supernodes, but make sure we restore their state after we do our thing + ASDisplayNode *oldParent = subnode.supernode; + BOOL disableNotifications = shouldDisableNotificationsForMovingBetweenParents(oldParent, self); + if (disableNotifications) { + [subnode __incrementVisibilityNotificationsDisabled]; + } + + [subnode _removeFromSupernode]; + [oldSubnode _removeFromSupernode]; + + __instanceLock__.lock(); + if (_subnodes == nil) { + _subnodes = [[NSMutableArray alloc] init]; + } + [_subnodes insertObject:subnode atIndex:subnodeIndex]; + _cachedSubnodes = nil; + __instanceLock__.unlock(); + + // This call will apply our .hierarchyState to the new subnode. + // If we are a managed hierarchy, as in ASCellNode trees, it will also apply our .interfaceState. + [subnode _setSupernode:self]; + + // If this subnode will be rasterized, enter hierarchy if needed + // TODO: Move this into _setSupernode: ? + if (isRasterized) { + if (self.inHierarchy) { + [subnode __enterHierarchy]; + } + } else if (self.nodeLoaded) { + // If not rasterizing, and node is loaded insert the subview/sublayer now. + [self _insertSubnodeSubviewOrSublayer:subnode atIndex:sublayerIndex]; + } // Otherwise we will insert subview/sublayer when we get loaded + + ASDisplayNodeAssert(disableNotifications == shouldDisableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated"); + if (disableNotifications) { + [subnode __decrementVisibilityNotificationsDisabled]; + } +} + +/* + * Inserts the view or layer of the given node at the given index + * + * @param subnode The subnode to insert + * @param idx The index in _view.subviews or _layer.sublayers at which to insert the subnode.view or + * subnode.layer of the subnode + */ +- (void)_insertSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(self.nodeLoaded, @"_insertSubnodeSubviewOrSublayer:atIndex: should never be called before our own view is created"); + + ASDisplayNodeAssert(idx != NSNotFound, @"Try to insert node on an index that was not found"); + if (idx == NSNotFound) { + return; + } + + // Because the view and layer can only be created and destroyed on Main, that is also the only thread + // where the view and layer can change. We can avoid locking. + + // If we can use view API, do. Due to an apple bug, -insertSubview:atIndex: actually wants a LAYER index, + // which we pass in. + if (canUseViewAPI(self, subnode)) { + [_view insertSubview:subnode.view atIndex:idx]; + } else { + [_layer insertSublayer:subnode.layer atIndex:(unsigned int)idx]; + } +} + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeLogEvent(self, @"addSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _addSubnode:subnode]; +} + +- (void)_addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDisplayNodeAssert(subnode, @"Cannot insert a nil subnode"); + + // Don't add if it's already a subnode + ASDisplayNode *oldParent = subnode.supernode; + if (!subnode || subnode == self || oldParent == self) { + return; + } + + NSUInteger subnodesIndex; + NSUInteger sublayersIndex; + { + MutexLocker l(__instanceLock__); + subnodesIndex = _subnodes.count; + sublayersIndex = _layer.sublayers.count; + } + + [self _insertSubnode:subnode atSubnodeIndex:subnodesIndex sublayerIndex:sublayersIndex andRemoveSubnode:nil]; +} + +- (void)_addSubnodeViewsAndLayers +{ + ASDisplayNodeAssertMainThread(); + + TIME_SCOPED(_debugTimeToAddSubnodeViews); + + for (ASDisplayNode *node in self.subnodes) { + [self _addSubnodeSubviewOrSublayer:node]; + } +} + +- (void)_addSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertMainThread(); + + // Due to a bug in Apple's framework we have to use the layer index to insert a subview + // so just use the count of the sublayers to add the subnode + NSInteger idx = _layer.sublayers.count; // No locking is needed as it's main thread only + [self _insertSubnodeSubviewOrSublayer:subnode atIndex:idx]; +} + +- (void)replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeLogEvent(self, @"replaceSubnode: %@ withSubnode: %@ with automaticallyManagesSubnodes: %@", + oldSubnode, replacementSubnode, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _replaceSubnode:oldSubnode withSubnode:replacementSubnode]; +} + +- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (replacementSubnode == nil) { + ASDisplayNodeFailAssert(@"Invalid subnode to replace"); + return; + } + + if (oldSubnode.supernode != self) { + ASDisplayNodeFailAssert(@"Old Subnode to replace must be a subnode"); + return; + } + + ASDisplayNodeAssert(!(self.nodeLoaded && !oldSubnode.nodeLoaded), @"We have view loaded, but child node does not."); + + NSInteger subnodeIndex; + NSInteger sublayerIndex = NSNotFound; + { + MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + subnodeIndex = [_subnodes indexOfObjectIdenticalTo:oldSubnode]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + sublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:oldSubnode.layer]; + ASDisplayNodeAssert(sublayerIndex != NSNotFound, @"Somehow oldSubnode's supernode is self, yet we could not find it in our layers to replace"); + if (sublayerIndex == NSNotFound) { + return; + } + } + } + } + + [self _insertSubnode:replacementSubnode atSubnodeIndex:subnodeIndex sublayerIndex:sublayerIndex andRemoveSubnode:oldSubnode]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ belowSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, below, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode belowSubnode:below]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + if (below.supernode != self) { + ASDisplayNodeFailAssert(@"Node to insert below must be a subnode"); + return; + } + + NSInteger belowSubnodeIndex; + NSInteger belowSublayerIndex = NSNotFound; + { + MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + belowSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:below]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + belowSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:below.layer]; + ASDisplayNodeAssert(belowSublayerIndex != NSNotFound, @"Somehow below's supernode is self, yet we could not find it in our layers to reference"); + if (belowSublayerIndex == NSNotFound) + return; + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to + // insert it will mess up our calculation + if (subnode.supernode == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes < belowSubnodeIndex) { + belowSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers < belowSublayerIndex) { + belowSublayerIndex--; + } + } + } + } + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find below in subnodes"); + + [self _insertSubnode:subnode atSubnodeIndex:belowSubnodeIndex sublayerIndex:belowSublayerIndex andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ abodeSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, above, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode aboveSubnode:above]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + if (above.supernode != self) { + ASDisplayNodeFailAssert(@"Node to insert above must be a subnode"); + return; + } + + NSInteger aboveSubnodeIndex; + NSInteger aboveSublayerIndex = NSNotFound; + { + MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + aboveSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:above]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + aboveSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:above.layer]; + ASDisplayNodeAssert(aboveSublayerIndex != NSNotFound, @"Somehow above's supernode is self, yet we could not find it in our layers to replace"); + if (aboveSublayerIndex == NSNotFound) + return; + } + + ASDisplayNodeAssert(aboveSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to + // insert it will mess up our calculation + if (subnode.supernode == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes <= aboveSubnodeIndex) { + aboveSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers <= aboveSublayerIndex) { + aboveSublayerIndex--; + } + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:incrementIfFound(aboveSubnodeIndex) sublayerIndex:incrementIfFound(aboveSublayerIndex) andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ atIndex: %td with automaticallyManagesSubnodes: %@", + subnode, idx, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode atIndex:idx]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + NSInteger sublayerIndex = NSNotFound; + { + MutexLocker l(__instanceLock__); + + if (idx > _subnodes.count || idx < 0) { + ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)idx, (long)_subnodes.count); + return; + } + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + // Account for potentially having other subviews + if (_layer && idx == 0) { + sublayerIndex = 0; + } else if (_layer) { + ASDisplayNode *positionInRelationTo = (_subnodes.count > 0 && idx > 0) ? _subnodes[idx - 1] : nil; + if (positionInRelationTo) { + sublayerIndex = incrementIfFound([_layer.sublayers indexOfObjectIdenticalTo:positionInRelationTo.layer]); + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:idx sublayerIndex:sublayerIndex andRemoveSubnode:nil]; +} + +- (void)_removeSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + // Don't call self.supernode here because that will retain/autorelease the supernode. This method -_removeSupernode: is often called while tearing down a node hierarchy, and the supernode in question might be in the middle of its -dealloc. The supernode is never messaged, only compared by value, so this is safe. + // The particular issue that triggers this edge case is when a node calls -removeFromSupernode on a subnode from within its own -dealloc method. + if (!subnode || subnode.supernode != self) { + return; + } + + __instanceLock__.lock(); + [_subnodes removeObjectIdenticalTo:subnode]; + _cachedSubnodes = nil; + __instanceLock__.unlock(); + + [subnode _setSupernode:nil]; +} + +- (void)removeFromSupernode +{ + ASDisplayNodeLogEvent(self, @"removeFromSupernode with automaticallyManagesSubnodes: %@", + self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _removeFromSupernode]; +} + +- (void)_removeFromSupernode +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + __instanceLock__.lock(); + __weak ASDisplayNode *supernode = _supernode; + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + __instanceLock__.unlock(); + + [self _removeFromSupernode:supernode view:view layer:layer]; +} + +- (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode +{ + ASDisplayNodeAssertThreadAffinity(self); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + __instanceLock__.lock(); + + // Only remove if supernode is still the expected supernode + if (!ASObjectIsEqual(_supernode, supernode)) { + __instanceLock__.unlock(); + return; + } + + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + __instanceLock__.unlock(); + + [self _removeFromSupernode:supernode view:view layer:layer]; +} + +- (void)_removeFromSupernode:(ASDisplayNode *)supernode view:(UIView *)view layer:(CALayer *)layer +{ + // Note: we continue even if supernode is nil to ensure view/layer are removed from hierarchy. + + if (supernode != nil) { + as_log_verbose(ASNodeLog(), "Remove %@ from supernode %@", self, supernode); + } + + // Clear supernode's reference to us before removing the view from the hierarchy, as _ASDisplayView + // will trigger us to clear our _supernode pointer in willMoveToSuperview:nil. + // This may result in removing the last strong reference, triggering deallocation after this method. + [supernode _removeSubnode:self]; + + if (view != nil) { + [view removeFromSuperview]; + } else if (layer != nil) { + [layer removeFromSuperlayer]; + } +} + +#pragma mark - Visibility API + +- (BOOL)__visibilityNotificationsDisabled +{ + // Currently, this method is only used by the testing infrastructure to verify this internal feature. + MutexLocker l(__instanceLock__); + return _flags.visibilityNotificationsDisabled > 0; +} + +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled +{ + MutexLocker l(__instanceLock__); + return (_hierarchyState & ASHierarchyStateTransitioningSupernodes); +} + +- (void)__incrementVisibilityNotificationsDisabled +{ + __instanceLock__.lock(); + const size_t maxVisibilityIncrement = (1ULL< 0, @"Can't decrement past 0"); + if (_flags.visibilityNotificationsDisabled > 0) { + _flags.visibilityNotificationsDisabled--; + } + BOOL visibilityNotificationsDisabled = (_flags.visibilityNotificationsDisabled == 0); + __instanceLock__.unlock(); + + if (visibilityNotificationsDisabled) { + // Must have just transitioned from 1 to 0. Notify all subnodes that we are no longer in a disabled state. + // FIXME: This system should be revisited when refactoring and consolidating the implementation of the + // addSubnode: and insertSubnode:... methods. As implemented, though logically irrelevant for expected use cases, + // multiple nodes in the subtree below may have a non-zero visibilityNotification count and still have + // the ASHierarchyState bit cleared (the only value checked when reading this state). + [self exitHierarchyState:ASHierarchyStateTransitioningSupernodes]; + } +} + +#pragma mark - Placeholder + +- (void)_locked_layoutPlaceholderIfNecessary +{ + ASAssertLocked(__instanceLock__); + if ([self _locked_shouldHavePlaceholderLayer]) { + [self _locked_setupPlaceholderLayerIfNeeded]; + } + // Update the placeholderLayer size in case the node size has changed since the placeholder was added. + _placeholderLayer.frame = self.threadSafeBounds; +} + +- (BOOL)_locked_shouldHavePlaceholderLayer +{ + ASAssertLocked(__instanceLock__); + return (_placeholderEnabled && [self _implementsDisplay]); +} + +- (void)_locked_setupPlaceholderLayerIfNeeded +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + + if (!_placeholderLayer) { + _placeholderLayer = [CALayer layer]; + // do not set to CGFLOAT_MAX in the case that something needs to be overtop the placeholder + _placeholderLayer.zPosition = 9999.0; + } + + if (_placeholderLayer.contents == nil) { + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + if (_placeholderImage) { + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(_placeholderImage.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetResizableContents(_placeholderLayer, _placeholderImage); + } else { + _placeholderLayer.contentsScale = self.contentsScale; + _placeholderLayer.contents = (id)_placeholderImage.CGImage; + } + } + } +} + +- (UIImage *)placeholderImage +{ + // Subclass hook + return nil; +} + +- (BOOL)placeholderShouldPersist +{ + // Subclass hook + return NO; +} + +#pragma mark - Hierarchy State + +- (BOOL)isInHierarchy +{ + MutexLocker l(__instanceLock__); + return _flags.isInHierarchy; +} + +- (void)__enterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy"); + ASDisplayNodeLogEvent(self, @"enterHierarchy"); + + // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. + __instanceLock__.lock(); + + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isEnteringHierarchy = YES; + _flags.isInHierarchy = YES; + + // Don't call -willEnterHierarchy while holding __instanceLock__. + // This method and subsequent ones (i.e -interfaceState and didEnter(.*)State) + // don't expect that they are called while the lock is being held. + // More importantly, didEnter(.*)State methods are meant to be overriden by clients. + // And so they can potentially walk up the node tree and cause deadlocks, or do expensive tasks and cause the lock to be held for too long. + __instanceLock__.unlock(); + [self willEnterHierarchy]; + for (ASDisplayNode *subnode in self.subnodes) { + [subnode __enterHierarchy]; + } + __instanceLock__.lock(); + + _flags.isEnteringHierarchy = NO; + + // If we don't have contents finished drawing by the time we are on screen, immediately add the placeholder (if it is enabled and we do have something to draw). + if (self.contents == nil && [self _implementsDisplay]) { + CALayer *layer = self.layer; + [layer setNeedsDisplay]; + + if ([self _locked_shouldHavePlaceholderLayer]) { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self _locked_setupPlaceholderLayerIfNeeded]; + _placeholderLayer.opacity = 1.0; + [CATransaction commit]; + [layer addSublayer:_placeholderLayer]; + } + } + } + + __instanceLock__.unlock(); + + [self didEnterHierarchy]; +} + +- (void)__exitHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"Should not cause recursive __exitHierarchy"); + ASDisplayNodeLogEvent(self, @"exitHierarchy"); + + // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. + __instanceLock__.lock(); + + if (_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isExitingHierarchy = YES; + _flags.isInHierarchy = NO; + + // Don't call -didExitHierarchy while holding __instanceLock__. + // This method and subsequent ones (i.e -interfaceState and didExit(.*)State) + // don't expect that they are called while the lock is being held. + // More importantly, didExit(.*)State methods are meant to be overriden by clients. + // And so they can potentially walk up the node tree and cause deadlocks, or do expensive tasks and cause the lock to be held for too long. + __instanceLock__.unlock(); + [self didExitHierarchy]; + for (ASDisplayNode *subnode in self.subnodes) { + [subnode __exitHierarchy]; + } + __instanceLock__.lock(); + + _flags.isExitingHierarchy = NO; + } + + __instanceLock__.unlock(); +} + +- (void)enterHierarchyState:(ASHierarchyState)hierarchyState +{ + if (hierarchyState == ASHierarchyStateNormal) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *node) { + node.hierarchyState |= hierarchyState; + }); +} + +- (void)exitHierarchyState:(ASHierarchyState)hierarchyState +{ + if (hierarchyState == ASHierarchyStateNormal) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *node) { + node.hierarchyState &= (~hierarchyState); + }); +} + +- (ASHierarchyState)hierarchyState +{ + MutexLocker l(__instanceLock__); + return _hierarchyState; +} + +- (void)setHierarchyState:(ASHierarchyState)newState +{ + ASHierarchyState oldState = ASHierarchyStateNormal; + { + MutexLocker l(__instanceLock__); + if (_hierarchyState == newState) { + return; + } + oldState = _hierarchyState; + _hierarchyState = newState; + } + + // Entered rasterization state. + if (newState & ASHierarchyStateRasterized) { + ASDisplayNodeAssert(checkFlag(Synchronous) == NO, @"Node created using -initWithViewBlock:/-initWithLayerBlock: cannot be added to subtree of node with subtree rasterization enabled. Node: %@", self); + } + + // Entered or exited range managed state. + if ((newState & ASHierarchyStateRangeManaged) != (oldState & ASHierarchyStateRangeManaged)) { + if (newState & ASHierarchyStateRangeManaged) { + [self enterInterfaceState:self.supernode.pendingInterfaceState]; + } else { + // The case of exiting a range-managed state should be fairly rare. Adding or removing the node + // to a view hierarchy will cause its interfaceState to be either fully set or unset (all fields), + // but because we might be about to be added to a view hierarchy, exiting the interface state now + // would cause inefficient churn. The tradeoff is that we may not clear contents / fetched data + // for nodes that are removed from a managed state and then retained but not used (bad idea anyway!) + } + } + + if ((newState & ASHierarchyStateLayoutPending) != (oldState & ASHierarchyStateLayoutPending)) { + if (newState & ASHierarchyStateLayoutPending) { + // Entering layout pending state + } else { + // Leaving layout pending state, reset related properties + MutexLocker l(__instanceLock__); + _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; + _pendingLayoutTransition = nil; + } + } + + ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState)); + as_log_verbose(ASNodeLog(), "%s%@ %@", sel_getName(_cmd), NSStringFromASHierarchyStateChange(oldState, newState), self); +} + +- (void)willEnterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isEnteringHierarchy, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); + + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateInHierarchy; + } else if (ASCATransactionQueueGet().enabled) { + __instanceLock__.lock(); + ASInterfaceState state = _preExitingInterfaceState; + _preExitingInterfaceState = ASInterfaceStateNone; + __instanceLock__.unlock(); + // Layer thrash happened, revert to before exiting. + if (state != ASInterfaceStateNone) { + self.interfaceState = state; + } + } +} + +- (void)didEnterHierarchy { + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"You should never call -didEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASDisplayNodeAssert(_flags.isInHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); +} + +- (void)didExitHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isExitingHierarchy, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); + + // This case is important when tearing down hierarchies. We must deliver a visibileStateDidChange:NO callback, as part our API guarantee that this method can be used for + // things like data analytics about user content viewing. We cannot call the method in the dealloc as any incidental retain operations in client code would fail. + // Additionally, it may be that a Standard UIView which is containing us is moving between hierarchies, and we should not send the call if we will be re-added in the + // same runloop. Strategy: strong reference (might be the last!), wait one runloop, and confirm we are still outside the hierarchy (both layer-backed and view-backed). + // TODO: This approach could be optimized by only performing the dispatch for root elements + recursively apply the interface state change. This would require a closer + // integration with _ASDisplayLayer to ensure that the superlayer pointer has been cleared by this stage (to check if we are root or not), or a different delegate call. + +#if !ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateNone; + return; + } +#endif + if (ASInterfaceStateIncludesVisible(self.pendingInterfaceState)) { + void(^exitVisibleInterfaceState)(void) = ^{ + // This block intentionally retains self. + __instanceLock__.lock(); + unsigned isStillInHierarchy = _flags.isInHierarchy; + BOOL isVisible = ASInterfaceStateIncludesVisible(_pendingInterfaceState); + ASInterfaceState newState = (_pendingInterfaceState & ~ASInterfaceStateVisible); + // layer may be thrashed, we need to remember the state so we can reset if it enters in same runloop later. + _preExitingInterfaceState = _pendingInterfaceState; + __instanceLock__.unlock(); + if (!isStillInHierarchy && isVisible) { +#if ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + newState = ASInterfaceStateNone; + } +#endif + self.interfaceState = newState; + } + }; + + if (!ASCATransactionQueueGet().enabled) { + dispatch_async(dispatch_get_main_queue(), exitVisibleInterfaceState); + } else { + exitVisibleInterfaceState(); + } + } +} + +#pragma mark - Interface State + +/** + * We currently only set interface state on nodes in table/collection views. For other nodes, if they are + * in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`. + */ +- (BOOL)supportsRangeManagedInterfaceState +{ + MutexLocker l(__instanceLock__); + return ASHierarchyStateIncludesRangeManaged(_hierarchyState); +} + +- (void)enterInterfaceState:(ASInterfaceState)interfaceState +{ + if (interfaceState == ASInterfaceStateNone) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState |= interfaceState; + }); +} + +- (void)exitInterfaceState:(ASInterfaceState)interfaceState +{ + if (interfaceState == ASInterfaceStateNone) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodeLogEvent(self, @"%s %@", sel_getName(_cmd), NSStringFromASInterfaceState(interfaceState)); + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState &= (~interfaceState); + }); +} + +- (void)recursivelySetInterfaceState:(ASInterfaceState)newInterfaceState +{ + as_activity_create_for_scope("Recursively set interface state"); + + // Instead of each node in the recursion assuming it needs to schedule itself for display, + // setInterfaceState: skips this when handling range-managed nodes (our whole subtree has this set). + // If our range manager intends for us to be displayed right now, and didn't before, get started! + BOOL shouldScheduleDisplay = [self supportsRangeManagedInterfaceState] && [self shouldScheduleDisplayWithNewInterfaceState:newInterfaceState]; + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState = newInterfaceState; + }); + if (shouldScheduleDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } +} + +- (ASInterfaceState)interfaceState +{ + MutexLocker l(__instanceLock__); + return _interfaceState; +} + +- (void)setInterfaceState:(ASInterfaceState)newState +{ + if (!ASCATransactionQueueGet().enabled) { + [self applyPendingInterfaceState:newState]; + } else { + MutexLocker l(__instanceLock__); + if (_pendingInterfaceState != newState) { + _pendingInterfaceState = newState; + [ASCATransactionQueueGet() enqueue:self]; + } + } +} + +- (ASInterfaceState)pendingInterfaceState +{ + MutexLocker l(__instanceLock__); + return _pendingInterfaceState; +} + +- (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState +{ + //This method is currently called on the main thread. The assert has been added here because all of the + //did(Enter|Exit)(Display|Visible|Preload)State methods currently guarantee calling on main. + ASDisplayNodeAssertMainThread(); + + // This method manages __instanceLock__ itself, to ensure the lock is not held while didEnter/Exit(.*)State methods are called, thus avoid potential deadlocks + ASAssertUnlocked(__instanceLock__); + + ASInterfaceState oldState = ASInterfaceStateNone; + ASInterfaceState newState = ASInterfaceStateNone; + { + MutexLocker l(__instanceLock__); + // newPendingState will not be used when ASCATransactionQueue is enabled + // and use _pendingInterfaceState instead for interfaceState update. + if (!ASCATransactionQueueGet().enabled) { + _pendingInterfaceState = newPendingState; + } + oldState = _interfaceState; + newState = _pendingInterfaceState; + if (newState == oldState) { + return; + } + _interfaceState = newState; + _preExitingInterfaceState = ASInterfaceStateNone; + } + + // It should never be possible for a node to be visible but not be allowed / expected to display. + ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); + + // TODO: Trigger asynchronous measurement if it is not already cached or being calculated. + // if ((newState & ASInterfaceStateMeasureLayout) != (oldState & ASInterfaceStateMeasureLayout)) { + // } + + // For the Preload and Display ranges, we don't want to call -clear* if not being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + // Still, the interfaceState should be updated to the current state of the node; just don't act on the transition. + + // Entered or exited data loading state. + BOOL nowPreload = ASInterfaceStateIncludesPreload(newState); + BOOL wasPreload = ASInterfaceStateIncludesPreload(oldState); + + if (nowPreload != wasPreload) { + if (nowPreload) { + [self _didEnterPreloadState]; + } else { + // We don't want to call -didExitPreloadState on nodes that aren't being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + if ([self supportsRangeManagedInterfaceState]) { + [self _didExitPreloadState]; + } + } + } + + // Entered or exited contents rendering state. + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(newState); + BOOL wasDisplay = ASInterfaceStateIncludesDisplay(oldState); + + if (nowDisplay != wasDisplay) { + if ([self supportsRangeManagedInterfaceState]) { + if (nowDisplay) { + // Once the working window is eliminated (ASRangeHandlerRender), trigger display directly here. + [self setDisplaySuspended:NO]; + } else { + [self setDisplaySuspended:YES]; + //schedule clear contents on next runloop + dispatch_async(dispatch_get_main_queue(), ^{ + __instanceLock__.lock(); + ASInterfaceState interfaceState = _interfaceState; + __instanceLock__.unlock(); + if (ASInterfaceStateIncludesDisplay(interfaceState) == NO) { + [self clearContents]; + } + }); + } + } else { + // NOTE: This case isn't currently supported as setInterfaceState: isn't exposed externally, and all + // internal use cases are range-managed. When a node is visible, don't mess with display - CA will start it. + if (!ASInterfaceStateIncludesVisible(newState)) { + // Check _implementsDisplay purely for efficiency - it's faster even than calling -asyncLayer. + if ([self _implementsDisplay]) { + if (nowDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } else { + [[self asyncLayer] cancelAsyncDisplay]; + //schedule clear contents on next runloop + dispatch_async(dispatch_get_main_queue(), ^{ + __instanceLock__.lock(); + ASInterfaceState interfaceState = _interfaceState; + __instanceLock__.unlock(); + if (ASInterfaceStateIncludesDisplay(interfaceState) == NO) { + [self clearContents]; + } + }); + } + } + } + } + + if (nowDisplay) { + [self _didEnterDisplayState]; + } else { + [self _didExitDisplayState]; + } + } + + // Became visible or invisible. When range-managed, this represents literal visibility - at least one pixel + // is onscreen. If not range-managed, we can't guarantee more than the node being present in an onscreen window. + BOOL nowVisible = ASInterfaceStateIncludesVisible(newState); + BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState); + + if (nowVisible != wasVisible) { + if (nowVisible) { + [self _didEnterVisibleState]; + } else { + [self _didExitVisibleState]; + } + } + + // Log this change, unless it's just the node going from {} -> {Measure} because that change happens + // for all cell nodes and it isn't currently meaningful. + BOOL measureChangeOnly = ((oldState | newState) == ASInterfaceStateMeasureLayout); + if (!measureChangeOnly) { + as_log_verbose(ASNodeLog(), "%s %@ %@", sel_getName(_cmd), NSStringFromASInterfaceStateChange(oldState, newState), self); + } + + ASDisplayNodeLogEvent(self, @"interfaceStateDidChange: %@", NSStringFromASInterfaceStateChange(oldState, newState)); + [self _interfaceStateDidChange:newState fromState:oldState]; +} + +- (void)prepareForCATransactionCommit +{ + // Apply _pendingInterfaceState actual _interfaceState, note that ASInterfaceStateNone is not used. + [self applyPendingInterfaceState:ASInterfaceStateNone]; +} + +- (void)_interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertMainThread(); + [self interfaceStateDidChange:newState fromState:oldState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del interfaceStateDidChange:newState fromState:oldState]; + }]; +} + +- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState +{ + BOOL willDisplay = ASInterfaceStateIncludesDisplay(newInterfaceState); + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(self.interfaceState); + return willDisplay && (willDisplay != nowDisplay); +} + +- (void)addInterfaceStateDelegate:(id )interfaceStateDelegate +{ + MutexLocker l(__instanceLock__); + _hasHadInterfaceStateDelegates = YES; + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == nil) { + _interfaceStateDelegates[i] = interfaceStateDelegate; + return; + } + } + ASDisplayNodeFailAssert(@"Exceeded interface state delegate limit: %d", AS_MAX_INTERFACE_STATE_DELEGATES); +} + +- (void)removeInterfaceStateDelegate:(id )interfaceStateDelegate +{ + MutexLocker l(__instanceLock__); + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == interfaceStateDelegate) { + _interfaceStateDelegates[i] = nil; + break; + } + } +} + +- (BOOL)isVisible +{ + MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesVisible(_interfaceState); +} + +- (void)_didEnterVisibleState +{ + ASDisplayNodeAssertMainThread(); + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // Rasterized node's loading state is merged with root node of rasterized tree. + if (!(self.hierarchyState & ASHierarchyStateRasterized)) { + ASDisplayNodeAssert(self.isNodeLoaded, @"Node should be loaded before entering visible state."); + } +#endif + + ASAssertUnlocked(__instanceLock__); + [self didEnterVisibleState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterVisibleState]; + }]; + +#if AS_ENABLE_TIPS + [ASTipsController.shared nodeDidAppear:self]; +#endif +} + +- (void)_didExitVisibleState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didExitVisibleState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitVisibleState]; + }]; +} + +- (BOOL)isInDisplayState +{ + MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesDisplay(_interfaceState); +} + +- (void)_didEnterDisplayState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didEnterDisplayState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterDisplayState]; + }]; +} + +- (void)_didExitDisplayState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didExitDisplayState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitDisplayState]; + }]; +} + +- (BOOL)isInPreloadState +{ + MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesPreload(_interfaceState); +} + +- (void)setNeedsPreload +{ + if (self.isInPreloadState) { + [self recursivelyPreload]; + } +} + +- (void)recursivelyPreload +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node didEnterPreloadState]; + }); + }); +} + +- (void)recursivelyClearPreloadedData +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node didExitPreloadState]; + }); + }); +} + +- (void)_didEnterPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didEnterPreloadState]; + + // If this node has ASM enabled and is not yet visible, force a layout pass to apply its applicable pending layout, if any, + // so that its subnodes are inserted/deleted and start preloading right away. + // + // - If it has an up-to-date layout (and subnodes), calling -layoutIfNeeded will be fast. + // + // - If it doesn't have a calculated or pending layout that fits its current bounds, a measurement pass will occur + // (see -__layout and -_u_measureNodeWithBoundsIfNecessary:). This scenario is uncommon, + // and running a measurement pass here is a fine trade-off because preloading any time after this point would be late. + + if (self.automaticallyManagesSubnodes && !ASActivateExperimentalFeature(ASExperimentalDidEnterPreloadSkipASMLayout)) { + [self layoutIfNeeded]; + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterPreloadState]; + }]; +} + +- (void)_didExitPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didExitPreloadState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitPreloadState]; + }]; +} + +- (void)clearContents +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + MutexLocker l(__instanceLock__); + if (_flags.canClearContentsOfLayer) { + // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. + _layer.contents = nil; + } + + _placeholderLayer.contents = nil; + _placeholderImage = nil; +} + +- (void)recursivelyClearContents +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node clearContents]; + }); + }); +} + +- (void)enumerateInterfaceStateDelegates:(void (NS_NOESCAPE ^)(id))block +{ + ASAssertUnlocked(__instanceLock__); + + id dels[AS_MAX_INTERFACE_STATE_DELEGATES]; + int count = 0; + { + ASLockScopeSelf(); + // Fast path for non-delegating nodes. + if (!_hasHadInterfaceStateDelegates) { + return; + } + + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if ((dels[count] = _interfaceStateDelegates[i])) { + count++; + } + } + } + for (int i = 0; i < count; i++) { + block(dels[i]); + } +} + +#pragma mark - Gesture Recognizing + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // This method is only implemented on UIView on iOS 6+. + ASDisplayNodeAssertMainThread(); + + // No locking needed as it's main thread only + UIView *view = _view; + if (view == nil) { + return YES; + } + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = view.superview; + return [superview gestureRecognizerShouldBegin:gestureRecognizer]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + return [_view hitTest:point withEvent:event]; +} + +- (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop +{ + MutexLocker l(__instanceLock__); + _hitTestSlop = hitTestSlop; +} + +- (UIEdgeInsets)hitTestSlop +{ + MutexLocker l(__instanceLock__); + return _hitTestSlop; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + UIEdgeInsets slop = self.hitTestSlop; + if (_view && UIEdgeInsetsEqualToEdgeInsets(slop, UIEdgeInsetsZero)) { + // Safer to use UIView's -pointInside:withEvent: if we can. + return [_view pointInside:point withEvent:event]; + } else { + return CGRectContainsPoint(UIEdgeInsetsInsetRect(self.bounds, slop), point); + } +} + + +#pragma mark - Pending View State + +- (void)_locked_applyPendingStateToViewOrLayer +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert(self.nodeLoaded, @"must have a view or layer"); + + TIME_SCOPED(_debugTimeToApplyPendingState); + + // If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values + // for the view/layer are still valid. + [self _locked_applyPendingViewState]; + + if (_flags.displaySuspended) { + self._locked_asyncLayer.displaySuspended = YES; + } + if (!_flags.displaysAsynchronously) { + self._locked_asyncLayer.displaysAsynchronously = NO; + } +} + +- (void)applyPendingViewState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + AS::UniqueLock l(__instanceLock__); + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + l.unlock(); + [self __setNeedsLayout]; + l.lock(); + } + + [self _locked_applyPendingViewState]; +} + +- (void)_locked_applyPendingViewState +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert([self _locked_isNodeLoaded], @"Expected node to be loaded before applying pending state."); + + if (_flags.layerBacked) { + [_pendingViewState applyToLayer:_layer]; + } else { + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); + [_pendingViewState applyToView:_view withSpecialPropertiesHandling:specialPropertiesHandling]; + } + + // _ASPendingState objects can add up very quickly when adding + // many nodes. This is especially an issue in large collection views + // and table views. This needs to be weighed against the cost of + // reallocing a _ASPendingState. So in range managed nodes we + // delete the pending state, otherwise we just clear it. + if (ASHierarchyStateIncludesRangeManaged(_hierarchyState)) { + _pendingViewState = nil; + } else { + [_pendingViewState clearChanges]; + } +} + +// This method has proved helpful in a few rare scenarios, similar to a category extension on UIView, but assumes knowledge of _ASDisplayView. +// It's considered private API for now and its use should not be encouraged. +- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy +{ + ASDisplayNode *supernode = self.supernode; + while (supernode) { + if ([supernode isKindOfClass:supernodeClass]) + return supernode; + supernode = supernode.supernode; + } + if (!checkViewHierarchy) { + return nil; + } + + UIView *view = self.view.superview; + while (view) { + ASDisplayNode *viewNode = ((_ASDisplayView *)view).asyncdisplaykit_node; + if (viewNode) { + if ([viewNode isKindOfClass:supernodeClass]) + return viewNode; + } + + view = view.superview; + } + + return nil; +} + +#pragma mark - Performance Measurement + +- (void)setMeasurementOptions:(ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + MutexLocker l(__instanceLock__); + _measurementOptions = measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + MutexLocker l(__instanceLock__); + return _measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurements)performanceMeasurements +{ + MutexLocker l(__instanceLock__); + ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN, .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN }; + if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec) { + measurements.layoutSpecNumberOfPasses = _layoutSpecNumberOfPasses; + measurements.layoutSpecTotalTime = _layoutSpecTotalTime; + } + if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation) { + measurements.layoutComputationNumberOfPasses = _layoutComputationNumberOfPasses; + measurements.layoutComputationTotalTime = _layoutComputationTotalTime; + } + return measurements; +} + +#pragma mark - Accessibility + +- (void)setIsAccessibilityContainer:(BOOL)isAccessibilityContainer +{ + MutexLocker l(__instanceLock__); + _isAccessibilityContainer = isAccessibilityContainer; +} + +- (BOOL)isAccessibilityContainer +{ + MutexLocker l(__instanceLock__); + return _isAccessibilityContainer; +} + +- (NSString *)defaultAccessibilityLabel +{ + return nil; +} + +- (NSString *)defaultAccessibilityHint +{ + return nil; +} + +- (NSString *)defaultAccessibilityValue +{ + return nil; +} + +- (NSString *)defaultAccessibilityIdentifier +{ + return nil; +} + +- (UIAccessibilityTraits)defaultAccessibilityTraits +{ + return UIAccessibilityTraitNone; +} + +#pragma mark - Debugging (Private) + +#if ASEVENTLOG_ENABLE +- (ASEventLog *)eventLog +{ + return _eventLog; +} +#endif + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + ASPushMainThreadAssertionsDisabled(); + + NSString *debugName = self.debugName; + if (debugName.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(debugName) }]; + } + + NSString *axId = self.accessibilityIdentifier; + if (axId.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(axId) }]; + } + + ASPopMainThreadAssertionsDisabled(); + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if (self.debugName.length > 0) { + [result addObject:@{ @"debugName" : ASStringWithQuotesIfMultiword(self.debugName)}]; + } + if (self.accessibilityIdentifier.length > 0) { + [result addObject:@{ @"axId": ASStringWithQuotesIfMultiword(self.accessibilityIdentifier) }]; + } + + CGRect windowFrame = [self _frameInWindow]; + if (CGRectIsNull(windowFrame) == NO) { + [result addObject:@{ @"frameInWindow" : [NSValue valueWithCGRect:windowFrame] }]; + } + + // Attempt to find view controller. + // Note that the convenience method asdk_associatedViewController has an assertion + // that it's run on main. Since this is a debug method, let's bypass the assertion + // and run up the chain ourselves. + if (_view != nil) { + for (UIResponder *responder in [_view asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc) { + [result addObject:@{ @"viewController" : ASObjectDescriptionMakeTiny(vc) }]; + break; + } + } + } + + if (_view != nil) { + [result addObject:@{ @"alpha" : @(_view.alpha) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_view.frame] }]; + } else if (_layer != nil) { + [result addObject:@{ @"alpha" : @(_layer.opacity) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_layer.frame] }]; + } else if (_pendingViewState != nil) { + [result addObject:@{ @"alpha" : @(_pendingViewState.alpha) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_pendingViewState.frame] }]; + } +#ifndef MINIMAL_ASDK + // Check supernode so that if we are a cell node we don't find self. + ASCellNode *cellNode = [self supernodeOfClass:[ASCellNode class] includingSelf:NO]; + if (cellNode != nil) { + [result addObject:@{ @"cellNode" : ASObjectDescriptionMakeTiny(cellNode) }]; + } +#endif + + [result addObject:@{ @"interfaceState" : NSStringFromASInterfaceState(self.interfaceState)} ]; + + if (_view != nil) { + [result addObject:@{ @"view" : ASObjectDescriptionMakeTiny(_view) }]; + } else if (_layer != nil) { + [result addObject:@{ @"layer" : ASObjectDescriptionMakeTiny(_layer) }]; + } else if (_viewClass != nil) { + [result addObject:@{ @"viewClass" : _viewClass }]; + } else if (_layerClass != nil) { + [result addObject:@{ @"layerClass" : _layerClass }]; + } else if (_viewBlock != nil) { + [result addObject:@{ @"viewBlock" : _viewBlock }]; + } else if (_layerBlock != nil) { + [result addObject:@{ @"layerBlock" : _layerBlock }]; + } + +#if TIME_DISPLAYNODE_OPS + NSString *creationTypeString = [NSString stringWithFormat:@"cr8:%.2lfms dl:%.2lfms ap:%.2lfms ad:%.2lfms", 1000 * _debugTimeToCreateView, 1000 * _debugTimeForDidLoad, 1000 * _debugTimeToApplyPendingState, 1000 * _debugTimeToAddSubnodeViews]; + [result addObject:@{ @"creationTypeString" : creationTypeString }]; +#endif + + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)debugDescription +{ + ASPushMainThreadAssertionsDisabled(); + const auto result = ASObjectDescriptionMake(self, [self propertiesForDebugDescription]); + ASPopMainThreadAssertionsDisabled(); + return result; +} + +// This should only be called for debugging. It's not thread safe and it doesn't assert. +// NOTE: Returns CGRectNull if the node isn't in a hierarchy. +- (CGRect)_frameInWindow +{ + if (self.isNodeLoaded == NO || self.isInHierarchy == NO) { + return CGRectNull; + } + + if (self.layerBacked) { + CALayer *rootLayer = _layer; + CALayer *nextLayer = nil; + while ((nextLayer = rootLayer.superlayer) != nil) { + rootLayer = nextLayer; + } + + return [_layer convertRect:self.threadSafeBounds toLayer:rootLayer]; + } else { + return [_view convertRect:self.threadSafeBounds toView:nil]; + } +} + +@end + +#pragma mark - ASDisplayNode (Debugging) + +@implementation ASDisplayNode (Debugging) + ++ (void)setShouldStoreUnflattenedLayouts:(BOOL)shouldStore +{ + storesUnflattenedLayouts.store(shouldStore); +} + ++ (BOOL)shouldStoreUnflattenedLayouts +{ + return storesUnflattenedLayouts.load(); +} + +- (ASLayout *)unflattenedCalculatedLayout +{ + MutexLocker l(__instanceLock__); + return _unflattenedLayout; +} + ++ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses +{ + suppressesInvalidCollectionUpdateExceptions.store(suppresses); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" ++ (BOOL)suppressesInvalidCollectionUpdateExceptions +{ + return suppressesInvalidCollectionUpdateExceptions.load(); +} +#pragma clang diagnostic pop + +- (NSString *)displayNodeRecursiveDescription +{ + return [self _recursiveDescriptionHelperWithIndent:@""]; +} + +- (NSString *)_recursiveDescriptionHelperWithIndent:(NSString *)indent +{ + NSMutableString *subtree = [[[indent stringByAppendingString:self.debugDescription] stringByAppendingString:@"\n"] mutableCopy]; + for (ASDisplayNode *n in self.subnodes) { + [subtree appendString:[n _recursiveDescriptionHelperWithIndent:[indent stringByAppendingString:@" | "]]]; + } + return subtree; +} + +- (NSString *)detailedLayoutDescription +{ + ASPushMainThreadAssertionsDisabled(); + MutexLocker l(__instanceLock__); + const auto props = [[NSMutableArray alloc] init]; + + [props addObject:@{ @"layoutVersion": @(_layoutVersion.load()) }]; + [props addObject:@{ @"bounds": [NSValue valueWithCGRect:self.bounds] }]; + + if (_calculatedDisplayNodeLayout.layout) { + [props addObject:@{ @"calculatedLayout": _calculatedDisplayNodeLayout.layout }]; + [props addObject:@{ @"calculatedVersion": @(_calculatedDisplayNodeLayout.version) }]; + [props addObject:@{ @"calculatedConstrainedSize" : NSStringFromASSizeRange(_calculatedDisplayNodeLayout.constrainedSize) }]; + if (_calculatedDisplayNodeLayout.requestedLayoutFromAbove) { + [props addObject:@{ @"calculatedRequestedLayoutFromAbove": @"YES" }]; + } + } + if (_pendingDisplayNodeLayout.layout) { + [props addObject:@{ @"pendingLayout": _pendingDisplayNodeLayout.layout }]; + [props addObject:@{ @"pendingVersion": @(_pendingDisplayNodeLayout.version) }]; + [props addObject:@{ @"pendingConstrainedSize" : NSStringFromASSizeRange(_pendingDisplayNodeLayout.constrainedSize) }]; + if (_pendingDisplayNodeLayout.requestedLayoutFromAbove) { + [props addObject:@{ @"pendingRequestedLayoutFromAbove": (id)kCFNull }]; + } + } + + ASPopMainThreadAssertionsDisabled(); + return ASObjectDescriptionMake(self, props); +} + +@end + +#pragma mark - ASDisplayNode UIKit / CA Categories + +// We use associated objects as a last resort if our view is not a _ASDisplayView ie it doesn't have the _node ivar to write to + +static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; + +@implementation UIView (ASDisplayNodeInternal) + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, weakProxy, OBJC_ASSOCIATION_RETAIN); // Weak reference to avoid cycle, since the node retains the view. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASWeakProxy *weakProxy = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return weakProxy.target; +} + +@end + +@implementation CALayer (ASDisplayNodeInternal) + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, weakProxy, OBJC_ASSOCIATION_RETAIN); // Weak reference to avoid cycle, since the node retains the layer. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASWeakProxy *weakProxy = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return weakProxy.target; +} + +@end + +@implementation UIView (AsyncDisplayKit) + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + if (subnode.layerBacked) { + // Call -addSubnode: so that we use the asyncdisplaykit_node path if possible. + [self.layer addSubnode:subnode]; + } else { + ASDisplayNode *selfNode = self.asyncdisplaykit_node; + if (selfNode) { + [selfNode addSubnode:subnode]; + } else { + if (subnode.supernode) { + [subnode removeFromSupernode]; + } + [self addSubview:subnode.view]; + } + } +} + +@end + +@implementation CALayer (AsyncDisplayKit) + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNode *selfNode = self.asyncdisplaykit_node; + if (selfNode) { + [selfNode addSubnode:subnode]; + } else { + if (subnode.supernode) { + [subnode removeFromSupernode]; + } + [self addSublayer:subnode.layer]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm.orig b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm.orig new file mode 100644 index 0000000000..50510817ab --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm.orig @@ -0,0 +1,3927 @@ +// +// ASDisplayNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) +#if TIME_DISPLAYNODE_OPS + #define TIME_SCOPED(outVar) ASDN::ScopeTimer t(outVar) +#else + #define TIME_SCOPED(outVar) +#endif +// This is trying to merge non-rangeManaged with rangeManaged, so both range-managed and standalone nodes wait before firing their exit-visibility handlers, as UIViewController transitions now do rehosting at both start & end of animation. +// Enable this will mitigate interface updating state when coalescing disabled. +// TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. +#define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 0 + +static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; +NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; + +// Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol. +// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 +@protocol CALayerDelegate; + +@interface ASDisplayNode () +/** + * See ASDisplayNodeInternal.h for ivars + */ + +@end + +@implementation ASDisplayNode + +@dynamic layoutElementType; + +@synthesize threadSafeBounds = _threadSafeBounds; + +static std::atomic_bool storesUnflattenedLayouts = ATOMIC_VAR_INIT(NO); + +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 ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLayerBacked) +{ + return isSynchronous && !isLayerBacked; +} + +_ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node) +{ + ASLockScope(node); + _ASPendingState *result = node->_pendingViewState; + if (result == nil) { + result = [[_ASPendingState alloc] init]; + node->_pendingViewState = result; + } + return result; +} + +/** + * Returns ASDisplayNodeFlags for the given class/instance. instance MAY BE NIL. + * + * @param c the class, required + * @param instance the instance, which may be nil. (If so, the class is inspected instead) + * @remarks The instance value is used only if we suspect the class may be dynamic (because it overloads + * +respondsToSelector: or -respondsToSelector.) In that case we use our "slow path", calling this + * method on each -init and passing the instance value. While this may seem like an unlikely scenario, + * it turns our our own internal tests use a dynamic class, so it's worth capturing this edge case. + * + * @return ASDisplayNode flags. + */ +static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *instance) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + struct ASDisplayNodeFlags flags = {0}; + + flags.isInHierarchy = NO; + flags.displaysAsynchronously = YES; + flags.shouldAnimateSizeChanges = YES; + flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); + flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); + if (instance) { + flags.implementsDrawParameters = ([instance respondsToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } else { + flags.implementsDrawParameters = ([c instancesRespondToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } + + + return flags; +} + +/** + * Returns ASDisplayNodeMethodOverrides for the given class + * + * @param c the class, required. + * + * @return ASDisplayNodeMethodOverrides. + */ +static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + ASDisplayNodeMethodOverrides overrides = ASDisplayNodeMethodOverrideNone; + + // Handling touches + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesBegan:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesBegan; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesMoved:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesMoved; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesCancelled:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesCancelled; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesEnded:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesEnded; + } + + // Responder chain + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canBecomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(becomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canResignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(resignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(isFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideIsFirstResponder; + } + + // Layout related methods + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(layoutSpecThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideLayoutSpecThatFits; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits:)) || + ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits: + restrictedToSize: + relativeToParentSize:))) { + overrides |= ASDisplayNodeMethodOverrideCalcLayoutThatFits; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideCalcSizeThatFits; + } + + return overrides; +} + ++ (void)initialize +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + if (self != [ASDisplayNode class]) { + + // Subclasses should never override these. Use unused to prevent warnings + __unused NSString *classString = NSStringFromClass(self); + + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:)), @"Subclass %@ must not override layoutThatFits: method. Instead override calculateLayoutThatFits:.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:parentSize:)), @"Subclass %@ must not override layoutThatFits:parentSize method. Instead override calculateLayoutThatFits:.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearPreloadedData)), @"Subclass %@ must not override recursivelyClearFetchedData method.", classString); + } else { + // Check if subnodes where modified during the creation of the layout + __block IMP originalLayoutSpecThatFitsIMP = ASReplaceMethodWithBlock(self, @selector(_locked_layoutElementThatFits:), ^(ASDisplayNode *_self, ASSizeRange sizeRange) { + NSArray *oldSubnodes = _self.subnodes; + ASLayoutSpec *layoutElement = ((ASLayoutSpec *( *)(id, SEL, ASSizeRange))originalLayoutSpecThatFitsIMP)(_self, @selector(_locked_layoutElementThatFits:), sizeRange); + NSArray *subnodes = _self.subnodes; + ASDisplayNodeAssert(oldSubnodes.count == subnodes.count, @"Adding or removing nodes in layoutSpecBlock or layoutSpecThatFits: is not allowed and can cause unexpected behavior."); + for (NSInteger i = 0; i < oldSubnodes.count; i++) { + ASDisplayNodeAssert(oldSubnodes[i] == subnodes[i], @"Adding or removing nodes in layoutSpecBlock or layoutSpecThatFits: is not allowed and can cause unexpected behavior."); + } + return layoutElement; + }); + } +#endif + + // Below we are pre-calculating values per-class and dynamically adding a method (_staticInitialize) to populate these values + // when each instance is constructed. These values don't change for each class, so there is significant performance benefit + // in doing it here. +initialize is guaranteed to be called before any instance method so it is safe to add this method here. + // Note that we take care to detect if the class overrides +respondsToSelector: or -respondsToSelector and take the slow path + // (recalculating for each instance) to make sure we are always correct. + + BOOL classOverridesRespondsToSelector = ASSubclassOverridesClassSelector([NSObject class], self, @selector(respondsToSelector:)); + BOOL instancesOverrideRespondsToSelector = ASSubclassOverridesSelector([NSObject class], self, @selector(respondsToSelector:)); + struct ASDisplayNodeFlags flags = GetASDisplayNodeFlags(self, nil); + ASDisplayNodeMethodOverrides methodOverrides = GetASDisplayNodeMethodOverrides(self); + + __unused Class initializeSelf = self; + + IMP staticInitialize = imp_implementationWithBlock(^(ASDisplayNode *node) { + ASDisplayNodeAssert(node.class == initializeSelf, @"Node class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", node.class, initializeSelf); + node->_flags = (classOverridesRespondsToSelector || instancesOverrideRespondsToSelector) ? GetASDisplayNodeFlags(node.class, node) : flags; + node->_methodOverrides = (classOverridesRespondsToSelector) ? GetASDisplayNodeMethodOverrides(node.class) : methodOverrides; + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); +} + +#if !AS_INITIALIZE_FRAMEWORK_MANUALLY ++ (void)load +{ + ASInitializeFrameworkMainThread(); +} +#endif + ++ (Class)viewClass +{ + return [_ASDisplayView class]; +} + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - Lifecycle + +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + +- (void)_initializeInstance +{ + [self _staticInitialize]; + +#if ASEVENTLOG_ENABLE + _eventLog = [[ASEventLog alloc] initWithObject:self]; +#endif + + _viewClass = [self.class viewClass]; + _layerClass = [self.class layerClass]; + BOOL isSynchronous = ![_viewClass isSubclassOfClass:[_ASDisplayView class]] + || ![_layerClass isSubclassOfClass:[_ASDisplayLayer class]]; + setFlag(Synchronous, isSynchronous); + + + _contentsScaleForDisplay = ASScreenScale(); + _drawingPriority = ASDefaultDrawingPriority; + + _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + + _layoutVersion = 1; + + _defaultLayoutTransitionDuration = 0.2; + _defaultLayoutTransitionDelay = 0.0; + _defaultLayoutTransitionOptions = UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionNone; + + _flags.canClearContentsOfLayer = YES; + _flags.canCallSetNeedsDisplayOfLayer = YES; + + _fallbackSafeAreaInsets = UIEdgeInsetsZero; + _fallbackInsetsLayoutMarginsFromSafeArea = YES; + _isViewControllerRoot = NO; + + _automaticallyRelayoutOnSafeAreaChanges = NO; + _automaticallyRelayoutOnLayoutMarginsChanges = NO; + + ASDisplayNodeLogEvent(self, @"init"); +} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + [self _initializeInstance]; + + return self; +} + +- (instancetype)initWithViewClass:(Class)viewClass +{ + if (!(self = [self init])) + return nil; + + ASDisplayNodeAssert([viewClass isSubclassOfClass:[UIView class]], @"should initialize with a subclass of UIView"); + + _viewClass = viewClass; + setFlag(Synchronous, ![viewClass isSubclassOfClass:[_ASDisplayView class]]); + + return self; +} + +- (instancetype)initWithLayerClass:(Class)layerClass +{ + if (!(self = [self init])) { + return nil; + } + + ASDisplayNodeAssert([layerClass isSubclassOfClass:[CALayer class]], @"should initialize with a subclass of CALayer"); + + _layerClass = layerClass; + _flags.layerBacked = YES; + setFlag(Synchronous, ![layerClass isSubclassOfClass:[_ASDisplayLayer class]]); + + return self; +} + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + return [self initWithViewBlock:viewBlock didLoadBlock:nil]; +} + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [self init])) { + return nil; + } + + [self setViewBlock:viewBlock]; + if (didLoadBlock != nil) { + [self onDidLoad:didLoadBlock]; + } + + return self; +} + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + return [self initWithLayerBlock:layerBlock didLoadBlock:nil]; +} + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [self init])) { + return nil; + } + + [self setLayerBlock:layerBlock]; + if (didLoadBlock != nil) { + [self onDidLoad:didLoadBlock]; + } + + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__); + +- (void)setViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + ASDisplayNodeAssertFalse(self.nodeLoaded); + ASDisplayNodeAssertNotNil(viewBlock, @"should initialize with a valid block that returns a UIView"); + + _viewBlock = viewBlock; + setFlag(Synchronous, YES); +} + +- (void)setLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + ASDisplayNodeAssertFalse(self.nodeLoaded); + ASDisplayNodeAssertNotNil(layerBlock, @"should initialize with a valid block that returns a CALayer"); + + _layerBlock = layerBlock; + _flags.layerBacked = YES; + setFlag(Synchronous, YES); +} + +- (ASDisplayNodeMethodOverrides)methodOverrides +{ + return _methodOverrides; +} + +- (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body +{ + ASDN::MutexLocker l(__instanceLock__); + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexUnlocker l(__instanceLock__); + body(self); + } else if (_onDidLoadBlocks == nil) { + _onDidLoadBlocks = [NSMutableArray arrayWithObject:body]; + } else { + [_onDidLoadBlocks addObject:body]; + } +} + +- (void)dealloc +{ + _flags.isDeallocating = YES; + + // Synchronous nodes may not be able to call the hierarchy notifications, so only enforce for regular nodes. + ASDisplayNodeAssert(checkFlag(Synchronous) || !ASInterfaceStateIncludesVisible(_interfaceState), @"Node should always be marked invisible before deallocating. Node: %@", self); + + self.asyncLayer.asyncDelegate = nil; + _view.asyncdisplaykit_node = nil; + _layer.asyncdisplaykit_node = nil; + + // Remove any subnodes so they lose their connection to the now deallocated parent. This can happen + // because subnodes do not retain their supernode, but subnodes can legitimately remain alive if another + // thing outside the view hierarchy system (e.g. async display, controller code, etc). keeps a retained + // reference to subnodes. + + for (ASDisplayNode *subnode in _subnodes) + [subnode _setSupernode:nil]; + + [self scheduleIvarsForMainThreadDeallocation]; + + // TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. + [self _setSupernode:nil]; +} + +#pragma mark - Loading + +- (BOOL)_locked_shouldLoadViewOrLayer +{ + ASAssertLocked(__instanceLock__); + return !_flags.isDeallocating && !(_hierarchyState & ASHierarchyStateRasterized); +} + +- (UIView *)_locked_viewToLoad +{ + ASAssertLocked(__instanceLock__); + + UIView *view = nil; + if (_viewBlock) { + view = _viewBlock(); + ASDisplayNodeAssertNotNil(view, @"View block returned nil"); + ASDisplayNodeAssert(![view isKindOfClass:[_ASDisplayView class]], @"View block should return a synchronously displayed view"); + _viewBlock = nil; + _viewClass = [view class]; + } else { + view = [[_viewClass alloc] init]; + } + + // Special handling of wrapping UIKit components + if (checkFlag(Synchronous)) { + [self checkResponderCompatibility]; + + // UIImageView layers. More details on the flags + if ([_viewClass isSubclassOfClass:[UIImageView class]]) { + _flags.canClearContentsOfLayer = NO; + _flags.canCallSetNeedsDisplayOfLayer = NO; + } + + // UIActivityIndicator + if ([_viewClass isSubclassOfClass:[UIActivityIndicatorView class]] + || [_viewClass isSubclassOfClass:[UIVisualEffectView class]]) { + self.opaque = NO; + } + + // CAEAGLLayer + if([[view.layer class] isSubclassOfClass:[CAEAGLLayer class]]){ + _flags.canClearContentsOfLayer = NO; + } + } + + return view; +} + +- (CALayer *)_locked_layerToLoad +{ + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert(_flags.layerBacked, @"_layerToLoad is only for layer-backed nodes"); + + CALayer *layer = nil; + if (_layerBlock) { + layer = _layerBlock(); + ASDisplayNodeAssertNotNil(layer, @"Layer block returned nil"); + ASDisplayNodeAssert(![layer isKindOfClass:[_ASDisplayLayer class]], @"Layer block should return a synchronously displayed layer"); + _layerBlock = nil; + _layerClass = [layer class]; + } else { + layer = [[_layerClass alloc] init]; + } + + return layer; +} + +- (void)_locked_loadViewOrLayer +{ + ASAssertLocked(__instanceLock__); + + if (_flags.layerBacked) { + TIME_SCOPED(_debugTimeToCreateView); + _layer = [self _locked_layerToLoad]; + static int ASLayerDelegateAssociationKey; + + /** + * CALayer's .delegate property is documented to be weak, but the implementation is actually assign. + * Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node + * begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only + * way to avoid a dangling pointer is to use a weak proxy. + */ + ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self]; + _layer.delegate = (id)instance; + objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + TIME_SCOPED(_debugTimeToCreateView); + _view = [self _locked_viewToLoad]; + _view.asyncdisplaykit_node = self; + _layer = _view.layer; + } + _layer.asyncdisplaykit_node = self; + + self._locked_asyncLayer.asyncDelegate = self; +} + +- (void)_didLoad +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"didLoad"); + as_log_verbose(ASNodeLog(), "didLoad %@", self); + TIME_SCOPED(_debugTimeForDidLoad); + + [self didLoad]; + + __instanceLock__.lock(); + let onDidLoadBlocks = ASTransferStrong(_onDidLoadBlocks); + __instanceLock__.unlock(); + + for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { + block(self); + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLoad]; + }]; +} + +- (void)didLoad +{ + ASDisplayNodeAssertMainThread(); + + // Subclass hook +} + +- (BOOL)isNodeLoaded +{ + if (ASDisplayNodeThreadIsMain()) { + // Because the view and layer can only be created and destroyed on Main, that is also the only thread + // where the state of this property can change. As an optimization, we can avoid locking. + return _loaded(self); + } else { + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_isNodeLoaded]; + } +} + +- (BOOL)_locked_isNodeLoaded +{ + ASAssertLocked(__instanceLock__); + return _loaded(self); +} + +#pragma mark - Misc Setter / Getter + +- (UIView *)view +{ + ASDN::MutexLocker l(__instanceLock__); + + ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes"); + BOOL isLayerBacked = _flags.layerBacked; + if (isLayerBacked) { + return nil; + } + + if (_view != nil) { + return _view; + } + + if (![self _locked_shouldLoadViewOrLayer]) { + return nil; + } + + // Loading a view needs to happen on the main thread + ASDisplayNodeAssertMainThread(); + [self _locked_loadViewOrLayer]; + + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + // MutexUnlocker will re-lock at the end of scope. + ASDN::MutexUnlocker u(__instanceLock__); + [self __setNeedsLayout]; + } + + [self _locked_applyPendingStateToViewOrLayer]; + + { + // The following methods should not be called with a lock + ASDN::MutexUnlocker u(__instanceLock__); + + // No need for the lock as accessing the subviews or layers are always happening on main + [self _addSubnodeViewsAndLayers]; + + // A subclass hook should never be called with a lock + [self _didLoad]; + } + + return _view; +} + +- (CALayer *)layer +{ + ASDN::MutexLocker l(__instanceLock__); + if (_layer != nil) { + return _layer; + } + + if (![self _locked_shouldLoadViewOrLayer]) { + return nil; + } + + // Loading a layer needs to happen on the main thread + ASDisplayNodeAssertMainThread(); + [self _locked_loadViewOrLayer]; + + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + // MutexUnlocker will re-lock at the end of scope. + ASDN::MutexUnlocker u(__instanceLock__); + [self __setNeedsLayout]; + } + + [self _locked_applyPendingStateToViewOrLayer]; + + { + // The following methods should not be called with a lock + ASDN::MutexUnlocker u(__instanceLock__); + + // No need for the lock as accessing the subviews or layers are always happening on main + [self _addSubnodeViewsAndLayers]; + + // A subclass hook should never be called with a lock + [self _didLoad]; + } + + return _layer; +} + +// Returns nil if the layer is not an _ASDisplayLayer; will not create the layer if nil. +- (_ASDisplayLayer *)asyncLayer +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_asyncLayer]; +} + +- (_ASDisplayLayer *)_locked_asyncLayer +{ + ASAssertLocked(__instanceLock__); + return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil; +} + +- (BOOL)isSynchronous +{ + return checkFlag(Synchronous); +} + +- (void)setLayerBacked:(BOOL)isLayerBacked +{ + // Only call this if assertions are enabled – it could be expensive. + ASDisplayNodeAssert(!isLayerBacked || self.supportsLayerBacking, @"Node %@ does not support layer backing.", self); + + ASDN::MutexLocker l(__instanceLock__); + if (_flags.layerBacked == isLayerBacked) { + return; + } + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeFailAssert(@"Cannot change layerBacked after view/layer has loaded."); + return; + } + + _flags.layerBacked = isLayerBacked; +} + +- (BOOL)isLayerBacked +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.layerBacked; +} + +- (BOOL)supportsLayerBacking +{ + ASDN::MutexLocker l(__instanceLock__); + return !checkFlag(Synchronous) && !_flags.viewEverHadAGestureRecognizerAttached && _viewClass == [_ASDisplayView class] && _layerClass == [_ASDisplayLayer class]; +} + +- (BOOL)shouldAnimateSizeChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.shouldAnimateSizeChanges; +} + +- (void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + ASDN::MutexLocker l(__instanceLock__); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + +- (CGRect)threadSafeBounds +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_threadSafeBounds]; +} + +- (CGRect)_locked_threadSafeBounds +{ + ASAssertLocked(__instanceLock__); + return _threadSafeBounds; +} + +- (void)setThreadSafeBounds:(CGRect)newBounds +{ + ASDN::MutexLocker l(__instanceLock__); + _threadSafeBounds = newBounds; +} + +- (void)nodeViewDidAddGestureRecognizer +{ + ASDN::MutexLocker l(__instanceLock__); + _flags.viewEverHadAGestureRecognizerAttached = YES; +} + +- (UIEdgeInsets)fallbackSafeAreaInsets +{ + ASDN::MutexLocker l(__instanceLock__); + return _fallbackSafeAreaInsets; +} + +- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets +{ + BOOL needsManualUpdate; + BOOL updatesLayoutMargins; + + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssertThreadAffinity(self); + + if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) { + return; + } + + _fallbackSafeAreaInsets = insets; + needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked; + updatesLayoutMargins = needsManualUpdate && [self _locked_insetsLayoutMarginsFromSafeArea]; + } + + if (needsManualUpdate) { + [self safeAreaInsetsDidChange]; + } + + if (updatesLayoutMargins) { + [self layoutMarginsDidChange]; + } +} + +- (void)_fallbackUpdateSafeAreaOnChildren +{ + ASDisplayNodeAssertThreadAffinity(self); + + UIEdgeInsets insets = self.safeAreaInsets; + CGRect bounds = self.bounds; + + for (ASDisplayNode *child in self.subnodes) { + if (AS_AT_LEAST_IOS11 && !child.layerBacked) { + // In iOS 11 view-backed nodes already know what their safe area is. + continue; + } + + if (child.viewControllerRoot) { + // Its safe area is controlled by a view controller. Don't override it. + continue; + } + + CGRect childFrame = child.frame; + UIEdgeInsets childInsets = UIEdgeInsetsMake(MAX(insets.top - (CGRectGetMinY(childFrame) - CGRectGetMinY(bounds)), 0), + MAX(insets.left - (CGRectGetMinX(childFrame) - CGRectGetMinX(bounds)), 0), + MAX(insets.bottom - (CGRectGetMaxY(bounds) - CGRectGetMaxY(childFrame)), 0), + MAX(insets.right - (CGRectGetMaxX(bounds) - CGRectGetMaxX(childFrame)), 0)); + + child.fallbackSafeAreaInsets = childInsets; + } +} + +- (BOOL)isViewControllerRoot +{ + ASDN::MutexLocker l(__instanceLock__); + return _isViewControllerRoot; +} + +- (void)setViewControllerRoot:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _isViewControllerRoot = flag; +} + +- (BOOL)automaticallyRelayoutOnSafeAreaChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnSafeAreaChanges; +} + +- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnSafeAreaChanges = flag; +} + +- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnLayoutMarginsChanges; +} + +- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnLayoutMarginsChanges = flag; +} + +- (void)__setNodeController:(ASNodeController *)controller +{ + // See docs for why we don't lock. + if (controller.shouldInvertStrongReference) { + _strongNodeController = controller; + _weakNodeController = nil; + } else { + _weakNodeController = controller; + _strongNodeController = nil; + } +} + +#pragma mark - UIResponder + +#define HANDLE_NODE_RESPONDER_METHOD(__sel) \ + /* All responder methods should be called on the main thread */ \ + ASDisplayNodeAssertMainThread(); \ + if (checkFlag(Synchronous)) { \ + /* If the view is not a _ASDisplayView subclass (Synchronous) just call through to the view as we + expect it's a non _ASDisplayView subclass that will respond */ \ + return [_view __sel]; \ + } else { \ + if (ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(__sel))) { \ + /* If the subclass overwrites canBecomeFirstResponder just call through + to it as we expect it will handle it */ \ + return [_view __sel]; \ + } else { \ + /* Call through to _ASDisplayView's superclass to get it handled */ \ + return [(_ASDisplayView *)_view __##__sel]; \ + } \ + } \ + +- (void)checkResponderCompatibility +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // There are certain cases we cannot handle and are not supported: + // 1. If the _view class is not a subclass of _ASDisplayView + if (checkFlag(Synchronous)) { + // 2. At least one UIResponder methods are overwritten in the node subclass + NSString *message = @"Overwritting %@ and having a backing view that is not an _ASDisplayView is not supported."; + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canBecomeFirstResponder)), ([NSString stringWithFormat:message, @"canBecomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(becomeFirstResponder)), ([NSString stringWithFormat:message, @"becomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canResignFirstResponder)), ([NSString stringWithFormat:message, @"canResignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(resignFirstResponder)), ([NSString stringWithFormat:message, @"resignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(isFirstResponder)), ([NSString stringWithFormat:message, @"isFirstResponder"])); + } +#endif +} + +- (BOOL)__canBecomeFirstResponder +{ + if (_view == nil) { + // By default we return NO if not view is created yet + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(canBecomeFirstResponder); +} + +- (BOOL)__becomeFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canBecomeFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(becomeFirstResponder); +} + +- (BOOL)__canResignFirstResponder +{ + if (_view == nil) { + // By default we return YES if no view is created yet + return YES; + } + + HANDLE_NODE_RESPONDER_METHOD(canResignFirstResponder); +} + +- (BOOL)__resignFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canResignFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(resignFirstResponder); +} + +- (BOOL)__isFirstResponder +{ + if (_view == nil) { + // If no view is created yet we can just return NO as it's unlikely it's the first responder + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(isFirstResponder); +} + +#pragma mark + +- (NSString *)debugName +{ + ASDN::MutexLocker l(__instanceLock__); + return _debugName; +} + +- (void)setDebugName:(NSString *)debugName +{ + ASDN::MutexLocker l(__instanceLock__); + if (!ASObjectIsEqual(_debugName, debugName)) { + _debugName = [debugName copy]; + } +} + +#pragma mark - Layout + +// At most a layoutSpecBlock or one of the three layout methods is overridden +#define __ASDisplayNodeCheckForLayoutMethodOverrides \ + ASDisplayNodeAssert(_layoutSpecBlock != NULL || \ + ((ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateSizeThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(layoutSpecThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateLayoutThatFits:)) ? 1 : 0)) <= 1, \ + @"Subclass %@ must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:", NSStringFromClass(self.class)) + + +#pragma mark + +- (BOOL)canLayoutAsynchronous +{ + return !self.isNodeLoaded; +} + +#pragma mark Layout Pass + +- (void)__setNeedsLayout +{ + [self invalidateCalculatedLayout]; +} + +- (void)invalidateCalculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + + _layoutVersion++; + + _unflattenedLayout = nil; + +#if YOGA + [self invalidateCalculatedYogaLayout]; +#endif +} + +- (void)__layout +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + BOOL loaded = NO; + { + ASDN::MutexLocker l(__instanceLock__); + loaded = [self _locked_isNodeLoaded]; + CGRect bounds = _threadSafeBounds; + + if (CGRectEqualToRect(bounds, CGRectZero)) { + // Performing layout on a zero-bounds view often results in frame calculations + // with negative sizes after applying margins, which will cause + // layoutThatFits: on subnodes to assert. + as_log_debug(OS_LOG_DISABLED, "Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); + return; + } + + // If a current layout transition is in progress there is no need to do a measurement and layout pass in here as + // this is supposed to happen within the layout transition process + if (_transitionID != ASLayoutElementContextInvalidTransitionID) { + return; + } + + as_activity_create_for_scope("-[ASDisplayNode __layout]"); + + // This method will confirm that the layout is up to date (and update if needed). + // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). + { + ASDN::MutexUnlocker u(__instanceLock__); + [self _u_measureNodeWithBoundsIfNecessary:bounds]; + } + + [self _locked_layoutPlaceholderIfNecessary]; + } + + [self _layoutSublayouts]; + + // Per API contract, `-layout` and `-layoutDidFinish` are called only if the node is loaded. + if (loaded) { + ASPerformBlockOnMainThread(^{ + [self layout]; + [self _layoutClipCornersIfNeeded]; + [self layoutDidFinish]; + }); + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + +- (void)layoutDidFinish +{ + // Hook for subclasses + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); +} + +#pragma mark Calculation + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ +<<<<<<< HEAD + // We only want one calculateLayout signpost interval per thread. +#ifndef MINIMAL_ASDK + static _Thread_local NSInteger tls_callDepth; +======= +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + as_activity_scope_verbose(as_activity_create("Calculate node layout", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASLayoutLog(), "Calculating layout for %@ sizeRange %@", self, NSStringFromASSizeRange(constrainedSize)); + +#if AS_KDEBUG_ENABLE + // We only want one calculateLayout signpost interval per thread. + // Currently there is no fallback for profiling i386, since it's not useful. + static _Thread_local NSInteger tls_callDepth; + if (tls_callDepth++ == 0) { + ASSignpostStart(ASSignpostCalculateLayout); + } +#endif + + ASSizeRange styleAndParentSize = ASLayoutElementSizeResolve(self.style.size, parentSize); + const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, styleAndParentSize); + ASLayout *result = [self calculateLayoutThatFits:resolvedRange]; +#ifndef MINIMAL_ASDK + as_log_verbose(ASLayoutLog(), "Calculated layout %@", result); + +#if AS_KDEBUG_ENABLE + if (--tls_callDepth == 0) { + ASSignpostEnd(ASSignpostCalculateLayout); + } +#endif +<<<<<<< HEAD +======= + +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + return result; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDN::MutexLocker l(__instanceLock__); + +#if YOGA + // There are several cases where Yoga could arrive here: + // - This node is not in a Yoga tree: it has neither a yogaParent nor yogaChildren. + // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. + // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. + // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); + if (usesYoga) { + // This node has some connection to a Yoga tree. + if ([self shouldHaveYogaMeasureFunc] == NO) { + // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then + // initiate a new Yoga calculation pass from root. + ASDN::MutexUnlocker ul(__instanceLock__); + as_activity_create_for_scope("Yoga layout calculation"); + if (self.yogaLayoutInProgress == NO) { + ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize)); + [self calculateLayoutFromYogaRoot:constrainedSize]; + } else { + ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); + } + ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; + } else { + // If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode). + ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); + } + } +#endif /* YOGA */ + + // Manual size calculation via calculateSizeThatFits: + if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) { + CGSize size = [self calculateSizeThatFits:constrainedSize.max]; + ASDisplayNodeLogEvent(self, @"calculatedSize: %@", NSStringFromCGSize(size)); + return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil]; + } + + // Size calcualtion with layout elements + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + if (measureLayoutSpec) { + _layoutSpecNumberOfPasses++; + } + + // Get layout element from the node + id layoutElement = [self _locked_layoutElementThatFits:constrainedSize]; +#if ASEnableVerboseLogging + for (NSString *asciiLine in [[layoutElement asciiArtString] componentsSeparatedByString:@"\n"]) { + as_log_verbose(ASLayoutLog(), "%@", asciiLine); + } +#endif + + + // Certain properties are necessary to set on an element of type ASLayoutSpec + if (layoutElement.layoutElementType == ASLayoutElementTypeLayoutSpec) { + ASLayoutSpec *layoutSpec = (ASLayoutSpec *)layoutElement; + +#if AS_DEDUPE_LAYOUT_SPEC_TREE + NSHashTable *duplicateElements = [layoutSpec findDuplicatedElementsInSubtree]; + if (duplicateElements.count > 0) { + ASDisplayNodeFailAssert(@"Node %@ returned a layout spec that contains the same elements in multiple positions. Elements: %@", self, duplicateElements); + // Use an empty layout spec to avoid crashes + layoutSpec = [[ASLayoutSpec alloc] init]; + } +#endif + + ASDisplayNodeAssert(layoutSpec.isMutable, @"Node %@ returned layout spec %@ that has already been used. Layout specs should always be regenerated.", self, layoutSpec); + + layoutSpec.isMutable = NO; + } + + // Manually propagate the trait collection here so that any layoutSpec children of layoutSpec will get a traitCollection + { + ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + ASTraitCollectionPropagateDown(layoutElement, self.primitiveTraitCollection); + } + + BOOL measureLayoutComputation = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation; + if (measureLayoutComputation) { + _layoutComputationNumberOfPasses++; + } + + // Layout element layout creation + ASLayout *layout = ({ + ASDN::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation); + [layoutElement layoutThatFits:constrainedSize]; + }); + ASDisplayNodeAssertNotNil(layout, @"[ASLayoutElement layoutThatFits:] should never return nil! %@, %@", self, layout); + + // Make sure layoutElementObject of the root layout is `self`, so that the flattened layout will be structurally correct. + BOOL isFinalLayoutElement = (layout.layoutElement != self); + if (isFinalLayoutElement) { + layout.position = CGPointZero; + layout = [ASLayout layoutWithLayoutElement:self size:layout.size sublayouts:@[layout]]; + } + ASDisplayNodeLogEvent(self, @"computedLayout: %@", layout); + + // PR #1157: Reduces accuracy of _unflattenedLayout for debugging/Weaver + if ([ASDisplayNode shouldStoreUnflattenedLayouts]) { + _unflattenedLayout = layout; + } + layout = [layout filteredNodeLayoutTree]; + + return layout; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeLogEvent(self, @"calculateSizeThatFits: with constrainedSize: %@", NSStringFromCGSize(constrainedSize)); + + return ASIsCGSizeValidForSize(constrainedSize) ? constrainedSize : CGSizeZero; +} + +- (id)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize +{ + ASAssertLocked(__instanceLock__); + __ASDisplayNodeCheckForLayoutMethodOverrides; + + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + + if (_layoutSpecBlock != NULL) { + return ({ + ASDN::MutexLocker l(__instanceLock__); + ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + _layoutSpecBlock(self, constrainedSize); + }); + } else { + return ({ + ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + [self layoutSpecThatFits:constrainedSize]; + }); + } +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeAssert(NO, @"-[ASDisplayNode layoutSpecThatFits:] should never return an empty value. One way this is caused is by calling -[super layoutSpecThatFits:] which is not currently supported."); + return [[ASLayoutSpec alloc] init]; +} + +- (void)layout +{ + // Hook for subclasses + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLayout]; + }]; +} + +#pragma mark Layout Transition + +- (void)_layoutTransitionMeasurementDidFinish +{ + // Hook for subclasses - No-Op in ASDisplayNode +} + +#pragma mark <_ASTransitionContextCompletionDelegate> + +/** + * After completeTransition: is called on the ASContextTransitioning object in animateLayoutTransition: this + * delegate method will be called that start the completion process of the transition + */ +- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete +{ + ASDisplayNodeAssertMainThread(); + + [self didCompleteLayoutTransition:context]; + + _pendingLayoutTransitionContext = nil; + + [self _pendingLayoutTransitionDidComplete]; +} + +- (void)calculatedLayoutDidChange +{ + // Subclass override +} + +#pragma mark - Display + +NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes"; +NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp"; + +- (BOOL)displaysAsynchronously +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_displaysAsynchronously]; +} + +/** + * Core implementation of -displaysAsynchronously. + */ +- (BOOL)_locked_displaysAsynchronously +{ + ASAssertLocked(__instanceLock__); + return checkFlag(Synchronous) == NO && _flags.displaysAsynchronously; +} + +- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDN::MutexLocker l(__instanceLock__); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (checkFlag(Synchronous)) { + return; + } + + if (_flags.displaysAsynchronously == displaysAsynchronously) { + return; + } + + _flags.displaysAsynchronously = displaysAsynchronously; + + self._locked_asyncLayer.displaysAsynchronously = displaysAsynchronously; +} + +- (BOOL)rasterizesSubtree +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.rasterizesSubtree; +} + +- (void)enableSubtreeRasterization +{ + ASDN::MutexLocker l(__instanceLock__); + // Already rasterized from self. + if (_flags.rasterizesSubtree) { + return; + } + + // If rasterized from above, bail. + if (ASHierarchyStateIncludesRasterized(_hierarchyState)) { + ASDisplayNodeFailAssert(@"Subnode of a rasterized node should not have redundant -enableSubtreeRasterization."); + return; + } + + // Ensure not loaded. + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeFailAssert(@"Cannot call %@ on loaded node: %@", NSStringFromSelector(_cmd), self); + return; + } + + // Ensure no loaded subnodes + ASDisplayNode *loadedSubnode = ASDisplayNodeFindFirstSubnode(self, ^BOOL(ASDisplayNode * _Nonnull node) { + return node.nodeLoaded; + }); + if (loadedSubnode != nil) { + ASDisplayNodeFailAssert(@"Cannot call %@ on node %@ with loaded subnode %@", NSStringFromSelector(_cmd), self, loadedSubnode); + return; + } + + _flags.rasterizesSubtree = YES; + + // Tell subnodes that now they're in a rasterized hierarchy (while holding lock!) + for (ASDisplayNode *subnode in _subnodes) { + [subnode enterHierarchyState:ASHierarchyStateRasterized]; + } +} + +- (CGFloat)contentsScaleForDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + + return _contentsScaleForDisplay; +} + +- (void)setContentsScaleForDisplay:(CGFloat)contentsScaleForDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + + if (_contentsScaleForDisplay == contentsScaleForDisplay) { + return; + } + + _contentsScaleForDisplay = contentsScaleForDisplay; +} + +- (void)displayImmediately +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!checkFlag(Synchronous), @"this method is designed for asynchronous mode only"); + + [self.asyncLayer displayImmediately]; +} + +- (void)recursivelyDisplayImmediately +{ + for (ASDisplayNode *child in self.subnodes) { + [child recursivelyDisplayImmediately]; + } + [self displayImmediately]; +} + +- (void)__setNeedsDisplay +{ + BOOL shouldScheduleForDisplay = NO; + { + ASDN::MutexLocker l(__instanceLock__); + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState); + // FIXME: This should not need to recursively display, so create a non-recursive variant. + // The semantics of setNeedsDisplay (as defined by CALayer behavior) are not recursive. + if (_layer != nil && !checkFlag(Synchronous) && nowDisplay && [self _implementsDisplay]) { + shouldScheduleForDisplay = YES; + } + } + + if (shouldScheduleForDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } +} + ++ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node +{ + static dispatch_once_t onceToken; + static ASRunLoopQueue *renderQueue; + dispatch_once(&onceToken, ^{ + renderQueue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() + retainObjects:NO + handler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { + [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; + if (isQueueDrained) { + CFTimeInterval timestamp = CACurrentMediaTime(); + [[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification + object:nil + userInfo:@{ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp: @(timestamp)}]; + } + }]; + }); + + as_log_verbose(ASDisplayLog(), "%s %@", sel_getName(_cmd), node); + [renderQueue enqueue:node]; +} + +/// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + + return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.rasterizesSubtree; +} + +// Track that a node will be displayed as part of the current node hierarchy. +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeWillDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + + // No lock needed as _pendingDisplayNodes is main thread only + if (!_pendingDisplayNodes) { + _pendingDisplayNodes = [[ASWeakSet alloc] init]; + } + + [_pendingDisplayNodes addObject:node]; +} + +// Notify that a node that was pending display finished +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeDidDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + + // No lock for _pendingDisplayNodes needed as it's main thread only + [_pendingDisplayNodes removeObject:node]; + + if (_pendingDisplayNodes.isEmpty) { + + [self hierarchyDisplayDidFinish]; + [self enumerateInterfaceStateDelegates:^(id delegate) { + [delegate hierarchyDisplayDidFinish]; + }]; + + BOOL placeholderShouldPersist = [self placeholderShouldPersist]; + + __instanceLock__.lock(); + if (_placeholderLayer.superlayer && !placeholderShouldPersist) { + void (^cleanupBlock)() = ^{ + [_placeholderLayer removeFromSuperlayer]; + }; + + if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [CATransaction begin]; + [CATransaction setCompletionBlock:cleanupBlock]; + [CATransaction setAnimationDuration:_placeholderFadeDuration]; + _placeholderLayer.opacity = 0.0; + [CATransaction commit]; + } else { + cleanupBlock(); + } + } + __instanceLock__.unlock(); + } +} + +- (void)hierarchyDisplayDidFinish +{ + // Subclass hook +} + +// Helper method to determine if it's safe to call setNeedsDisplay on a layer without throwing away the content. +// For details look at the comment on the canCallSetNeedsDisplayOfLayer flag +- (BOOL)_canCallSetNeedsDisplayOfLayer +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.canCallSetNeedsDisplayOfLayer; +} + +void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) +{ + // This recursion must handle layers in various states: + // 1. Just added to hierarchy, CA hasn't yet called -display + // 2. Previously in a hierarchy (such as a working window owned by an Intelligent Preloading class, like ASTableView / ASCollectionView / ASViewController) + // 3. Has no content to display at all + // Specifically for case 1), we need to explicitly trigger a -display call now. + // Otherwise, there is no opportunity to block the main thread after CoreAnimation's transaction commit + // (even a runloop observer at a late call order will not stop the next frame from compositing, showing placeholders). + + ASDisplayNode *node = [layer asyncdisplaykit_node]; + + if (node.isSynchronous && [node _canCallSetNeedsDisplayOfLayer]) { + // Layers for UIKit components that are wrapped within a node needs to be set to be displayed as the contents of + // the layer get's cleared and would not be recreated otherwise. + // We do not call this for _ASDisplayLayer as an optimization. + [layer setNeedsDisplay]; + } + + if ([node _implementsDisplay]) { + // For layers that do get displayed here, this immediately kicks off the work on the concurrent -[_ASDisplayLayer displayQueue]. + // At the same time, it creates an associated _ASAsyncTransaction, which we can use to block on display completion. See ASDisplayNode+AsyncDisplay.mm. + [layer displayIfNeeded]; + } + + // Kick off the recursion first, so that all necessary display calls are sent and the displayQueue is full of parallelizable work. + // NOTE: The docs report that `sublayers` returns a copy but it actually doesn't. + for (CALayer *sublayer in [layer.sublayers copy]) { + recursivelyTriggerDisplayForLayer(sublayer, shouldBlock); + } + + if (shouldBlock) { + // As the recursion unwinds, verify each transaction is complete and block if it is not. + // While blocking on one transaction, others may be completing concurrently, so it doesn't matter which blocks first. + BOOL waitUntilComplete = (!node.shouldBypassEnsureDisplay); + if (waitUntilComplete) { + for (_ASAsyncTransaction *transaction in [layer.asyncdisplaykit_asyncLayerTransactions copy]) { + // Even if none of the layers have had a chance to start display earlier, they will still be allowed to saturate a multicore CPU while blocking main. + // This significantly reduces time on the main thread relative to UIKit. + [transaction waitUntilComplete]; + } + } + } +} + +- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock +{ + ASDisplayNodeAssertMainThread(); + + CALayer *layer = self.layer; + // -layoutIfNeeded is recursive, and even walks up to superlayers to check if they need layout, + // so we should call it outside of starting the recursion below. If our own layer is not marked + // as dirty, we can assume layout has run on this subtree before. + if ([layer needsLayout]) { + [layer layoutIfNeeded]; + } + recursivelyTriggerDisplayForLayer(layer, shouldBlock); +} + +- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously +{ + [self _recursivelyTriggerDisplayAndBlock:synchronously]; +} + +- (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + _flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay; +} + +- (BOOL)shouldBypassEnsureDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.shouldBypassEnsureDisplay; +} + +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale +{ + { + ASDN::MutexLocker l(__instanceLock__); + if (contentsScale == _contentsScaleForDisplay) { + return; + } + + _contentsScaleForDisplay = contentsScale; + } + + [self setNeedsDisplay]; +} + +- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + [node setNeedsDisplayAtScale:contentsScale]; + }); +} + +- (void)_layoutClipCornersIfNeeded +{ + ASDisplayNodeAssertMainThread(); + if (_clipCornerLayers[0] == nil) { + return; + } + + CGSize boundsSize = self.bounds.size; + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + if (_clipCornerLayers[idx]) { + // Note the Core Animation coordinates are reversed for y; 0 is at the bottom. + _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0); + [_layer addSublayer:_clipCornerLayers[idx]]; + } + } +} + +- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor +{ + ASPerformBlockOnMainThread(^{ + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + // Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left. + // anchorPoint is Bottom Left at 0,0 and Top Right at 1,1. + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + + CGSize size = CGSizeMake(radius + 1, radius + 1); + ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + if (isRight == YES) { + CGContextTranslateCTM(ctx, -radius + 1, 0); + } + if (isTop == YES) { + CGContextTranslateCTM(ctx, 0, -radius + 1); + } + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; + [roundedRect setUsesEvenOddFillRule:YES]; + [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; + [backgroundColor setFill]; + [roundedRect fill]; + + // No lock needed, as _clipCornerLayers is only modified on the main thread. + CALayer *clipCornerLayer = _clipCornerLayers[idx]; + clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage); + clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height); + clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + } + [self _layoutClipCornersIfNeeded]; + }); +} + +- (void)_setClipCornerLayersVisible:(BOOL)visible +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + if (visible) { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + if (_clipCornerLayers[idx] == nil) { + static ASDisplayNodeCornerLayerDelegate *clipCornerLayers; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init]; + }); + _clipCornerLayers[idx] = [[CALayer alloc] init]; + _clipCornerLayers[idx].zPosition = 99999; + _clipCornerLayers[idx].delegate = clipCornerLayers; + } + } + [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; + } else { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + [_clipCornerLayers[idx] removeFromSuperlayer]; + _clipCornerLayers[idx] = nil; + } + } + }); +} + +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius +{ + __instanceLock__.lock(); + CGFloat oldCornerRadius = _cornerRadius; + ASCornerRoundingType oldRoundingType = _cornerRoundingType; + + _cornerRadius = newCornerRadius; + _cornerRoundingType = newRoundingType; + __instanceLock__.unlock(); + + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + if (newRoundingType == ASCornerRoundingTypePrecomposited) { + self.layerCornerRadius = 0.0; + if (oldCornerRadius > 0.0) { + [self displayImmediately]; + } else { + [self setNeedsDisplay]; // Async display is OK if we aren't replacing an existing .cornerRadius. + } + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + self.layerCornerRadius = 0.0; + [self _setClipCornerLayersVisible:YES]; + } else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + } + } + else if (oldRoundingType == ASCornerRoundingTypePrecomposited) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + // Corners are already precomposited, but the radius has changed. + // Default to async re-display. The user may force a synchronous display if desired. + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + [self _setClipCornerLayersVisible:YES]; + [self setNeedsDisplay]; + } + } + else if (oldRoundingType == ASCornerRoundingTypeClipping) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self _setClipCornerLayersVisible:NO]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + [self _setClipCornerLayersVisible:NO]; + [self displayImmediately]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + // Clip corners already exist, but the radius has changed. + [self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor]; + } + } + } + }); +} + +- (void)recursivelySetDisplaySuspended:(BOOL)flag +{ + _recursivelySetDisplaySuspended(self, nil, flag); +} + +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or a variant with a condition / test block. +static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, BOOL flag) +{ + // If there is no layer, but node whose its view is loaded, then we can traverse down its layer hierarchy. Otherwise we must stick to the node hierarchy to avoid loading views prematurely. Note that for nodes that haven't loaded their views, they can't possibly have subviews/sublayers, so we don't need to traverse the layer hierarchy for them. + if (!layer && node && node.nodeLoaded) { + layer = node.layer; + } + + // If we don't know the node, but the layer is an async layer, get the node from the layer. + if (!node && layer && [layer isKindOfClass:[_ASDisplayLayer class]]) { + node = layer.asyncdisplaykit_node; + } + + // Set the flag on the node. If this is a pure layer (no node) then this has no effect (plain layers don't support preventing/cancelling display). + node.displaySuspended = flag; + + if (layer && !node.rasterizesSubtree) { + // If there is a layer, recurse down the layer hierarchy to set the flag on descendants. This will cover both layer-based and node-based children. + for (CALayer *sublayer in layer.sublayers) { + _recursivelySetDisplaySuspended(nil, sublayer, flag); + } + } else { + // If there is no layer (view not loaded yet) or this node rasterizes descendants (there won't be a layer tree to traverse), recurse down the subnode hierarchy to set the flag on descendants. This covers only node-based children, but for a node whose view is not loaded it can't possibly have nodeless children. + for (ASDisplayNode *subnode in node.subnodes) { + _recursivelySetDisplaySuspended(subnode, nil, flag); + } + } +} + +- (BOOL)displaySuspended +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.displaySuspended; +} + +- (void)setDisplaySuspended:(BOOL)flag +{ + ASDisplayNodeAssertThreadAffinity(self); + __instanceLock__.lock(); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (checkFlag(Synchronous) || _flags.displaySuspended == flag) { + __instanceLock__.unlock(); + return; + } + + _flags.displaySuspended = flag; + + self._locked_asyncLayer.displaySuspended = flag; + + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + if ([self _implementsDisplay]) { + // Display start and finish methods needs to happen on the main thread + ASPerformBlockOnMainThread(^{ + if (flag) { + [supernode subnodeDisplayDidFinish:self]; + } else { + [supernode subnodeDisplayWillStart:self]; + } + }); + } +} + +#pragma mark <_ASDisplayLayerDelegate> + +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer asynchronously:(BOOL)asynchronously +{ + // Subclass hook. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self displayWillStart]; +#pragma clang diagnostic pop + + [self displayWillStartAsynchronously:asynchronously]; +} + +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + // Subclass hook. + [self displayDidFinish]; +} + +- (void)displayWillStart {} +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNodeLogEvent(self, @"displayWillStart"); + // in case current node takes longer to display than it's subnodes, treat it as a dependent node + [self _pendingNodeWillDisplay:self]; + + __instanceLock__.lock(); + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + [supernode subnodeDisplayWillStart:self]; +} + +- (void)displayDidFinish +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNodeLogEvent(self, @"displayDidFinish"); + [self _pendingNodeDidDisplay:self]; + + __instanceLock__.lock(); + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + [supernode subnodeDisplayDidFinish:self]; +} + +- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode +{ + // Subclass hook + [self _pendingNodeWillDisplay:subnode]; +} + +- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode +{ + // Subclass hook + [self _pendingNodeDidDisplay:subnode]; +} + +#pragma mark + +// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + if (event == kCAOnOrderIn) { + [self __enterHierarchy]; + } else if (event == kCAOnOrderOut) { + [self __exitHierarchy]; + } + + ASDisplayNodeAssert(_flags.layerBacked, @"We shouldn't get called back here unless we are layer-backed."); + return (id)kCFNull; +} + +#pragma mark - Error Handling + ++ (void)setNonFatalErrorBlock:(ASDisplayNodeNonFatalErrorBlock)nonFatalErrorBlock +{ + if (_nonFatalErrorBlock != nonFatalErrorBlock) { + _nonFatalErrorBlock = [nonFatalErrorBlock copy]; + } +} + ++ (ASDisplayNodeNonFatalErrorBlock)nonFatalErrorBlock +{ + return _nonFatalErrorBlock; +} + +#pragma mark - Converting to and from the Node's Coordinate System + +- (CATransform3D)_transformToAncestor:(ASDisplayNode *)ancestor +{ + CATransform3D transform = CATransform3DIdentity; + ASDisplayNode *currentNode = self; + while (currentNode.supernode) { + if (currentNode == ancestor) { + return transform; + } + + CGPoint anchorPoint = currentNode.anchorPoint; + CGRect bounds = currentNode.bounds; + CGPoint position = currentNode.position; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + + transform = CATransform3DTranslate(transform, origin.x, origin.y, 0); + transform = CATransform3DTranslate(transform, -bounds.origin.x, -bounds.origin.y, 0); + currentNode = currentNode.supernode; + } + return transform; +} + +static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNode *referenceNode, ASDisplayNode *targetNode) +{ + ASDisplayNode *ancestor = ASDisplayNodeFindClosestCommonAncestor(referenceNode, targetNode); + + // Transform into global (away from reference coordinate space) + CATransform3D transformToGlobal = [referenceNode _transformToAncestor:ancestor]; + + // Transform into local (via inverse transform from target to ancestor) + CATransform3D transformToLocal = CATransform3DInvert([targetNode _transformToAncestor:ancestor]); + + return CATransform3DConcat(transformToGlobal, transformToLocal); +} + +- (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + /** + * When passed node=nil, all methods in this family use the UIView-style + * behavior – that is, convert from/to window coordinates if there's a window, + * otherwise return the point untransformed. + */ + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertPoint:point fromLayer:window.layer]; + } else { + return point; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertPoint:point toLayer:window.layer]; + } else { + return point; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertRect:rect fromLayer:window.layer]; + } else { + return rect; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertRect:rect toLayer:window.layer]; + } else { + return rect; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +#pragma mark - Managing the Node Hierarchy + +ASDISPLAYNODE_INLINE bool shouldDisableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASDisplayNode *to) { + if (!from || !to) return NO; + if (from.isSynchronous) return NO; + if (to.isSynchronous) return NO; + if (from.isInHierarchy != to.isInHierarchy) return NO; + return YES; +} + +/// Returns incremented value of i if i is not NSNotFound +ASDISPLAYNODE_INLINE NSInteger incrementIfFound(NSInteger i) { + return i == NSNotFound ? NSNotFound : i + 1; +} + +/// Returns if a node is a member of a rasterized tree +ASDISPLAYNODE_INLINE BOOL canUseViewAPI(ASDisplayNode *node, ASDisplayNode *subnode) { + return (subnode.isLayerBacked == NO && node.isLayerBacked == NO); +} + +/// Returns if node is a member of a rasterized tree +ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { + return (node.rasterizesSubtree || (node.hierarchyState & ASHierarchyStateRasterized)); +} + +// NOTE: This method must be dealloc-safe (should not retain self). +- (ASDisplayNode *)supernode +{ +#if CHECK_LOCKING_SAFETY + if (__instanceLock__.locked()) { + NSLog(@"WARNING: Accessing supernode while holding recursive instance lock of this node is worrisome. It's likely that you will soon try to acquire the supernode's lock, and this can easily cause deadlocks."); + } +#endif + + ASDN::MutexLocker l(__instanceLock__); + return _supernode; +} + +- (void)_setSupernode:(ASDisplayNode *)newSupernode +{ + BOOL supernodeDidChange = NO; + ASDisplayNode *oldSupernode = nil; + { + ASDN::MutexLocker l(__instanceLock__); + if (_supernode != newSupernode) { + oldSupernode = _supernode; // Access supernode properties outside of lock to avoid remote chance of deadlock, + // in case supernode implementation must access one of our properties. + _supernode = newSupernode; + supernodeDidChange = YES; + } + } + + if (supernodeDidChange) { + ASDisplayNodeLogEvent(self, @"supernodeDidChange: %@, oldValue = %@", ASObjectDescriptionMakeTiny(newSupernode), ASObjectDescriptionMakeTiny(oldSupernode)); + // Hierarchy state + ASHierarchyState stateToEnterOrExit = (newSupernode ? newSupernode.hierarchyState + : oldSupernode.hierarchyState); + + // Rasterized state + BOOL parentWasOrIsRasterized = (newSupernode ? newSupernode.rasterizesSubtree + : oldSupernode.rasterizesSubtree); + if (parentWasOrIsRasterized) { + stateToEnterOrExit |= ASHierarchyStateRasterized; + } + if (newSupernode) { + [self enterHierarchyState:stateToEnterOrExit]; + + // If a node was added to a supernode, the supernode could be in a layout pending state. All of the hierarchy state + // properties related to the transition need to be copied over as well as propagated down the subtree. + // This is especially important as with automatic subnode management, adding subnodes can happen while a transition + // is in fly + if (ASHierarchyStateIncludesLayoutPending(stateToEnterOrExit)) { + int32_t pendingTransitionId = newSupernode->_pendingTransitionID; + if (pendingTransitionId != ASLayoutElementContextInvalidTransitionID) { + { + _pendingTransitionID = pendingTransitionId; + + // Propagate down the new pending transition id + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + node->_pendingTransitionID = pendingTransitionId; + }); + } + } + } + + // Now that we have a supernode, propagate its traits to self. + ASTraitCollectionPropagateDown(self, newSupernode.primitiveTraitCollection); + + } else { + // If a node will be removed from the supernode it should go out from the layout pending state to remove all + // layout pending state related properties on the node + stateToEnterOrExit |= ASHierarchyStateLayoutPending; + + [self exitHierarchyState:stateToEnterOrExit]; + + // We only need to explicitly exit hierarchy here if we were rasterized. + // Otherwise we will exit the hierarchy when our view/layer does so + // which has some nice carry-over machinery to handle cases where we are removed from a hierarchy + // and then added into it again shortly after. + __instanceLock__.lock(); + BOOL isInHierarchy = _flags.isInHierarchy; + __instanceLock__.unlock(); + + if (parentWasOrIsRasterized && isInHierarchy) { + [self __exitHierarchy]; + } + } + } +} + +- (NSArray *)subnodes +{ + ASDN::MutexLocker l(__instanceLock__); + if (_cachedSubnodes == nil) { + _cachedSubnodes = [_subnodes copy]; + } else { + ASDisplayNodeAssert(ASObjectIsEqual(_cachedSubnodes, _subnodes), @"Expected _subnodes and _cachedSubnodes to have the same contents."); + } + return _cachedSubnodes ?: @[]; +} + +/* + * Central private helper method that should eventually be called if submethods add, insert or replace subnodes + * This method is called with thread affinity and without lock held. + * + * @param subnode The subnode to insert + * @param subnodeIndex The index in _subnodes to insert it + * @param viewSublayerIndex The index in layer.sublayers (not view.subviews) at which to insert the view (use if we can use the view API) otherwise pass NSNotFound + * @param sublayerIndex The index in layer.sublayers at which to insert the layer (use if either parent or subnode is layer-backed) otherwise pass NSNotFound + * @param oldSubnode Remove this subnode before inserting; ok to be nil if no removal is desired + */ +- (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnodeIndex sublayerIndex:(NSInteger)sublayerIndex andRemoveSubnode:(ASDisplayNode *)oldSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + as_log_verbose(ASNodeLog(), "Insert subnode %@ at index %zd of %@ and remove subnode %@", subnode, subnodeIndex, self, oldSubnode); + + if (subnode == nil || subnode == self) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode or self as subnode"); + return; + } + + if (subnodeIndex == NSNotFound) { + ASDisplayNodeFailAssert(@"Try to insert node on an index that was not found"); + return; + } + + if (self.layerBacked && !subnode.layerBacked) { + ASDisplayNodeFailAssert(@"Cannot add a view-backed node as a subnode of a layer-backed node. Supernode: %@, subnode: %@", self, subnode); + return; + } + + BOOL isRasterized = subtreeIsRasterized(self); + if (isRasterized && subnode.nodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot add loaded node %@ to rasterized subtree of node %@", ASObjectDescriptionMakeTiny(subnode), ASObjectDescriptionMakeTiny(self)); + return; + } + + __instanceLock__.lock(); + NSUInteger subnodesCount = _subnodes.count; + __instanceLock__.unlock(); + if (subnodeIndex > subnodesCount || subnodeIndex < 0) { + ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)subnodeIndex, (long)subnodesCount); + return; + } + + // Disable appearance methods during move between supernodes, but make sure we restore their state after we do our thing + ASDisplayNode *oldParent = subnode.supernode; + BOOL disableNotifications = shouldDisableNotificationsForMovingBetweenParents(oldParent, self); + if (disableNotifications) { + [subnode __incrementVisibilityNotificationsDisabled]; + } + + [subnode _removeFromSupernode]; + [oldSubnode _removeFromSupernode]; + + __instanceLock__.lock(); + if (_subnodes == nil) { + _subnodes = [[NSMutableArray alloc] init]; + } + [_subnodes insertObject:subnode atIndex:subnodeIndex]; + _cachedSubnodes = nil; + __instanceLock__.unlock(); + + // This call will apply our .hierarchyState to the new subnode. + // If we are a managed hierarchy, as in ASCellNode trees, it will also apply our .interfaceState. + [subnode _setSupernode:self]; + + // If this subnode will be rasterized, enter hierarchy if needed + // TODO: Move this into _setSupernode: ? + if (isRasterized) { + if (self.inHierarchy) { + [subnode __enterHierarchy]; + } + } else if (self.nodeLoaded) { + // If not rasterizing, and node is loaded insert the subview/sublayer now. + [self _insertSubnodeSubviewOrSublayer:subnode atIndex:sublayerIndex]; + } // Otherwise we will insert subview/sublayer when we get loaded + + ASDisplayNodeAssert(disableNotifications == shouldDisableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated"); + if (disableNotifications) { + [subnode __decrementVisibilityNotificationsDisabled]; + } +} + +/* + * Inserts the view or layer of the given node at the given index + * + * @param subnode The subnode to insert + * @param idx The index in _view.subviews or _layer.sublayers at which to insert the subnode.view or + * subnode.layer of the subnode + */ +- (void)_insertSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(self.nodeLoaded, @"_insertSubnodeSubviewOrSublayer:atIndex: should never be called before our own view is created"); + + ASDisplayNodeAssert(idx != NSNotFound, @"Try to insert node on an index that was not found"); + if (idx == NSNotFound) { + return; + } + + // Because the view and layer can only be created and destroyed on Main, that is also the only thread + // where the view and layer can change. We can avoid locking. + + // If we can use view API, do. Due to an apple bug, -insertSubview:atIndex: actually wants a LAYER index, + // which we pass in. + if (canUseViewAPI(self, subnode)) { + [_view insertSubview:subnode.view atIndex:idx]; + } else { + [_layer insertSublayer:subnode.layer atIndex:(unsigned int)idx]; + } +} + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeLogEvent(self, @"addSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _addSubnode:subnode]; +} + +- (void)_addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDisplayNodeAssert(subnode, @"Cannot insert a nil subnode"); + + // Don't add if it's already a subnode + ASDisplayNode *oldParent = subnode.supernode; + if (!subnode || subnode == self || oldParent == self) { + return; + } + + NSUInteger subnodesIndex; + NSUInteger sublayersIndex; + { + ASDN::MutexLocker l(__instanceLock__); + subnodesIndex = _subnodes.count; + sublayersIndex = _layer.sublayers.count; + } + + [self _insertSubnode:subnode atSubnodeIndex:subnodesIndex sublayerIndex:sublayersIndex andRemoveSubnode:nil]; +} + +- (void)_addSubnodeViewsAndLayers +{ + ASDisplayNodeAssertMainThread(); + + TIME_SCOPED(_debugTimeToAddSubnodeViews); + + for (ASDisplayNode *node in self.subnodes) { + [self _addSubnodeSubviewOrSublayer:node]; + } +} + +- (void)_addSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertMainThread(); + + // Due to a bug in Apple's framework we have to use the layer index to insert a subview + // so just use the count of the sublayers to add the subnode + NSInteger idx = _layer.sublayers.count; // No locking is needed as it's main thread only + [self _insertSubnodeSubviewOrSublayer:subnode atIndex:idx]; +} + +- (void)replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeLogEvent(self, @"replaceSubnode: %@ withSubnode: %@ with automaticallyManagesSubnodes: %@", + oldSubnode, replacementSubnode, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _replaceSubnode:oldSubnode withSubnode:replacementSubnode]; +} + +- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (replacementSubnode == nil) { + ASDisplayNodeFailAssert(@"Invalid subnode to replace"); + return; + } + + if (oldSubnode.supernode != self) { + ASDisplayNodeFailAssert(@"Old Subnode to replace must be a subnode"); + return; + } + + ASDisplayNodeAssert(!(self.nodeLoaded && !oldSubnode.nodeLoaded), @"We have view loaded, but child node does not."); + + NSInteger subnodeIndex; + NSInteger sublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + subnodeIndex = [_subnodes indexOfObjectIdenticalTo:oldSubnode]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + sublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:oldSubnode.layer]; + ASDisplayNodeAssert(sublayerIndex != NSNotFound, @"Somehow oldSubnode's supernode is self, yet we could not find it in our layers to replace"); + if (sublayerIndex == NSNotFound) { + return; + } + } + } + } + + [self _insertSubnode:replacementSubnode atSubnodeIndex:subnodeIndex sublayerIndex:sublayerIndex andRemoveSubnode:oldSubnode]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ belowSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, below, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode belowSubnode:below]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + if (below.supernode != self) { + ASDisplayNodeFailAssert(@"Node to insert below must be a subnode"); + return; + } + + NSInteger belowSubnodeIndex; + NSInteger belowSublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + belowSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:below]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + belowSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:below.layer]; + ASDisplayNodeAssert(belowSublayerIndex != NSNotFound, @"Somehow below's supernode is self, yet we could not find it in our layers to reference"); + if (belowSublayerIndex == NSNotFound) + return; + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to + // insert it will mess up our calculation + if (subnode.supernode == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes < belowSubnodeIndex) { + belowSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers < belowSublayerIndex) { + belowSublayerIndex--; + } + } + } + } + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find below in subnodes"); + + [self _insertSubnode:subnode atSubnodeIndex:belowSubnodeIndex sublayerIndex:belowSublayerIndex andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ abodeSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, above, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode aboveSubnode:above]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + if (above.supernode != self) { + ASDisplayNodeFailAssert(@"Node to insert above must be a subnode"); + return; + } + + NSInteger aboveSubnodeIndex; + NSInteger aboveSublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + aboveSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:above]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + aboveSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:above.layer]; + ASDisplayNodeAssert(aboveSublayerIndex != NSNotFound, @"Somehow above's supernode is self, yet we could not find it in our layers to replace"); + if (aboveSublayerIndex == NSNotFound) + return; + } + + ASDisplayNodeAssert(aboveSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to + // insert it will mess up our calculation + if (subnode.supernode == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes <= aboveSubnodeIndex) { + aboveSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers <= aboveSublayerIndex) { + aboveSublayerIndex--; + } + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:incrementIfFound(aboveSubnodeIndex) sublayerIndex:incrementIfFound(aboveSublayerIndex) andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ atIndex: %td with automaticallyManagesSubnodes: %@", + subnode, idx, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode atIndex:idx]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + NSInteger sublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + + if (idx > _subnodes.count || idx < 0) { + ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)idx, (long)_subnodes.count); + return; + } + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + // Account for potentially having other subviews + if (_layer && idx == 0) { + sublayerIndex = 0; + } else if (_layer) { + ASDisplayNode *positionInRelationTo = (_subnodes.count > 0 && idx > 0) ? _subnodes[idx - 1] : nil; + if (positionInRelationTo) { + sublayerIndex = incrementIfFound([_layer.sublayers indexOfObjectIdenticalTo:positionInRelationTo.layer]); + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:idx sublayerIndex:sublayerIndex andRemoveSubnode:nil]; +} + +- (void)_removeSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + // Don't call self.supernode here because that will retain/autorelease the supernode. This method -_removeSupernode: is often called while tearing down a node hierarchy, and the supernode in question might be in the middle of its -dealloc. The supernode is never messaged, only compared by value, so this is safe. + // The particular issue that triggers this edge case is when a node calls -removeFromSupernode on a subnode from within its own -dealloc method. + if (!subnode || subnode.supernode != self) { + return; + } + + __instanceLock__.lock(); + [_subnodes removeObjectIdenticalTo:subnode]; + _cachedSubnodes = nil; + __instanceLock__.unlock(); + + [subnode _setSupernode:nil]; +} + +- (void)removeFromSupernode +{ + ASDisplayNodeLogEvent(self, @"removeFromSupernode with automaticallyManagesSubnodes: %@", + self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _removeFromSupernode]; +} + +- (void)_removeFromSupernode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + __instanceLock__.lock(); + __weak ASDisplayNode *supernode = _supernode; + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + __instanceLock__.unlock(); + + [self _removeFromSupernode:supernode view:view layer:layer]; +} + +- (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + __instanceLock__.lock(); + + // Only remove if supernode is still the expected supernode + if (!ASObjectIsEqual(_supernode, supernode)) { + __instanceLock__.unlock(); + return; + } + + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + __instanceLock__.unlock(); + + [self _removeFromSupernode:supernode view:view layer:layer]; +} + +- (void)_removeFromSupernode:(ASDisplayNode *)supernode view:(UIView *)view layer:(CALayer *)layer +{ + // Note: we continue even if supernode is nil to ensure view/layer are removed from hierarchy. + + if (supernode != nil) { + as_log_verbose(ASNodeLog(), "Remove %@ from supernode %@", self, supernode); + } + + // Clear supernode's reference to us before removing the view from the hierarchy, as _ASDisplayView + // will trigger us to clear our _supernode pointer in willMoveToSuperview:nil. + // This may result in removing the last strong reference, triggering deallocation after this method. + [supernode _removeSubnode:self]; + + if (view != nil) { + [view removeFromSuperview]; + } else if (layer != nil) { + [layer removeFromSuperlayer]; + } +} + +#pragma mark - Visibility API + +- (BOOL)__visibilityNotificationsDisabled +{ + // Currently, this method is only used by the testing infrastructure to verify this internal feature. + ASDN::MutexLocker l(__instanceLock__); + return _flags.visibilityNotificationsDisabled > 0; +} + +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled +{ + ASDN::MutexLocker l(__instanceLock__); + return (_hierarchyState & ASHierarchyStateTransitioningSupernodes); +} + +- (void)__incrementVisibilityNotificationsDisabled +{ + __instanceLock__.lock(); + const size_t maxVisibilityIncrement = (1ULL< 0, @"Can't decrement past 0"); + if (_flags.visibilityNotificationsDisabled > 0) { + _flags.visibilityNotificationsDisabled--; + } + BOOL visibilityNotificationsDisabled = (_flags.visibilityNotificationsDisabled == 0); + __instanceLock__.unlock(); + + if (visibilityNotificationsDisabled) { + // Must have just transitioned from 1 to 0. Notify all subnodes that we are no longer in a disabled state. + // FIXME: This system should be revisited when refactoring and consolidating the implementation of the + // addSubnode: and insertSubnode:... methods. As implemented, though logically irrelevant for expected use cases, + // multiple nodes in the subtree below may have a non-zero visibilityNotification count and still have + // the ASHierarchyState bit cleared (the only value checked when reading this state). + [self exitHierarchyState:ASHierarchyStateTransitioningSupernodes]; + } +} + +#pragma mark - Placeholder + +- (void)_locked_layoutPlaceholderIfNecessary +{ + ASAssertLocked(__instanceLock__); + if ([self _locked_shouldHavePlaceholderLayer]) { + [self _locked_setupPlaceholderLayerIfNeeded]; + } + // Update the placeholderLayer size in case the node size has changed since the placeholder was added. + _placeholderLayer.frame = self.threadSafeBounds; +} + +- (BOOL)_locked_shouldHavePlaceholderLayer +{ + ASAssertLocked(__instanceLock__); + return (_placeholderEnabled && [self _implementsDisplay]); +} + +- (void)_locked_setupPlaceholderLayerIfNeeded +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + + if (!_placeholderLayer) { + _placeholderLayer = [CALayer layer]; + // do not set to CGFLOAT_MAX in the case that something needs to be overtop the placeholder + _placeholderLayer.zPosition = 9999.0; + } + + if (_placeholderLayer.contents == nil) { + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + if (_placeholderImage) { + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(_placeholderImage.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetResizableContents(_placeholderLayer, _placeholderImage); + } else { + _placeholderLayer.contentsScale = self.contentsScale; + _placeholderLayer.contents = (id)_placeholderImage.CGImage; + } + } + } +} + +- (UIImage *)placeholderImage +{ + // Subclass hook + return nil; +} + +- (BOOL)placeholderShouldPersist +{ + // Subclass hook + return NO; +} + +#pragma mark - Hierarchy State + +- (BOOL)isInHierarchy +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.isInHierarchy; +} + +- (void)__enterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy"); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"enterHierarchy"); + + // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. + __instanceLock__.lock(); + + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isEnteringHierarchy = YES; + _flags.isInHierarchy = YES; + + // Don't call -willEnterHierarchy while holding __instanceLock__. + // This method and subsequent ones (i.e -interfaceState and didEnter(.*)State) + // don't expect that they are called while the lock is being held. + // More importantly, didEnter(.*)State methods are meant to be overriden by clients. + // And so they can potentially walk up the node tree and cause deadlocks, or do expensive tasks and cause the lock to be held for too long. + __instanceLock__.unlock(); + [self willEnterHierarchy]; + for (ASDisplayNode *subnode in self.subnodes) { + [subnode __enterHierarchy]; + } + __instanceLock__.lock(); + + _flags.isEnteringHierarchy = NO; + + // If we don't have contents finished drawing by the time we are on screen, immediately add the placeholder (if it is enabled and we do have something to draw). + if (self.contents == nil && [self _implementsDisplay]) { + CALayer *layer = self.layer; + [layer setNeedsDisplay]; + + if ([self _locked_shouldHavePlaceholderLayer]) { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self _locked_setupPlaceholderLayerIfNeeded]; + _placeholderLayer.opacity = 1.0; + [CATransaction commit]; + [layer addSublayer:_placeholderLayer]; + } + } + } + + __instanceLock__.unlock(); + + [self didEnterHierarchy]; +} + +- (void)__exitHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"Should not cause recursive __exitHierarchy"); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"exitHierarchy"); + + // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. + __instanceLock__.lock(); + + if (_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isExitingHierarchy = YES; + _flags.isInHierarchy = NO; + + // Don't call -didExitHierarchy while holding __instanceLock__. + // This method and subsequent ones (i.e -interfaceState and didExit(.*)State) + // don't expect that they are called while the lock is being held. + // More importantly, didExit(.*)State methods are meant to be overriden by clients. + // And so they can potentially walk up the node tree and cause deadlocks, or do expensive tasks and cause the lock to be held for too long. + __instanceLock__.unlock(); + [self didExitHierarchy]; + for (ASDisplayNode *subnode in self.subnodes) { + [subnode __exitHierarchy]; + } + __instanceLock__.lock(); + + _flags.isExitingHierarchy = NO; + } + + __instanceLock__.unlock(); +} + +- (void)enterHierarchyState:(ASHierarchyState)hierarchyState +{ + if (hierarchyState == ASHierarchyStateNormal) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *node) { + node.hierarchyState |= hierarchyState; + }); +} + +- (void)exitHierarchyState:(ASHierarchyState)hierarchyState +{ + if (hierarchyState == ASHierarchyStateNormal) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *node) { + node.hierarchyState &= (~hierarchyState); + }); +} + +- (ASHierarchyState)hierarchyState +{ + ASDN::MutexLocker l(__instanceLock__); + return _hierarchyState; +} + +- (void)setHierarchyState:(ASHierarchyState)newState +{ + ASHierarchyState oldState = ASHierarchyStateNormal; + { + ASDN::MutexLocker l(__instanceLock__); + if (_hierarchyState == newState) { + return; + } + oldState = _hierarchyState; + _hierarchyState = newState; + } + + // Entered rasterization state. + if (newState & ASHierarchyStateRasterized) { + ASDisplayNodeAssert(checkFlag(Synchronous) == NO, @"Node created using -initWithViewBlock:/-initWithLayerBlock: cannot be added to subtree of node with subtree rasterization enabled. Node: %@", self); + } + + // Entered or exited range managed state. + if ((newState & ASHierarchyStateRangeManaged) != (oldState & ASHierarchyStateRangeManaged)) { + if (newState & ASHierarchyStateRangeManaged) { + [self enterInterfaceState:self.supernode.pendingInterfaceState]; + } else { + // The case of exiting a range-managed state should be fairly rare. Adding or removing the node + // to a view hierarchy will cause its interfaceState to be either fully set or unset (all fields), + // but because we might be about to be added to a view hierarchy, exiting the interface state now + // would cause inefficient churn. The tradeoff is that we may not clear contents / fetched data + // for nodes that are removed from a managed state and then retained but not used (bad idea anyway!) + } + } + + if ((newState & ASHierarchyStateLayoutPending) != (oldState & ASHierarchyStateLayoutPending)) { + if (newState & ASHierarchyStateLayoutPending) { + // Entering layout pending state + } else { + // Leaving layout pending state, reset related properties + ASDN::MutexLocker l(__instanceLock__); + _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; + _pendingLayoutTransition = nil; + } + } + + ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState)); + as_log_verbose(ASNodeLog(), "%s%@ %@", sel_getName(_cmd), NSStringFromASHierarchyStateChange(oldState, newState), self); +} + +- (void)willEnterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isEnteringHierarchy, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); + + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateInHierarchy; + } else if (ASCATransactionQueue.sharedQueue.isEnabled) { + __instanceLock__.lock(); + ASInterfaceState state = _preExitingInterfaceState; + _preExitingInterfaceState = ASInterfaceStateNone; + __instanceLock__.unlock(); + // Layer thrash happened, revert to before exiting. + if (state != ASInterfaceStateNone) { + self.interfaceState = state; + } + } +} + +- (void)didEnterHierarchy { + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"You should never call -didEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASDisplayNodeAssert(_flags.isInHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); +} + +- (void)didExitHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isExitingHierarchy, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); + + // This case is important when tearing down hierarchies. We must deliver a visibileStateDidChange:NO callback, as part our API guarantee that this method can be used for + // things like data analytics about user content viewing. We cannot call the method in the dealloc as any incidental retain operations in client code would fail. + // Additionally, it may be that a Standard UIView which is containing us is moving between hierarchies, and we should not send the call if we will be re-added in the + // same runloop. Strategy: strong reference (might be the last!), wait one runloop, and confirm we are still outside the hierarchy (both layer-backed and view-backed). + // TODO: This approach could be optimized by only performing the dispatch for root elements + recursively apply the interface state change. This would require a closer + // integration with _ASDisplayLayer to ensure that the superlayer pointer has been cleared by this stage (to check if we are root or not), or a different delegate call. + +#if !ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateNone; + return; + } +#endif + if (ASInterfaceStateIncludesVisible(self.pendingInterfaceState)) { + void(^exitVisibleInterfaceState)(void) = ^{ + // This block intentionally retains self. + __instanceLock__.lock(); + unsigned isStillInHierarchy = _flags.isInHierarchy; + BOOL isVisible = ASInterfaceStateIncludesVisible(_pendingInterfaceState); + ASInterfaceState newState = (_pendingInterfaceState & ~ASInterfaceStateVisible); + // layer may be thrashed, we need to remember the state so we can reset if it enters in same runloop later. + _preExitingInterfaceState = _pendingInterfaceState; + __instanceLock__.unlock(); + if (!isStillInHierarchy && isVisible) { +#if ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + newState = ASInterfaceStateNone; + } +#endif + self.interfaceState = newState; + } + }; + + if (!ASCATransactionQueue.sharedQueue.enabled) { + dispatch_async(dispatch_get_main_queue(), exitVisibleInterfaceState); + } else { + exitVisibleInterfaceState(); + } + } +} + +#pragma mark - Interface State + +/** + * We currently only set interface state on nodes in table/collection views. For other nodes, if they are + * in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`. + */ +- (BOOL)supportsRangeManagedInterfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return ASHierarchyStateIncludesRangeManaged(_hierarchyState); +} + +- (void)enterInterfaceState:(ASInterfaceState)interfaceState +{ + if (interfaceState == ASInterfaceStateNone) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState |= interfaceState; + }); +} + +- (void)exitInterfaceState:(ASInterfaceState)interfaceState +{ + if (interfaceState == ASInterfaceStateNone) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodeLogEvent(self, @"%s %@", sel_getName(_cmd), NSStringFromASInterfaceState(interfaceState)); + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState &= (~interfaceState); + }); +} + +- (void)recursivelySetInterfaceState:(ASInterfaceState)newInterfaceState +{ + as_activity_create_for_scope("Recursively set interface state"); + + // Instead of each node in the recursion assuming it needs to schedule itself for display, + // setInterfaceState: skips this when handling range-managed nodes (our whole subtree has this set). + // If our range manager intends for us to be displayed right now, and didn't before, get started! + BOOL shouldScheduleDisplay = [self supportsRangeManagedInterfaceState] && [self shouldScheduleDisplayWithNewInterfaceState:newInterfaceState]; + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState = newInterfaceState; + }); + if (shouldScheduleDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } +} + +- (ASInterfaceState)interfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return _interfaceState; +} + +- (void)setInterfaceState:(ASInterfaceState)newState +{ + if (!ASCATransactionQueue.sharedQueue.enabled) { + [self applyPendingInterfaceState:newState]; + } else { + ASDN::MutexLocker l(__instanceLock__); + if (_pendingInterfaceState != newState) { + _pendingInterfaceState = newState; + [[ASCATransactionQueue sharedQueue] enqueue:self]; + } + } +} + +- (ASInterfaceState)pendingInterfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return _pendingInterfaceState; +} + +- (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState +{ + //This method is currently called on the main thread. The assert has been added here because all of the + //did(Enter|Exit)(Display|Visible|Preload)State methods currently guarantee calling on main. + ASDisplayNodeAssertMainThread(); + + // This method manages __instanceLock__ itself, to ensure the lock is not held while didEnter/Exit(.*)State methods are called, thus avoid potential deadlocks + ASAssertUnlocked(__instanceLock__); + + ASInterfaceState oldState = ASInterfaceStateNone; + ASInterfaceState newState = ASInterfaceStateNone; + { + ASDN::MutexLocker l(__instanceLock__); + // newPendingState will not be used when ASCATransactionQueue is enabled + // and use _pendingInterfaceState instead for interfaceState update. + if (!ASCATransactionQueue.sharedQueue.enabled) { + _pendingInterfaceState = newPendingState; + } + oldState = _interfaceState; + newState = _pendingInterfaceState; + if (newState == oldState) { + return; + } + _interfaceState = newState; + _preExitingInterfaceState = ASInterfaceStateNone; + } + + // It should never be possible for a node to be visible but not be allowed / expected to display. + ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); + + // TODO: Trigger asynchronous measurement if it is not already cached or being calculated. + // if ((newState & ASInterfaceStateMeasureLayout) != (oldState & ASInterfaceStateMeasureLayout)) { + // } + + // For the Preload and Display ranges, we don't want to call -clear* if not being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + // Still, the interfaceState should be updated to the current state of the node; just don't act on the transition. + + // Entered or exited data loading state. + BOOL nowPreload = ASInterfaceStateIncludesPreload(newState); + BOOL wasPreload = ASInterfaceStateIncludesPreload(oldState); + + if (nowPreload != wasPreload) { + if (nowPreload) { + [self didEnterPreloadState]; + } else { + // We don't want to call -didExitPreloadState on nodes that aren't being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + if ([self supportsRangeManagedInterfaceState]) { + [self didExitPreloadState]; + } + } + } + + // Entered or exited contents rendering state. + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(newState); + BOOL wasDisplay = ASInterfaceStateIncludesDisplay(oldState); + + if (nowDisplay != wasDisplay) { + if ([self supportsRangeManagedInterfaceState]) { + if (nowDisplay) { + // Once the working window is eliminated (ASRangeHandlerRender), trigger display directly here. + [self setDisplaySuspended:NO]; + } else { + [self setDisplaySuspended:YES]; + //schedule clear contents on next runloop + dispatch_async(dispatch_get_main_queue(), ^{ + __instanceLock__.lock(); + ASInterfaceState interfaceState = _interfaceState; + __instanceLock__.unlock(); + if (ASInterfaceStateIncludesDisplay(interfaceState) == NO) { + [self clearContents]; + } + }); + } + } else { + // NOTE: This case isn't currently supported as setInterfaceState: isn't exposed externally, and all + // internal use cases are range-managed. When a node is visible, don't mess with display - CA will start it. + if (!ASInterfaceStateIncludesVisible(newState)) { + // Check _implementsDisplay purely for efficiency - it's faster even than calling -asyncLayer. + if ([self _implementsDisplay]) { + if (nowDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } else { + [[self asyncLayer] cancelAsyncDisplay]; + //schedule clear contents on next runloop + dispatch_async(dispatch_get_main_queue(), ^{ + __instanceLock__.lock(); + ASInterfaceState interfaceState = _interfaceState; + __instanceLock__.unlock(); + if (ASInterfaceStateIncludesDisplay(interfaceState) == NO) { + [self clearContents]; + } + }); + } + } + } + } + + if (nowDisplay) { + [self didEnterDisplayState]; + } else { + [self didExitDisplayState]; + } + } + + // Became visible or invisible. When range-managed, this represents literal visibility - at least one pixel + // is onscreen. If not range-managed, we can't guarantee more than the node being present in an onscreen window. + BOOL nowVisible = ASInterfaceStateIncludesVisible(newState); + BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState); + + if (nowVisible != wasVisible) { + if (nowVisible) { + [self didEnterVisibleState]; + } else { + [self didExitVisibleState]; + } + } + + // Log this change, unless it's just the node going from {} -> {Measure} because that change happens + // for all cell nodes and it isn't currently meaningful. + BOOL measureChangeOnly = ((oldState | newState) == ASInterfaceStateMeasureLayout); + if (!measureChangeOnly) { + as_log_verbose(ASNodeLog(), "%s %@ %@", sel_getName(_cmd), NSStringFromASInterfaceStateChange(oldState, newState), self); + } + + ASDisplayNodeLogEvent(self, @"interfaceStateDidChange: %@", NSStringFromASInterfaceStateChange(oldState, newState)); + [self interfaceStateDidChange:newState fromState:oldState]; +} + +- (void)prepareForCATransactionCommit +{ + // Apply _pendingInterfaceState actual _interfaceState, note that ASInterfaceStateNone is not used. + [self applyPendingInterfaceState:ASInterfaceStateNone]; +} + +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + // Subclass hook + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertMainThread(); + [self enumerateInterfaceStateDelegates:^(id del) { + [del interfaceStateDidChange:newState fromState:oldState]; + }]; +} + +- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState +{ + BOOL willDisplay = ASInterfaceStateIncludesDisplay(newInterfaceState); + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(self.interfaceState); + return willDisplay && (willDisplay != nowDisplay); +} + +- (void)addInterfaceStateDelegate:(id )interfaceStateDelegate +{ + ASDN::MutexLocker l(__instanceLock__); + _hasHadInterfaceStateDelegates = YES; + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == nil) { + _interfaceStateDelegates[i] = interfaceStateDelegate; + return; + } + } + ASDisplayNodeFailAssert(@"Exceeded interface state delegate limit: %d", AS_MAX_INTERFACE_STATE_DELEGATES); +} + +- (void)removeInterfaceStateDelegate:(id )interfaceStateDelegate +{ + ASDN::MutexLocker l(__instanceLock__); + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == interfaceStateDelegate) { + _interfaceStateDelegates[i] = nil; + break; + } + } +} + +- (BOOL)isVisible +{ + ASDN::MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesVisible(_interfaceState); +} + +- (void)didEnterVisibleState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterVisibleState]; + }]; +#if AS_ENABLE_TIPS + [ASTipsController.shared nodeDidAppear:self]; +#endif +} + +- (void)didExitVisibleState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitVisibleState]; + }]; +} + +- (BOOL)isInDisplayState +{ + ASDN::MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesDisplay(_interfaceState); +} + +- (void)didEnterDisplayState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterDisplayState]; + }]; +} + +- (void)didExitDisplayState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitDisplayState]; + }]; +} + +- (BOOL)isInPreloadState +{ + ASDN::MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesPreload(_interfaceState); +} + +- (void)setNeedsPreload +{ + if (self.isInPreloadState) { + [self recursivelyPreload]; + } +} + +- (void)recursivelyPreload +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node didEnterPreloadState]; + }); + }); +} + +- (void)recursivelyClearPreloadedData +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node didExitPreloadState]; + }); + }); +} + +- (void)didEnterPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + // If this node has ASM enabled and is not yet visible, force a layout pass to apply its applicable pending layout, if any, + // so that its subnodes are inserted/deleted and start preloading right away. + // + // - If it has an up-to-date layout (and subnodes), calling -layoutIfNeeded will be fast. + // + // - If it doesn't have a calculated or pending layout that fits its current bounds, a measurement pass will occur + // (see -__layout and -_u_measureNodeWithBoundsIfNecessary:). This scenario is uncommon, + // and running a measurement pass here is a fine trade-off because preloading any time after this point would be late. + if (self.automaticallyManagesSubnodes) { + [self layoutIfNeeded]; + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterPreloadState]; + }]; +} + +- (void)didExitPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitPreloadState]; + }]; +} + +- (void)clearContents +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + ASDN::MutexLocker l(__instanceLock__); + if (_flags.canClearContentsOfLayer) { + // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. + _layer.contents = nil; + } + + _placeholderLayer.contents = nil; + _placeholderImage = nil; +} + +- (void)recursivelyClearContents +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node clearContents]; + }); + }); +} + +- (void)enumerateInterfaceStateDelegates:(void (NS_NOESCAPE ^)(id))block +{ + ASAssertUnlocked(__instanceLock__); + + id dels[AS_MAX_INTERFACE_STATE_DELEGATES]; + int count = 0; + { + ASLockScopeSelf(); + // Fast path for non-delegating nodes. + if (!_hasHadInterfaceStateDelegates) { + return; + } + + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if ((dels[count] = _interfaceStateDelegates[i])) { + count++; + } + } + } + for (int i = 0; i < count; i++) { + block(dels[i]); + } +} + +#pragma mark - Gesture Recognizing + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // This method is only implemented on UIView on iOS 6+. + ASDisplayNodeAssertMainThread(); + + // No locking needed as it's main thread only + UIView *view = _view; + if (view == nil) { + return YES; + } + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = view.superview; + return [superview gestureRecognizerShouldBegin:gestureRecognizer]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + return [_view hitTest:point withEvent:event]; +} + +- (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop +{ + ASDN::MutexLocker l(__instanceLock__); + _hitTestSlop = hitTestSlop; +} + +- (UIEdgeInsets)hitTestSlop +{ + ASDN::MutexLocker l(__instanceLock__); + return _hitTestSlop; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + UIEdgeInsets slop = self.hitTestSlop; + if (_view && UIEdgeInsetsEqualToEdgeInsets(slop, UIEdgeInsetsZero)) { + // Safer to use UIView's -pointInside:withEvent: if we can. + return [_view pointInside:point withEvent:event]; + } else { + return CGRectContainsPoint(UIEdgeInsetsInsetRect(self.bounds, slop), point); + } +} + + +#pragma mark - Pending View State + +- (void)_locked_applyPendingStateToViewOrLayer +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert(self.nodeLoaded, @"must have a view or layer"); + + TIME_SCOPED(_debugTimeToApplyPendingState); + + // If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values + // for the view/layer are still valid. + [self _locked_applyPendingViewState]; + + if (_flags.displaySuspended) { + self._locked_asyncLayer.displaySuspended = YES; + } + if (!_flags.displaysAsynchronously) { + self._locked_asyncLayer.displaysAsynchronously = NO; + } +} + +- (void)applyPendingViewState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + ASDN::MutexLocker l(__instanceLock__); + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + // MutexUnlocker will re-lock at the end of scope. + ASDN::MutexUnlocker u(__instanceLock__); + [self __setNeedsLayout]; + } + + [self _locked_applyPendingViewState]; +} + +- (void)_locked_applyPendingViewState +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert([self _locked_isNodeLoaded], @"Expected node to be loaded before applying pending state."); + + if (_flags.layerBacked) { + [_pendingViewState applyToLayer:_layer]; + } else { + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); + [_pendingViewState applyToView:_view withSpecialPropertiesHandling:specialPropertiesHandling]; + } + + // _ASPendingState objects can add up very quickly when adding + // many nodes. This is especially an issue in large collection views + // and table views. This needs to be weighed against the cost of + // reallocing a _ASPendingState. So in range managed nodes we + // delete the pending state, otherwise we just clear it. + if (ASHierarchyStateIncludesRangeManaged(_hierarchyState)) { + _pendingViewState = nil; + } else { + [_pendingViewState clearChanges]; + } +} + +// This method has proved helpful in a few rare scenarios, similar to a category extension on UIView, but assumes knowledge of _ASDisplayView. +// It's considered private API for now and its use should not be encouraged. +- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy +{ + ASDisplayNode *supernode = self.supernode; + while (supernode) { + if ([supernode isKindOfClass:supernodeClass]) + return supernode; + supernode = supernode.supernode; + } + if (!checkViewHierarchy) { + return nil; + } + + UIView *view = self.view.superview; + while (view) { + ASDisplayNode *viewNode = ((_ASDisplayView *)view).asyncdisplaykit_node; + if (viewNode) { + if ([viewNode isKindOfClass:supernodeClass]) + return viewNode; + } + + view = view.superview; + } + + return nil; +} + +#pragma mark - Performance Measurement + +- (void)setMeasurementOptions:(ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + ASDN::MutexLocker l(__instanceLock__); + _measurementOptions = measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + ASDN::MutexLocker l(__instanceLock__); + return _measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurements)performanceMeasurements +{ + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN, .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN }; + if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec) { + measurements.layoutSpecNumberOfPasses = _layoutSpecNumberOfPasses; + measurements.layoutSpecTotalTime = _layoutSpecTotalTime; + } + if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation) { + measurements.layoutComputationNumberOfPasses = _layoutComputationNumberOfPasses; + measurements.layoutComputationTotalTime = _layoutComputationTotalTime; + } + return measurements; +} + +#pragma mark - Accessibility + +- (void)setIsAccessibilityContainer:(BOOL)isAccessibilityContainer +{ + ASDN::MutexLocker l(__instanceLock__); + _isAccessibilityContainer = isAccessibilityContainer; +} + +- (BOOL)isAccessibilityContainer +{ + ASDN::MutexLocker l(__instanceLock__); + return _isAccessibilityContainer; +} + +#pragma mark - Debugging (Private) + +#if ASEVENTLOG_ENABLE +- (ASEventLog *)eventLog +{ + return _eventLog; +} +#endif + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + ASPushMainThreadAssertionsDisabled(); + + NSString *debugName = self.debugName; + if (debugName.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(debugName) }]; + } + + NSString *axId = self.accessibilityIdentifier; + if (axId.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(axId) }]; + } + + ASPopMainThreadAssertionsDisabled(); + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if (self.debugName.length > 0) { + [result addObject:@{ @"debugName" : ASStringWithQuotesIfMultiword(self.debugName)}]; + } + if (self.accessibilityIdentifier.length > 0) { + [result addObject:@{ @"axId": ASStringWithQuotesIfMultiword(self.accessibilityIdentifier) }]; + } + + CGRect windowFrame = [self _frameInWindow]; + if (CGRectIsNull(windowFrame) == NO) { + [result addObject:@{ @"frameInWindow" : [NSValue valueWithCGRect:windowFrame] }]; + } + + // Attempt to find view controller. + // Note that the convenience method asdk_associatedViewController has an assertion + // that it's run on main. Since this is a debug method, let's bypass the assertion + // and run up the chain ourselves. + if (_view != nil) { + for (UIResponder *responder in [_view asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc) { + [result addObject:@{ @"viewController" : ASObjectDescriptionMakeTiny(vc) }]; + break; + } + } + } + + if (_view != nil) { + [result addObject:@{ @"alpha" : @(_view.alpha) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_view.frame] }]; + } else if (_layer != nil) { + [result addObject:@{ @"alpha" : @(_layer.opacity) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_layer.frame] }]; + } else if (_pendingViewState != nil) { + [result addObject:@{ @"alpha" : @(_pendingViewState.alpha) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_pendingViewState.frame] }]; + } +#ifndef MINIMAL_ASDK + // Check supernode so that if we are a cell node we don't find self. + ASCellNode *cellNode = [self supernodeOfClass:[ASCellNode class] includingSelf:NO]; + if (cellNode != nil) { + [result addObject:@{ @"cellNode" : ASObjectDescriptionMakeTiny(cellNode) }]; + } +#endif + + [result addObject:@{ @"interfaceState" : NSStringFromASInterfaceState(self.interfaceState)} ]; + + if (_view != nil) { + [result addObject:@{ @"view" : ASObjectDescriptionMakeTiny(_view) }]; + } else if (_layer != nil) { + [result addObject:@{ @"layer" : ASObjectDescriptionMakeTiny(_layer) }]; + } else if (_viewClass != nil) { + [result addObject:@{ @"viewClass" : _viewClass }]; + } else if (_layerClass != nil) { + [result addObject:@{ @"layerClass" : _layerClass }]; + } else if (_viewBlock != nil) { + [result addObject:@{ @"viewBlock" : _viewBlock }]; + } else if (_layerBlock != nil) { + [result addObject:@{ @"layerBlock" : _layerBlock }]; + } + +#if TIME_DISPLAYNODE_OPS + NSString *creationTypeString = [NSString stringWithFormat:@"cr8:%.2lfms dl:%.2lfms ap:%.2lfms ad:%.2lfms", 1000 * _debugTimeToCreateView, 1000 * _debugTimeForDidLoad, 1000 * _debugTimeToApplyPendingState, 1000 * _debugTimeToAddSubnodeViews]; + [result addObject:@{ @"creationTypeString" : creationTypeString }]; +#endif + + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)debugDescription +{ + ASPushMainThreadAssertionsDisabled(); + let result = ASObjectDescriptionMake(self, [self propertiesForDebugDescription]); + ASPopMainThreadAssertionsDisabled(); + return result; +} + +// This should only be called for debugging. It's not thread safe and it doesn't assert. +// NOTE: Returns CGRectNull if the node isn't in a hierarchy. +- (CGRect)_frameInWindow +{ + if (self.isNodeLoaded == NO || self.isInHierarchy == NO) { + return CGRectNull; + } + + if (self.layerBacked) { + CALayer *rootLayer = _layer; + CALayer *nextLayer = nil; + while ((nextLayer = rootLayer.superlayer) != nil) { + rootLayer = nextLayer; + } + + return [_layer convertRect:self.threadSafeBounds toLayer:rootLayer]; + } else { + return [_view convertRect:self.threadSafeBounds toView:nil]; + } +} + +#pragma mark - Trait Collection Hooks + +- (void)asyncTraitCollectionDidChange +{ + // Subclass override +} +@end + +#pragma mark - ASDisplayNode (Debugging) + +@implementation ASDisplayNode (Debugging) + ++ (void)setShouldStoreUnflattenedLayouts:(BOOL)shouldStore +{ + storesUnflattenedLayouts.store(shouldStore); +} + ++ (BOOL)shouldStoreUnflattenedLayouts +{ + return storesUnflattenedLayouts.load(); +} + +- (ASLayout *)unflattenedCalculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + return _unflattenedLayout; +} + +- (NSString *)displayNodeRecursiveDescription +{ + return [self _recursiveDescriptionHelperWithIndent:@""]; +} + +- (NSString *)_recursiveDescriptionHelperWithIndent:(NSString *)indent +{ + NSMutableString *subtree = [[[indent stringByAppendingString:self.debugDescription] stringByAppendingString:@"\n"] mutableCopy]; + for (ASDisplayNode *n in self.subnodes) { + [subtree appendString:[n _recursiveDescriptionHelperWithIndent:[indent stringByAppendingString:@" | "]]]; + } + return subtree; +} + +- (NSString *)detailedLayoutDescription +{ + ASPushMainThreadAssertionsDisabled(); + ASDN::MutexLocker l(__instanceLock__); + let props = [[NSMutableArray alloc] init]; + + [props addObject:@{ @"layoutVersion": @(_layoutVersion.load()) }]; + [props addObject:@{ @"bounds": [NSValue valueWithCGRect:self.bounds] }]; + + if (_calculatedDisplayNodeLayout.layout) { + [props addObject:@{ @"calculatedLayout": _calculatedDisplayNodeLayout.layout }]; + [props addObject:@{ @"calculatedVersion": @(_calculatedDisplayNodeLayout.version) }]; + [props addObject:@{ @"calculatedConstrainedSize" : NSStringFromASSizeRange(_calculatedDisplayNodeLayout.constrainedSize) }]; + if (_calculatedDisplayNodeLayout.requestedLayoutFromAbove) { + [props addObject:@{ @"calculatedRequestedLayoutFromAbove": @"YES" }]; + } + } + if (_pendingDisplayNodeLayout.layout) { + [props addObject:@{ @"pendingLayout": _pendingDisplayNodeLayout.layout }]; + [props addObject:@{ @"pendingVersion": @(_pendingDisplayNodeLayout.version) }]; + [props addObject:@{ @"pendingConstrainedSize" : NSStringFromASSizeRange(_pendingDisplayNodeLayout.constrainedSize) }]; + if (_pendingDisplayNodeLayout.requestedLayoutFromAbove) { + [props addObject:@{ @"pendingRequestedLayoutFromAbove": (id)kCFNull }]; + } + } + + ASPopMainThreadAssertionsDisabled(); + return ASObjectDescriptionMake(self, props); +} + +@end + +#pragma mark - ASDisplayNode UIKit / CA Categories + +// We use associated objects as a last resort if our view is not a _ASDisplayView ie it doesn't have the _node ivar to write to + +static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; + +@implementation UIView (ASDisplayNodeInternal) + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, weakProxy, OBJC_ASSOCIATION_RETAIN); // Weak reference to avoid cycle, since the node retains the view. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASWeakProxy *weakProxy = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return weakProxy.target; +} + +@end + +@implementation CALayer (ASDisplayNodeInternal) + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, weakProxy, OBJC_ASSOCIATION_RETAIN); // Weak reference to avoid cycle, since the node retains the layer. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASWeakProxy *weakProxy = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return weakProxy.target; +} + +@end + +@implementation UIView (AsyncDisplayKit) + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + if (subnode.layerBacked) { + // Call -addSubnode: so that we use the asyncdisplaykit_node path if possible. + [self.layer addSubnode:subnode]; + } else { + ASDisplayNode *selfNode = self.asyncdisplaykit_node; + if (selfNode) { + [selfNode addSubnode:subnode]; + } else { + if (subnode.supernode) { + [subnode removeFromSupernode]; + } + [self addSubview:subnode.view]; + } + } +} + +@end + +@implementation CALayer (AsyncDisplayKit) + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNode *selfNode = self.asyncdisplaykit_node; + if (selfNode) { + [selfNode addSubnode:subnode]; + } else { + if (subnode.supernode) { + [subnode removeFromSupernode]; + } + [self addSublayer:subnode.layer]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.h b/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.h new file mode 100644 index 0000000000..09512ead9b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.h @@ -0,0 +1,213 @@ +// +// ASDisplayNodeExtras.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import +#import + +/** + * Sets the debugName field for these nodes to the given symbol names, within the domain of "self.class" + * For instance, in `MYButtonNode` if you call `ASSetDebugNames(self.titleNode, _countNode)` the debug names + * for the nodes will be set to `MYButtonNode.titleNode` and `MYButtonNode.countNode`. + */ +#if DEBUG + #define ASSetDebugName(node, format, ...) node.debugName = [NSString stringWithFormat:format, __VA_ARGS__] + #define ASSetDebugNames(...) _ASSetDebugNames(self.class, @"" # __VA_ARGS__, __VA_ARGS__, nil) +#else + #define ASSetDebugName(node, format, ...) + #define ASSetDebugNames(...) +#endif + +NS_ASSUME_NONNULL_BEGIN + +/// For deallocation of objects on the main thread across multiple run loops. +AS_EXTERN void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr); + +// Because inline methods can't be extern'd and need to be part of the translation unit of code +// that compiles with them to actually inline, we both declare and define these in the header. +ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateVisible) == ASInterfaceStateVisible); +} + +ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesDisplay(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateDisplay) == ASInterfaceStateDisplay); +} + +ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesPreload(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); +} + +ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesMeasureLayout(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateMeasureLayout) == ASInterfaceStateMeasureLayout); +} + +__unused static NSString * NSStringFromASInterfaceState(ASInterfaceState interfaceState) +{ + NSMutableArray *states = [NSMutableArray array]; + if (interfaceState == ASInterfaceStateNone) { + [states addObject:@"No state"]; + } + if (ASInterfaceStateIncludesMeasureLayout(interfaceState)) { + [states addObject:@"MeasureLayout"]; + } + if (ASInterfaceStateIncludesPreload(interfaceState)) { + [states addObject:@"Preload"]; + } + if (ASInterfaceStateIncludesDisplay(interfaceState)) { + [states addObject:@"Display"]; + } + if (ASInterfaceStateIncludesVisible(interfaceState)) { + [states addObject:@"Visible"]; + } + return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; +} + +#define INTERFACE_STATE_DELTA(Name) ({ \ + if ((oldState & ASInterfaceState##Name) != (newState & ASInterfaceState##Name)) { \ + [changes appendFormat:@"%c%s ", (newState & ASInterfaceState##Name ? '+' : '-'), #Name]; \ + } \ +}) + +/// e.g. { +Visible, -Preload } (although that should never actually happen.) +/// NOTE: Changes to MeasureLayout state don't really mean anything so we omit them for now. +__unused static NSString *NSStringFromASInterfaceStateChange(ASInterfaceState oldState, ASInterfaceState newState) +{ + if (oldState == newState) { + return @"{ }"; + } + + NSMutableString *changes = [NSMutableString stringWithString:@"{ "]; + INTERFACE_STATE_DELTA(Preload); + INTERFACE_STATE_DELTA(Display); + INTERFACE_STATE_DELTA(Visible); + [changes appendString:@"}"]; + return changes; +} + +#undef INTERFACE_STATE_DELTA + +/** + Returns the appropriate interface state for a given ASDisplayNode and window + */ +AS_EXTERN ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window) AS_WARN_UNUSED_RESULT; + +/** + Given a layer, returns the associated display node, if any. + */ +AS_EXTERN ASDisplayNode * _Nullable ASLayerToDisplayNode(CALayer * _Nullable layer) AS_WARN_UNUSED_RESULT; + +/** + Given a view, returns the associated display node, if any. + */ +AS_EXTERN ASDisplayNode * _Nullable ASViewToDisplayNode(UIView * _Nullable view) AS_WARN_UNUSED_RESULT; + +/** + Given a node, returns the root of the node hierarchy (where supernode == nil) + */ +AS_EXTERN ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node) AS_WARN_UNUSED_RESULT; + +/** + If traverseSublayers == YES, this function will walk the layer hierarchy, spanning discontinuous sections of the node hierarchy\ + (e.g. the layers of UIKit intermediate views in UIViewControllers, UITableView, UICollectionView). + In the event that a node's backing layer is not created yet, the function will only walk the direct subnodes instead + of forcing the layer hierarchy to be created. + */ +AS_EXTERN void ASDisplayNodePerformBlockOnEveryNode(CALayer * _Nullable layer, ASDisplayNode * _Nullable node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node)); + +/** + This function will walk the node hierarchy in a breadth first fashion. It does run the block on the node provided + directly to the function call. It does NOT traverse sublayers. + */ +AS_EXTERN void ASDisplayNodePerformBlockOnEveryNodeBFS(ASDisplayNode *node, void(^block)(ASDisplayNode *node)); + +/** + Identical to ASDisplayNodePerformBlockOnEveryNode, except it does not run the block on the + node provided directly to the function call - only on all descendants. + */ +AS_EXTERN void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. + */ +AS_EXTERN ASDisplayNode * _Nullable ASDisplayNodeFindFirstSupernode(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use the `supernodes` property instead."); + +/** + Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class. + */ +AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use the `supernodeOfClass:includingSelf:` method instead."); + +/** + * Given a layer, find the window it lives in, if any. + */ +AS_EXTERN UIWindow * _Nullable ASFindWindowOfLayer(CALayer *layer) AS_WARN_UNUSED_RESULT; + +/** + * Given a layer, find the closest view it lives in, if any. + */ +AS_EXTERN UIView * _Nullable ASFindClosestViewOfLayer(CALayer *layer) AS_WARN_UNUSED_RESULT; + +/** + * Given two nodes, finds their most immediate common parent. Used for geometry conversion methods. + * NOTE: It is an error to try to convert between nodes which do not share a common ancestor. This behavior is + * disallowed in UIKit documentation and the behavior is left undefined. The output does not have a rigorously defined + * failure mode (i.e. returning CGPointZero or returning the point exactly as passed in). Rather than track the internal + * undefined and undocumented behavior of UIKit in ASDisplayNode, this operation is defined to be incorrect in all + * circumstances and must be fixed wherever encountered. + */ +AS_EXTERN ASDisplayNode * _Nullable ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2) AS_WARN_UNUSED_RESULT; + +/** + Given a display node, collects all descendants. This is a specialization of ASCollectContainer() that walks the Core Animation layer tree as opposed to the display node tree, thus supporting non-continues display node hierarchies. + */ +AS_EXTERN NSArray *ASCollectDisplayNodes(ASDisplayNode *node) AS_WARN_UNUSED_RESULT; + +/** + Given a display node, traverses down the node hierarchy, returning all the display nodes that pass the block. + */ +AS_EXTERN NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT; + +/** + Given a display node, traverses down the node hierarchy, returning all the display nodes of kind class. + */ +AS_EXTERN NSArray<__kindof ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT; + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node, including the start node that pass the block. + */ +AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstNode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT; + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node, excluding the start node, that pass the block + */ +AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT; + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node of kind class. + */ +AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT; + +AS_EXTERN UIColor *ASDisplayNodeDefaultPlaceholderColor(void) AS_WARN_UNUSED_RESULT; +AS_EXTERN UIColor *ASDisplayNodeDefaultTintColor(void) AS_WARN_UNUSED_RESULT; + +/** + Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported. + */ +AS_EXTERN void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node); +AS_EXTERN void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node); + +// Not to be called directly. +AS_EXTERN void _ASSetDebugNames(Class owningClass, NSString *names, ASDisplayNode * _Nullable object, ...); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm new file mode 100644 index 0000000000..d1be1576e3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm @@ -0,0 +1,338 @@ +// +// ASDisplayNodeExtras.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#import +#import + +void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr) { + /** + * UIKit components must be deallocated on the main thread. We use this shared + * run loop queue to gradually deallocate them across many turns of the main run loop. + */ + static ASRunLoopQueue *queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; + queue.batchSize = 10; + }); + + if (objectPtr != NULL && *objectPtr != nil) { + // TODO: If ASRunLoopQueue supported an "unsafe_unretained" mode, we could + // transfer the caller's +1 into it and save the retain/release pair. + + // Lock queue while enqueuing and releasing, so that there's no risk + // that the queue will release before we get a chance to release. + [queue lock]; + [queue enqueue:*objectPtr]; // Retain, +1 + *objectPtr = nil; // Release, +0 + [queue unlock]; // (After queue drains), release, -1 + } +} + +void _ASSetDebugNames(Class _Nonnull owningClass, NSString * _Nonnull names, ASDisplayNode * _Nullable object, ...) +{ + NSString *owningClassName = NSStringFromClass(owningClass); + NSArray *nameArray = [names componentsSeparatedByString:@", "]; + va_list args; + va_start(args, object); + NSInteger i = 0; + for (ASDisplayNode *node = object; node != nil; node = va_arg(args, id), i++) { + NSMutableString *symbolName = [nameArray[i] mutableCopy]; + // Remove any `self.` or `_` prefix + [symbolName replaceOccurrencesOfString:@"self." withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)]; + [symbolName replaceOccurrencesOfString:@"_" withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)]; + node.debugName = [NSString stringWithFormat:@"%@.%@", owningClassName, symbolName]; + } + ASDisplayNodeCAssert(nameArray.count == i, @"Malformed call to ASSetDebugNames: %@", names); + va_end(args); +} + +ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window) +{ + ASDisplayNodeCAssert(![displayNode isLayerBacked], @"displayNode must not be layer backed as it may have a nil window"); + if (displayNode && [displayNode supportsRangeManagedInterfaceState]) { + // Directly clear the visible bit if we are not in a window. This means that the interface state is, + // if not already, about to be set to invisible as it is not possible for an element to be visible + // while outside of a window. + ASInterfaceState interfaceState = displayNode.pendingInterfaceState; + return (window == nil ? (interfaceState &= (~ASInterfaceStateVisible)) : interfaceState); + } else { + // For not range managed nodes we might be on our own to try to guess if we're visible. + return (window == nil ? ASInterfaceStateNone : (ASInterfaceStateVisible | ASInterfaceStateDisplay)); + } +} + +ASDisplayNode *ASLayerToDisplayNode(CALayer *layer) +{ + return layer.asyncdisplaykit_node; +} + +ASDisplayNode *ASViewToDisplayNode(UIView *view) +{ + return view.asyncdisplaykit_node; +} + +void ASDisplayNodePerformBlockOnEveryNode(CALayer * _Nullable layer, ASDisplayNode * _Nullable node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node)) +{ + if (!node) { + ASDisplayNodeCAssertNotNil(layer, @"Cannot recursively perform with nil node and nil layer"); + ASDisplayNodeCAssertMainThread(); + node = ASLayerToDisplayNode(layer); + } + + if (node) { + block(node); + } + if (traverseSublayers && !layer && [node isNodeLoaded] && ASDisplayNodeThreadIsMain()) { + layer = node.layer; + } + + if (traverseSublayers && layer && node.rasterizesSubtree == NO) { + /// NOTE: The docs say `sublayers` returns a copy, but it does not. + /// See: http://stackoverflow.com/questions/14854480/collection-calayerarray-0x1ed8faa0-was-mutated-while-being-enumerated + for (CALayer *sublayer in [[layer sublayers] copy]) { + ASDisplayNodePerformBlockOnEveryNode(sublayer, nil, traverseSublayers, block); + } + } else if (node) { + for (ASDisplayNode *subnode in [node subnodes]) { + ASDisplayNodePerformBlockOnEveryNode(nil, subnode, traverseSublayers, block); + } + } +} + +void ASDisplayNodePerformBlockOnEveryNodeBFS(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) +{ + // Queue used to keep track of subnodes while traversing this layout in a BFS fashion. + std::queue queue; + queue.push(node); + + while (!queue.empty()) { + node = queue.front(); + queue.pop(); + + block(node); + + // Add all subnodes to process in next step + for (ASDisplayNode *subnode in node.subnodes) { + queue.push(subnode); + } + } +} + +void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node)) +{ + for (ASDisplayNode *subnode in node.subnodes) { + ASDisplayNodePerformBlockOnEveryNode(nil, subnode, YES, block); + } +} + +ASDisplayNode *ASDisplayNodeFindFirstSupernode(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +{ + // This function has historically started with `self` but the name suggests + // that it wouldn't. Perhaps we should change the behavior. + for (ASDisplayNode *ancestor in node.supernodesIncludingSelf) { + if (block(ancestor)) { + return ancestor; + } + } + return nil; +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) +{ + // This function has historically started with `self` but the name suggests + // that it wouldn't. Perhaps we should change the behavior. + return [start supernodeOfClass:c includingSelf:YES]; +} + +static void _ASCollectDisplayNodes(NSMutableArray *array, CALayer *layer) +{ + ASDisplayNode *node = ASLayerToDisplayNode(layer); + + if (nil != node) { + [array addObject:node]; + } + + for (CALayer *sublayer in layer.sublayers) + _ASCollectDisplayNodes(array, sublayer); +} + +NSArray *ASCollectDisplayNodes(ASDisplayNode *node) +{ + NSMutableArray *list = [[NSMutableArray alloc] init]; + for (CALayer *sublayer in node.layer.sublayers) { + _ASCollectDisplayNodes(list, sublayer); + } + return list; +} + +#pragma mark - Find all subnodes + +static void _ASDisplayNodeFindAllSubnodes(NSMutableArray *array, ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +{ + if (!node) + return; + + for (ASDisplayNode *subnode in node.subnodes) { + if (block(subnode)) { + [array addObject:subnode]; + } + + _ASDisplayNodeFindAllSubnodes(array, subnode, block); + } +} + +NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) +{ + NSMutableArray *list = [[NSMutableArray alloc] init]; + _ASDisplayNodeFindAllSubnodes(list, start, block); + return list; +} + +NSArray<__kindof ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFindAllSubnodes(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +#pragma mark - Find first subnode + +static ASDisplayNode *_ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) +{ + for (ASDisplayNode *subnode in startNode.subnodes) { + ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block); + if (foundNode) { + return foundNode; + } + } + + if (includeStartNode && block(startNode)) + return startNode; + + return nil; +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, YES, block); +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, NO, block); +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFindFirstSubnode(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +static inline BOOL _ASDisplayNodeIsAncestorOfDisplayNode(ASDisplayNode *possibleAncestor, ASDisplayNode *possibleDescendant) +{ + ASDisplayNode *supernode = possibleDescendant; + while (supernode) { + if (supernode == possibleAncestor) { + return YES; + } + supernode = supernode.supernode; + } + + return NO; +} + +UIWindow * _Nullable ASFindWindowOfLayer(CALayer *layer) +{ + UIView *view = ASFindClosestViewOfLayer(layer); + if (UIWindow *window = ASDynamicCast(view, UIWindow)) { + return window; + } else { + return view.window; + } +} + +UIView * _Nullable ASFindClosestViewOfLayer(CALayer *layer) +{ + while (layer != nil) { + if (UIView *view = ASDynamicCast(layer.delegate, UIView)) { + return view; + } + layer = layer.superlayer; + } + return nil; +} + +ASDisplayNode *ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2) +{ + ASDisplayNode *possibleAncestor = node1; + while (possibleAncestor) { + if (_ASDisplayNodeIsAncestorOfDisplayNode(possibleAncestor, node2)) { + break; + } + possibleAncestor = possibleAncestor.supernode; + } + + ASDisplayNodeCAssertNotNil(possibleAncestor, @"Could not find a common ancestor between node1: %@ and node2: %@", node1, node2); + return possibleAncestor; +} + +ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node) +{ + // node <- supernode on each loop + // previous <- node on each loop where node is not nil + // previous is the final non-nil value of supernode, i.e. the root node + ASDisplayNode *previousNode = node; + while ((node = [node supernode])) { + previousNode = node; + } + return previousNode; +} + +#pragma mark - Placeholders + +UIColor *ASDisplayNodeDefaultPlaceholderColor() +{ + static UIColor *defaultPlaceholderColor; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultPlaceholderColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + }); + return defaultPlaceholderColor; +} + +UIColor *ASDisplayNodeDefaultTintColor() +{ + static UIColor *defaultTintColor; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultTintColor = [UIColor colorWithRed:0.0 green:0.478 blue:1.0 alpha:1.0]; + }); + return defaultTintColor; +} + +#pragma mark - Hierarchy Notifications + +void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node) +{ + [node __incrementVisibilityNotificationsDisabled]; +} + +void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node) +{ + [node __decrementVisibilityNotificationsDisabled]; +} diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.h b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.h new file mode 100644 index 0000000000..030084cfb1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.h @@ -0,0 +1,222 @@ +// +// ASEditableTextNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASEditableTextNodeDelegate; +@class ASTextKitComponents; + +@interface ASEditableTextNodeTargetForAction: NSObject + +@property (nonatomic, strong, readonly) id _Nullable target; + +- (instancetype)initWithTarget:(id _Nullable)target; + +@end + +/** + @abstract Implements a node that supports text editing. + @discussion Does not support layer backing. + */ +@interface ASEditableTextNode : ASDisplayNode + +/** + * @abstract Initializes an editable text node using default TextKit components. + * + * @return An initialized ASEditableTextNode. + */ +- (instancetype)init; + +/** + * @abstract Initializes an editable text node using the provided TextKit components. + * + * @param textKitComponents The TextKit stack used to render text. + * @param placeholderTextKitComponents The TextKit stack used to render placeholder text. + * + * @return An initialized ASEditableTextNode. + */ +- (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents + placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents; + +//! @abstract The text node's delegate, which must conform to the protocol. +@property (nullable, weak) id delegate; + +#pragma mark - Configuration + +/** + @abstract Enable scrolling on the textView + @default true + */ +@property (nonatomic) BOOL scrollEnabled; +@property (nonatomic, strong) UIFont *baseFont; + +/** + @abstract Access to underlying UITextView for more configuration options. + @warning This property should only be used on the main thread and should not be accessed before the editable text node's view is created. + */ +@property (nonatomic, readonly) UITextView *textView; + +//! @abstract The attributes to apply to new text being entered by the user. +@property (nullable, nonatomic, copy) NSDictionary *typingAttributes; + +//! @abstract The range of text currently selected. If length is zero, the range is the cursor location. +@property NSRange selectedRange; + +#pragma mark - Placeholder +/** + @abstract Indicates if the receiver is displaying the placeholder text. + @discussion To update the placeholder, see the property. + @result YES if the placeholder is currently displayed; NO otherwise. + */ +- (BOOL)isDisplayingPlaceholder AS_WARN_UNUSED_RESULT; + +/** + @abstract The styled placeholder text displayed by the text node while no text is entered + @discussion The placeholder is displayed when the user has not entered any text and the keyboard is not visible. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedPlaceholderText; + +#pragma mark - Modifying User Text +/** + @abstract The styled text displayed by the receiver. + @discussion When the placeholder is displayed (as indicated by -isDisplayingPlaceholder), this value is nil. Otherwise, this value is the attributed text the user has entered. This value can be modified regardless of whether the receiver is the first responder (and thus, editing) or not. Changing this value from nil to non-nil will result in the placeholder being hidden, and the new value being displayed. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedText; + +#pragma mark - Managing The Keyboard +//! @abstract The text input mode used by the receiver's keyboard, if it is visible. This value is undefined if the receiver is not the first responder. +@property (nonatomic, readonly) UITextInputMode *textInputMode; + +/** + @abstract The textContainerInset of both the placeholder and typed textView. This value defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets textContainerInset; + +/** + @abstract The maximum number of lines to display. Additional lines will require scrolling. + @default 0 (No limit) + */ +@property (nonatomic) NSUInteger maximumLinesToDisplay; + +/** + @abstract Indicates whether the receiver's text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user. + @result YES if the receiver's text view is the first-responder; NO otherwise. + */ +- (BOOL)isFirstResponder AS_WARN_UNUSED_RESULT; + +//! @abstract Makes the receiver's text view the first responder. +- (BOOL)becomeFirstResponder; + +//! @abstract Resigns the receiver's text view from first-responder status, if it has it. +- (BOOL)resignFirstResponder; + +#pragma mark - Geometry +/** + @abstract Returns the frame of the given range of characters. + @param textRange A range of characters. + @discussion This method raises an exception if `textRange` is not a valid range of characters within the receiver's attributed text. + @result A CGRect that is the bounding box of the glyphs covered by the given range of characters, in the coordinate system of the receiver. + */ +- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract properties. + */ +@property (nonatomic) UITextAutocapitalizationType autocapitalizationType; // default is UITextAutocapitalizationTypeSentences +@property (nonatomic) UITextAutocorrectionType autocorrectionType; // default is UITextAutocorrectionTypeDefault +@property (nonatomic) UITextSpellCheckingType spellCheckingType; // default is UITextSpellCheckingTypeDefault; +@property (nonatomic) UIKeyboardType keyboardType; // default is UIKeyboardTypeDefault +@property (nonatomic) UIKeyboardAppearance keyboardAppearance; // default is UIKeyboardAppearanceDefault +@property (nonatomic) UIReturnKeyType returnKeyType; // default is UIReturnKeyDefault (See note under UIReturnKeyType enum) +@property (nonatomic) BOOL enablesReturnKeyAutomatically; // default is NO (when YES, will automatically disable return key when text widget has zero-length contents, and will automatically enable when text widget has non-zero-length contents) +@property (nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; // default is NO + +@property (nonatomic, strong) NSString * _Nullable initialPrimaryLanguage; + +- (void)dropAutocorrection; + +@end + +@interface ASEditableTextNode (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +@end + +#pragma mark - +/** + * The methods declared by the ASEditableTextNodeDelegate protocol allow the adopting delegate to + * respond to notifications such as began and finished editing, selection changed and text updated; + * and manage whether a specified text should be replaced. + */ +@protocol ASEditableTextNodeDelegate + +@optional +/** + @abstract Asks the delegate if editing should begin for the text node. + @param editableTextNode An editable text node. + @discussion YES if editing should begin; NO if editing should not begin -- the default returns YES. + */ +- (BOOL)editableTextNodeShouldBeginEditing:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Indicates to the delegate that the text node began editing. + @param editableTextNode An editable text node. + @discussion The invocation of this method coincides with the keyboard animating to become visible. + */ +- (void)editableTextNodeDidBeginEditing:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Asks the delegate whether the specified text should be replaced in the editable text node. + @param editableTextNode An editable text node. + @param range The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character. + @param text The text to insert. + @discussion YES if the old text should be replaced by the new text; NO if the replacement operation should be aborted. + @result The text node calls this method whenever the user types a new character or deletes an existing character. Implementation of this method is optional -- the default implementation returns YES. + */ +- (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text; + +/** + @abstract Indicates to the delegate that the text node's selection has changed. + @param editableTextNode An editable text node. + @param fromSelectedRange The previously selected range. + @param toSelectedRange The current selected range. Equivalent to the property. + @param dueToEditing YES if the selection change was due to editing; NO otherwise. + @discussion You can access the selection of the receiver via . + */ +- (void)editableTextNodeDidChangeSelection:(ASEditableTextNode *)editableTextNode fromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange dueToEditing:(BOOL)dueToEditing; + +/** + @abstract Indicates to the delegate that the text node's text was updated. + @param editableTextNode An editable text node. + @discussion This method is called each time the user updated the text node's text. It is not called for programmatic changes made to the text via the property. + */ +- (void)editableTextNodeDidUpdateText:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Indicates to the delegate that the text node has finished editing. + @param editableTextNode An editable text node. + @discussion The invocation of this method coincides with the keyboard animating to become hidden. + */ +- (void)editableTextNodeDidFinishEditing:(ASEditableTextNode *)editableTextNode; + +- (BOOL)editableTextNodeShouldCopy:(ASEditableTextNode *)editableTextNode; +- (BOOL)editableTextNodeShouldPaste:(ASEditableTextNode *)editableTextNode; +- (ASEditableTextNodeTargetForAction * _Nullable)editableTextNodeTargetForAction:(SEL)action; +- (BOOL)editableTextNodeShouldReturn:(ASEditableTextNode *)editableTextNode; +- (void)editableTextNodeBackspaceWhileEmpty:(ASEditableTextNode *)editableTextNode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.h.orig b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.h.orig new file mode 100644 index 0000000000..ba546db317 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.h.orig @@ -0,0 +1,221 @@ +// +// ASEditableTextNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASEditableTextNodeDelegate; +@class ASTextKitComponents; + +@interface ASEditableTextNodeTargetForAction: NSObject + +@property (nonatomic, strong, readonly) id _Nullable target; + +- (instancetype)initWithTarget:(id _Nullable)target; + +@end + +/** + @abstract Implements a node that supports text editing. + @discussion Does not support layer backing. + */ +@interface ASEditableTextNode : ASDisplayNode + +/** + * @abstract Initializes an editable text node using default TextKit components. + * + * @return An initialized ASEditableTextNode. + */ +- (instancetype)init; + +/** + * @abstract Initializes an editable text node using the provided TextKit components. + * + * @param textKitComponents The TextKit stack used to render text. + * @param placeholderTextKitComponents The TextKit stack used to render placeholder text. + * + * @return An initialized ASEditableTextNode. + */ +- (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents + placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents; + +//! @abstract The text node's delegate, which must conform to the protocol. +@property (nullable, weak) id delegate; + +#pragma mark - Configuration + +/** + @abstract Enable scrolling on the textView + @default true + */ +@property (nonatomic) BOOL scrollEnabled; + +/** + @abstract Access to underlying UITextView for more configuration options. + @warning This property should only be used on the main thread and should not be accessed before the editable text node's view is created. + */ +@property (nonatomic, readonly) UITextView *textView; + +//! @abstract The attributes to apply to new text being entered by the user. +@property (nullable, nonatomic, copy) NSDictionary *typingAttributes; + +//! @abstract The range of text currently selected. If length is zero, the range is the cursor location. +@property NSRange selectedRange; + +#pragma mark - Placeholder +/** + @abstract Indicates if the receiver is displaying the placeholder text. + @discussion To update the placeholder, see the property. + @result YES if the placeholder is currently displayed; NO otherwise. + */ +- (BOOL)isDisplayingPlaceholder AS_WARN_UNUSED_RESULT; + +/** + @abstract The styled placeholder text displayed by the text node while no text is entered + @discussion The placeholder is displayed when the user has not entered any text and the keyboard is not visible. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedPlaceholderText; + +#pragma mark - Modifying User Text +/** + @abstract The styled text displayed by the receiver. + @discussion When the placeholder is displayed (as indicated by -isDisplayingPlaceholder), this value is nil. Otherwise, this value is the attributed text the user has entered. This value can be modified regardless of whether the receiver is the first responder (and thus, editing) or not. Changing this value from nil to non-nil will result in the placeholder being hidden, and the new value being displayed. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedText; + +#pragma mark - Managing The Keyboard +//! @abstract The text input mode used by the receiver's keyboard, if it is visible. This value is undefined if the receiver is not the first responder. +@property (nonatomic, readonly) UITextInputMode *textInputMode; + +/** + @abstract The textContainerInset of both the placeholder and typed textView. This value defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets textContainerInset; + +/** + @abstract The maximum number of lines to display. Additional lines will require scrolling. + @default 0 (No limit) + */ +@property (nonatomic) NSUInteger maximumLinesToDisplay; + +/** + @abstract Indicates whether the receiver's text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user. + @result YES if the receiver's text view is the first-responder; NO otherwise. + */ +- (BOOL)isFirstResponder AS_WARN_UNUSED_RESULT; + +//! @abstract Makes the receiver's text view the first responder. +- (BOOL)becomeFirstResponder; + +//! @abstract Resigns the receiver's text view from first-responder status, if it has it. +- (BOOL)resignFirstResponder; + +#pragma mark - Geometry +/** + @abstract Returns the frame of the given range of characters. + @param textRange A range of characters. + @discussion This method raises an exception if `textRange` is not a valid range of characters within the receiver's attributed text. + @result A CGRect that is the bounding box of the glyphs covered by the given range of characters, in the coordinate system of the receiver. + */ +- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract properties. + */ +@property (nonatomic) UITextAutocapitalizationType autocapitalizationType; // default is UITextAutocapitalizationTypeSentences +@property (nonatomic) UITextAutocorrectionType autocorrectionType; // default is UITextAutocorrectionTypeDefault +@property (nonatomic) UITextSpellCheckingType spellCheckingType; // default is UITextSpellCheckingTypeDefault; +@property (nonatomic) UIKeyboardType keyboardType; // default is UIKeyboardTypeDefault +@property (nonatomic) UIKeyboardAppearance keyboardAppearance; // default is UIKeyboardAppearanceDefault +@property (nonatomic) UIReturnKeyType returnKeyType; // default is UIReturnKeyDefault (See note under UIReturnKeyType enum) +@property (nonatomic) BOOL enablesReturnKeyAutomatically; // default is NO (when YES, will automatically disable return key when text widget has zero-length contents, and will automatically enable when text widget has non-zero-length contents) +@property (nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; // default is NO + +- (void)dropAutocorrection; + + +@end + +@interface ASEditableTextNode (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +@end + +#pragma mark - +/** + * The methods declared by the ASEditableTextNodeDelegate protocol allow the adopting delegate to + * respond to notifications such as began and finished editing, selection changed and text updated; + * and manage whether a specified text should be replaced. + */ +@protocol ASEditableTextNodeDelegate + +@optional +/** + @abstract Asks the delegate if editing should begin for the text node. + @param editableTextNode An editable text node. + @discussion YES if editing should begin; NO if editing should not begin -- the default returns YES. + */ +- (BOOL)editableTextNodeShouldBeginEditing:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Indicates to the delegate that the text node began editing. + @param editableTextNode An editable text node. + @discussion The invocation of this method coincides with the keyboard animating to become visible. + */ +- (void)editableTextNodeDidBeginEditing:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Asks the delegate whether the specified text should be replaced in the editable text node. + @param editableTextNode An editable text node. + @param range The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character. + @param text The text to insert. + @discussion YES if the old text should be replaced by the new text; NO if the replacement operation should be aborted. + @result The text node calls this method whenever the user types a new character or deletes an existing character. Implementation of this method is optional -- the default implementation returns YES. + */ +- (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text; + +/** + @abstract Indicates to the delegate that the text node's selection has changed. + @param editableTextNode An editable text node. + @param fromSelectedRange The previously selected range. + @param toSelectedRange The current selected range. Equivalent to the property. + @param dueToEditing YES if the selection change was due to editing; NO otherwise. + @discussion You can access the selection of the receiver via . + */ +- (void)editableTextNodeDidChangeSelection:(ASEditableTextNode *)editableTextNode fromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange dueToEditing:(BOOL)dueToEditing; + +/** + @abstract Indicates to the delegate that the text node's text was updated. + @param editableTextNode An editable text node. + @discussion This method is called each time the user updated the text node's text. It is not called for programmatic changes made to the text via the property. + */ +- (void)editableTextNodeDidUpdateText:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Indicates to the delegate that the text node has finished editing. + @param editableTextNode An editable text node. + @discussion The invocation of this method coincides with the keyboard animating to become hidden. + */ +- (void)editableTextNodeDidFinishEditing:(ASEditableTextNode *)editableTextNode; + +<<<<<<< HEAD +- (BOOL)editableTextNodeShouldPaste:(ASEditableTextNode *)editableTextNode; +- (ASEditableTextNodeTargetForAction * _Nullable)editableTextNodeTargetForAction:(SEL)action; +- (BOOL)editableTextNodeShouldReturn:(ASEditableTextNode *)editableTextNode; + +======= +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm new file mode 100644 index 0000000000..c1f4cd93ea --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -0,0 +1,1141 @@ +// +// ASEditableTextNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import +#import + +@implementation ASEditableTextNodeTargetForAction + +- (instancetype)initWithTarget:(id _Nullable)target { + self = [super init]; + if (self != nil) { + _target = target; + } + return self; +} + +@end + +/** + @abstract Object to hold UITextView's pending UITextInputTraits +**/ +@interface _ASTextInputTraitsPendingState : NSObject + +@property UITextAutocapitalizationType autocapitalizationType; +@property UITextAutocorrectionType autocorrectionType; +@property UITextSpellCheckingType spellCheckingType; +@property UIKeyboardAppearance keyboardAppearance; +@property UIKeyboardType keyboardType; +@property UIReturnKeyType returnKeyType; +@property BOOL enablesReturnKeyAutomatically; +@property (getter=isSecureTextEntry) BOOL secureTextEntry; + +@end + +@implementation _ASTextInputTraitsPendingState + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // set default values, as defined in Apple's comments in UITextInputTraits.h + _autocapitalizationType = UITextAutocapitalizationTypeSentences; + _autocorrectionType = UITextAutocorrectionTypeDefault; + _spellCheckingType = UITextSpellCheckingTypeDefault; + _keyboardAppearance = UIKeyboardAppearanceDefault; + _keyboardType = UIKeyboardTypeDefault; + _returnKeyType = UIReturnKeyDefault; + + return self; +} + +@end + +/** + @abstract As originally reported in rdar://14729288, when scrollEnabled = NO, + UITextView does not calculate its contentSize. This makes it difficult + for a client to embed a UITextView inside a different scroll view with + other content (setting scrollEnabled = NO on the UITextView itself, + because the containing scroll view will handle the gesture)... + because accessing contentSize is typically necessary to perform layout. + Apple later closed the issue as expected behavior. This works around + the issue by ensuring that contentSize is always calculated, while + still providing control over the UITextView's scrolling. + + See issue: https://github.com/facebook/AsyncDisplayKit/issues/1063 + */ + +@interface ASPanningOverriddenUITextView : ASTextKitComponentsTextView +{ + BOOL _shouldBlockPanGesture; + BOOL _initializedPrimaryInputLanguage; +} + +@property (nonatomic, copy) bool (^shouldCopy)(); +@property (nonatomic, copy) bool (^shouldPaste)(); +@property (nonatomic, copy) ASEditableTextNodeTargetForAction *(^targetForActionImpl)(SEL); +@property (nonatomic, copy) bool (^shouldReturn)(); +@property (nonatomic, copy) void (^backspaceWhileEmpty)(); + +@property (nonatomic, strong) NSString * _Nullable initialPrimaryLanguage; + +@end + +@implementation ASPanningOverriddenUITextView + +#if TARGET_OS_IOS + // tvOS doesn't support self.scrollsToTop +- (BOOL)scrollEnabled +{ + return _shouldBlockPanGesture; +} + +- (void)setScrollEnabled:(BOOL)scrollEnabled +{ + _shouldBlockPanGesture = !scrollEnabled; + self.scrollsToTop = scrollEnabled; + + [super setScrollEnabled:YES]; +} + +- (void)setContentSize:(CGSize)contentSize { + [super setContentSize:contentSize]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (_targetForActionImpl) { + ASEditableTextNodeTargetForAction *result = _targetForActionImpl(action); + if (result) { + return result.target != nil; + } + } + + if (action == @selector(paste:)) { + NSArray *items = [UIMenuController sharedMenuController].menuItems; + if (((UIMenuItem *)items.firstObject).action == @selector(toggleBoldface:)) { + return false; + } + return true; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + static SEL promptForReplaceSelector; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptForReplaceSelector = NSSelectorFromString(@"_promptForReplace:"); + }); + if (action == promptForReplaceSelector) { + return false; + } +#pragma clang diagnostic pop + + if (action == @selector(toggleUnderline:)) { + return false; + } + + return [super canPerformAction:action withSender:sender]; +} + +- (id)targetForAction:(SEL)action withSender:(id)__unused sender +{ + if (_targetForActionImpl) { + ASEditableTextNodeTargetForAction *result = _targetForActionImpl(action); + if (result) { + return result.target; + } + } + return [super targetForAction:action withSender:sender]; +} + +- (void)copy:(id)sender { + if (_shouldCopy == nil || _shouldCopy()) { + [super copy:sender]; + } +} + +- (void)paste:(id)sender +{ + if (_shouldPaste == nil || _shouldPaste()) { + [super paste:sender]; + } +} + +- (NSArray *)keyCommands { + UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)]; + return @[ + plainReturn + ]; +} + +- (void)handlePlainReturn:(id)__unused sender { + if (_shouldReturn) { + _shouldReturn(); + } +} + +- (void)deleteBackward { + bool notify = self.text.length == 0; + [super deleteBackward]; + if (notify) { + if (_backspaceWhileEmpty) { + _backspaceWhileEmpty(); + } + } +} + +- (UITextInputMode *)textInputMode { + if (!_initializedPrimaryInputLanguage) { + _initializedPrimaryInputLanguage = true; + if (_initialPrimaryLanguage != nil) { + for (UITextInputMode *inputMode in [UITextInputMode activeInputModes]) { + NSString *primaryLanguage = inputMode.primaryLanguage; + if (primaryLanguage != nil && [primaryLanguage isEqualToString:_initialPrimaryLanguage]) { + return inputMode; + } + } + } + } + return [super textInputMode]; +} + +#endif + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // Never allow our pans to begin when _shouldBlockPanGesture is true. + if (_shouldBlockPanGesture && gestureRecognizer == self.panGestureRecognizer) + return NO; + + // Otherwise, proceed as usual. + if ([UITextView instancesRespondToSelector:_cmd]) + return [super gestureRecognizerShouldBegin:gestureRecognizer]; + return YES; +} + +@end + +#pragma mark - +@interface ASEditableTextNode () +{ + @private + // Configuration. + NSDictionary *_typingAttributes; + + // Core. + id __weak _delegate; + BOOL _delegateDidUpdateEnqueued; + + // TextKit. + AS::RecursiveMutex _textKitLock; + ASTextKitComponents *_textKitComponents; + ASTextKitComponents *_placeholderTextKitComponents; + // Forwards NSLayoutManagerDelegate methods related to word kerning + ASTextNodeWordKerner *_wordKerner; + + // UITextInputTraits + AS::RecursiveMutex _textInputTraitsLock; + _ASTextInputTraitsPendingState *_textInputTraits; + + // Misc. State. + BOOL _displayingPlaceholder; // Defaults to YES. + BOOL _isPreservingSelection; + BOOL _isPreservingText; + BOOL _selectionChangedForEditedText; + NSRange _previousSelectedRange; +} + +@property (nonatomic, readonly) _ASTextInputTraitsPendingState *textInputTraits; + +@end + +@implementation ASEditableTextNode + +#pragma mark - NSObject Overrides +- (instancetype)init +{ + return [self initWithTextKitComponents:[ASTextKitComponents componentsWithAttributedSeedString:nil textContainerSize:CGSizeZero] + placeholderTextKitComponents:[ASTextKitComponents componentsWithAttributedSeedString:nil textContainerSize:CGSizeZero]]; +} + +- (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents + placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents +{ + if (!(self = [super init])) + return nil; + + _displayingPlaceholder = YES; + _scrollEnabled = YES; + + // Create the scaffolding for the text view. + _textKitComponents = textKitComponents; + _textKitComponents.layoutManager.delegate = self; + _wordKerner = [[ASTextNodeWordKerner alloc] init]; + _textContainerInset = UIEdgeInsetsZero; + + // Create the placeholder scaffolding. + _placeholderTextKitComponents = placeholderTextKitComponents; + _placeholderTextKitComponents.layoutManager.delegate = self; + + return self; +} + +#pragma mark - ASDisplayNode Overrides +- (void)didLoad +{ + [super didLoad]; + + void (^configureTextView)(UITextView *) = ^(UITextView *textView) { + if (!_displayingPlaceholder || textView != _textKitComponents.textView) { + // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. + textView.backgroundColor = self.backgroundColor; + textView.opaque = self.opaque; + } else if (_displayingPlaceholder && textView == _textKitComponents.textView) { + // The default backgroundColor for a textView is white. Due to the reason described above, make sure the editable textView starts out transparent. + textView.backgroundColor = nil; + textView.opaque = NO; + } + textView.textContainerInset = self.textContainerInset; + + // Configure textView with UITextInputTraits + { + AS::MutexLocker l(_textInputTraitsLock); + if (_textInputTraits) { + textView.autocapitalizationType = _textInputTraits.autocapitalizationType; + textView.autocorrectionType = _textInputTraits.autocorrectionType; + textView.spellCheckingType = _textInputTraits.spellCheckingType; + textView.keyboardType = _textInputTraits.keyboardType; + textView.keyboardAppearance = _textInputTraits.keyboardAppearance; + textView.returnKeyType = _textInputTraits.returnKeyType; + textView.enablesReturnKeyAutomatically = _textInputTraits.enablesReturnKeyAutomatically; + textView.secureTextEntry = _textInputTraits.isSecureTextEntry; + } + } + + [self.view addSubview:textView]; + }; + + AS::MutexLocker l(_textKitLock); + + // Create and configure the placeholder text view. + _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; + _placeholderTextKitComponents.textView.userInteractionEnabled = NO; + _placeholderTextKitComponents.textView.accessibilityElementsHidden = YES; + configureTextView(_placeholderTextKitComponents.textView); + + // Create and configure our text view. + ASPanningOverriddenUITextView *textView = [[ASPanningOverriddenUITextView alloc] initWithFrame:CGRectZero textContainer:_textKitComponents.textContainer]; + textView.initialPrimaryLanguage = _initialPrimaryLanguage; + __weak ASEditableTextNode *weakSelf = self; + textView.shouldCopy = ^bool{ + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeShouldCopy:)]) { + return [strongSelf->_delegate editableTextNodeShouldCopy:self]; + } + } + return true; + }; + textView.shouldPaste = ^bool{ + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeShouldPaste:)]) { + return [strongSelf->_delegate editableTextNodeShouldPaste:self]; + } + } + return true; + }; + textView.targetForActionImpl = ^id(SEL action) { + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeTargetForAction:)]) { + return [strongSelf->_delegate editableTextNodeTargetForAction:action]; + } + } + return nil; + }; + textView.shouldReturn = ^bool { + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeShouldReturn:)]) { + return [strongSelf->_delegate editableTextNodeShouldReturn:strongSelf]; + } + } + return true; + }; + textView.backspaceWhileEmpty = ^{ + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeBackspaceWhileEmpty:)]) { + [strongSelf->_delegate editableTextNodeBackspaceWhileEmpty:strongSelf]; + } + } + }; + _textKitComponents.textView = textView; + _textKitComponents.textView.scrollEnabled = _scrollEnabled; + _textKitComponents.textView.delegate = self; + #if TARGET_OS_IOS + _textKitComponents.textView.editable = YES; + #endif + _textKitComponents.textView.typingAttributes = _typingAttributes; + _textKitComponents.textView.accessibilityHint = _placeholderTextKitComponents.textStorage.string; + configureTextView(_textKitComponents.textView); + + [self _updateDisplayingPlaceholder]; + + // once view is loaded, setters set directly on view + _textInputTraits = nil; + + UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)]; + tapRecognizer.cancelsTouchesInView = false; + tapRecognizer.delaysTouchesBegan = false; + tapRecognizer.delaysTouchesEnded = false; + tapRecognizer.delegate = self; + [self.view addGestureRecognizer:tapRecognizer]; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + return true; +} + +- (void)tapGesture:(UITapGestureRecognizer *)recognizer { + static Class promptClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptClass = NSClassFromString([[NSString alloc] initWithFormat:@"%@AutocorrectInlinePrompt", @"UI"]); + }); + + if (recognizer.state == UIGestureRecognizerStateEnded) { + UIView *result = [self hitTest:[recognizer locationInView:self.view] withEvent:nil]; + if (result != nil && [result class] == promptClass) { + [self dropAutocorrection]; + } + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASTextKitComponents *displayedComponents = [self isDisplayingPlaceholder] ? _placeholderTextKitComponents : _textKitComponents; + + CGSize textSize; + + if (_maximumLinesToDisplay > 0) { + textSize = [displayedComponents sizeForConstrainedWidth:constrainedSize.width + forMaxNumberOfLines: _maximumLinesToDisplay]; + } else { + textSize = [displayedComponents sizeForConstrainedWidth:constrainedSize.width]; + } + + CGFloat width = std::ceil(textSize.width + _textContainerInset.left + _textContainerInset.right); + CGFloat height = std::ceil(textSize.height + _textContainerInset.top + _textContainerInset.bottom); + return CGSizeMake(std::fmin(width, constrainedSize.width), std::fmin(height, constrainedSize.height)); +} + +- (void)layout +{ + ASDisplayNodeAssertMainThread(); + + [super layout]; + [self _layoutTextView]; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + [super setBackgroundColor:backgroundColor]; + + AS::MutexLocker l(_textKitLock); + + // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. + // The backgroundColor/opaque will be propagated to the editable textView when editing begins. + if (!_displayingPlaceholder) { + _textKitComponents.textView.backgroundColor = backgroundColor; + } + _placeholderTextKitComponents.textView.backgroundColor = backgroundColor; +} + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + AS::MutexLocker l(_textKitLock); + + _textContainerInset = textContainerInset; + _textKitComponents.textView.textContainerInset = textContainerInset; + _placeholderTextKitComponents.textView.textContainerInset = textContainerInset; +} + +- (void)setOpaque:(BOOL)opaque +{ + [super setOpaque:opaque]; + + AS::MutexLocker l(_textKitLock); + + // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. + // The backgroundColor/opaque will be propagated to the editable textView when editing begins. + if (!_displayingPlaceholder) { + _textKitComponents.textView.opaque = opaque; + } + _placeholderTextKitComponents.textView.opaque = opaque; +} + +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!layerBacked, @"Cannot set layerBacked to YES on ASEditableTextNode – instances must be view-backed in order to ensure touch events can be passed to the internal UITextView during editing."); + [super setLayerBacked:layerBacked]; +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +#pragma mark - Configuration +@synthesize delegate = _delegate; + +- (void)setScrollEnabled:(BOOL)scrollEnabled +{ + AS::MutexLocker l(_textKitLock); + _scrollEnabled = scrollEnabled; + [_textKitComponents.textView setScrollEnabled:_scrollEnabled]; +} + +- (UITextView *)textView +{ + ASDisplayNodeAssertMainThread(); + [self view]; + ASDisplayNodeAssert(_textKitComponents.textView != nil, @"UITextView must be created in -[ASEditableTextNode didLoad]"); + return _textKitComponents.textView; +} + +- (void)setMaximumLinesToDisplay:(NSUInteger)maximumLines +{ + _maximumLinesToDisplay = maximumLines; + [self setNeedsLayout]; +} + +#pragma mark - +@dynamic typingAttributes; + +- (NSDictionary *)typingAttributes +{ + return _typingAttributes; +} + +- (void)setTypingAttributes:(NSDictionary *)typingAttributes +{ + if (ASObjectIsEqual(typingAttributes, _typingAttributes)) + return; + + _typingAttributes = [typingAttributes copy]; + + AS::MutexLocker l(_textKitLock); + + _textKitComponents.textView.typingAttributes = _typingAttributes; +} + +#pragma mark - +@dynamic selectedRange; + +- (NSRange)selectedRange +{ + AS::MutexLocker l(_textKitLock); + return _textKitComponents.textView.selectedRange; +} + +- (void)setSelectedRange:(NSRange)selectedRange +{ + AS::MutexLocker l(_textKitLock); + _textKitComponents.textView.selectedRange = selectedRange; +} + +#pragma mark - Placeholder +- (BOOL)isDisplayingPlaceholder +{ + return _displayingPlaceholder; +} + +#pragma mark - +@dynamic attributedPlaceholderText; +- (NSAttributedString *)attributedPlaceholderText +{ + AS::MutexLocker l(_textKitLock); + + return [_placeholderTextKitComponents.textStorage copy]; +} + +- (void)setAttributedPlaceholderText:(NSAttributedString *)attributedPlaceholderText +{ + AS::MutexLocker l(_textKitLock); + + if (ASObjectIsEqual(_placeholderTextKitComponents.textStorage, attributedPlaceholderText)) + return; + + [_placeholderTextKitComponents.textStorage setAttributedString:attributedPlaceholderText ? : [[NSAttributedString alloc] initWithString:@""]]; + _textKitComponents.textView.accessibilityHint = attributedPlaceholderText.string; +} + +#pragma mark - Modifying User Text +@dynamic attributedText; +- (NSAttributedString *)attributedText +{ + // Per contract in our header, this value is nil when the placeholder is displayed. + if ([self isDisplayingPlaceholder]) + return nil; + + AS::MutexLocker l(_textKitLock); + + return [_textKitComponents.textStorage copy]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + AS::MutexLocker l(_textKitLock); + + // If we (_cmd) are called while the text view itself is updating (-textViewDidUpdate:), you cannot update the text storage and expect perfect propagation to the text view. + // Thus, we always update the textview directly if it's been created already. + if (ASObjectIsEqual((_textKitComponents.textView.attributedText ? : _textKitComponents.textStorage), attributedText)) + return; + + // If the cursor isn't at the end of the text, we need to preserve the selected range to avoid moving the cursor. + NSRange selectedRange = _textKitComponents.textView.selectedRange; + BOOL preserveSelectedRange = (selectedRange.location != _textKitComponents.textStorage.length); + + NSAttributedString *attributedStringToDisplay = nil; + + if (attributedText) + attributedStringToDisplay = attributedText; + // Otherwise, note that we don't simply nil out attributed text. Because the insertion point is guided by the attributes at index 0, we need to attribute an empty string to ensure the insert point obeys our typing attributes. + else + attributedStringToDisplay = [[NSAttributedString alloc] initWithString:@"" attributes:self.typingAttributes]; + + // Always prefer updating the text view directly if it's been created (see above). + if (_textKitComponents.textView) + [_textKitComponents.textView setAttributedText:attributedStringToDisplay]; + else + [_textKitComponents.textStorage setAttributedString:attributedStringToDisplay]; + + // Calculated size depends on the seeded text. + [self setNeedsLayout]; + + // Update if placeholder is shown. + [self _updateDisplayingPlaceholder]; + + // Preserve cursor range, if necessary. + if (preserveSelectedRange) { + _isPreservingSelection = YES; // Used in -textViewDidChangeSelection: to avoid informing our delegate about our preservation. + [_textKitComponents.textView setSelectedRange:selectedRange]; + _isPreservingSelection = NO; + } +} + +- (void)dropAutocorrection { + _isPreservingSelection = YES; // Used in -textViewDidChangeSelection: to avoid informing our delegate about our preservation. + _isPreservingText = YES; + + UITextView *textView = _textKitComponents.textView; + + NSRange rangeCopy = textView.selectedRange; + NSRange fakeRange = rangeCopy; + if (fakeRange.location != 0) { + fakeRange.location--; + } + [textView unmarkText]; + [textView setSelectedRange:fakeRange]; + [textView setSelectedRange:rangeCopy]; + + //[_textKitComponents.textView.inputDelegate textWillChange:_textKitComponents.textView]; + //[_textKitComponents.textView.inputDelegate textDidChange:_textKitComponents.textView]; + + _isPreservingSelection = NO; + _isPreservingText = NO; +} + +#pragma mark - Core +- (void)_updateDisplayingPlaceholder +{ + AS::MutexLocker l(_textKitLock); + + // Show the placeholder if necessary. + _displayingPlaceholder = (_textKitComponents.textStorage.length == 0); + _placeholderTextKitComponents.textView.hidden = !_displayingPlaceholder; + + // If hiding the placeholder, propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and was kept transparent so it doesn't obscure the placeholder text. Now that we're editing it and the placeholder is hidden, we can make it opaque to avoid unnecessary blending. + if (!_displayingPlaceholder) { + _textKitComponents.textView.opaque = self.isOpaque; + _textKitComponents.textView.backgroundColor = self.backgroundColor; + } else { + _textKitComponents.textView.opaque = NO; + _textKitComponents.textView.backgroundColor = nil; + } +} + +- (void)_layoutTextView +{ + AS::MutexLocker l(_textKitLock); + + // Layout filling our bounds. + _textKitComponents.textView.frame = self.bounds; + _placeholderTextKitComponents.textView.frame = self.bounds; + + // Note that both of these won't be necessary once we can disable scrolling, pending rdar://14729288 + // When we resize to fit (above) the prior layout becomes invalid. For whatever reason, UITextView doesn't invalidate its layout when its frame changes on its own, so we have to do so ourselves. + [_textKitComponents.layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, [_textKitComponents.textStorage length]) actualCharacterRange:NULL]; + + // When you type beyond UITextView's bounds it scrolls you down a line. We need to remain at the top. + [_textKitComponents.textView setContentOffset:CGPointZero animated:NO]; + [_textKitComponents.layoutManager ensureGlyphsForCharacterRange:NSMakeRange(0, [_textKitComponents.textStorage length])]; + NSRange range = [self selectedRange]; + range.location = range.location + range.length - 1; + range.length = 1; + [self.textView scrollRangeToVisible:range]; + + CGPoint bottomOffset = CGPointMake(0, self.textView.contentSize.height - self.textView.bounds.size.height); + //[self.textView setContentOffset:bottomOffset animated:NO]; +} + +#pragma mark - Keyboard +@dynamic textInputMode; +- (UITextInputMode *)textInputMode +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView textInputMode]; +} + +- (BOOL)isFirstResponder +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView isFirstResponder]; +} + +- (BOOL)canBecomeFirstResponder { + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canBecomeFirstResponder]; +} + +- (BOOL)becomeFirstResponder +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView becomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canResignFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView resignFirstResponder]; +} + +#pragma mark - UITextInputTraits + +- (_ASTextInputTraitsPendingState *)textInputTraits +{ + if (!_textInputTraits) { + _textInputTraits = [[_ASTextInputTraitsPendingState alloc] init]; + } + return _textInputTraits; +} + +- (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizationType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setAutocapitalizationType:autocapitalizationType]; + } else { + [self.textInputTraits setAutocapitalizationType:autocapitalizationType]; + } +} + +- (UITextAutocapitalizationType)autocapitalizationType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView autocapitalizationType]; + } else { + return [self.textInputTraits autocapitalizationType]; + } +} + +- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setAutocorrectionType:autocorrectionType]; + } else { + [self.textInputTraits setAutocorrectionType:autocorrectionType]; + } +} + +- (UITextAutocorrectionType)autocorrectionType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView autocorrectionType]; + } else { + return [self.textInputTraits autocorrectionType]; + } +} + +- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setSpellCheckingType:spellCheckingType]; + } else { + [self.textInputTraits setSpellCheckingType:spellCheckingType]; + } +} + +- (UITextSpellCheckingType)spellCheckingType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView spellCheckingType]; + } else { + return [self.textInputTraits spellCheckingType]; + } +} + +- (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically]; + } else { + [self.textInputTraits setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically]; + } +} + +- (BOOL)enablesReturnKeyAutomatically +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView enablesReturnKeyAutomatically]; + } else { + return [self.textInputTraits enablesReturnKeyAutomatically]; + } +} + +- (void)setKeyboardAppearance:(UIKeyboardAppearance)setKeyboardAppearance +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setKeyboardAppearance:setKeyboardAppearance]; + } else { + [self.textInputTraits setKeyboardAppearance:setKeyboardAppearance]; + } +} + +- (UIKeyboardAppearance)keyboardAppearance +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView keyboardAppearance]; + } else { + return [self.textInputTraits keyboardAppearance]; + } +} + +- (void)setKeyboardType:(UIKeyboardType)keyboardType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setKeyboardType:keyboardType]; + } else { + [self.textInputTraits setKeyboardType:keyboardType]; + } +} + +- (UIKeyboardType)keyboardType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView keyboardType]; + } else { + return [self.textInputTraits keyboardType]; + } +} + +- (void)setReturnKeyType:(UIReturnKeyType)returnKeyType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setReturnKeyType:returnKeyType]; + } else { + [self.textInputTraits setReturnKeyType:returnKeyType]; + } +} + +- (UIReturnKeyType)returnKeyType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView returnKeyType]; + } else { + return [self.textInputTraits returnKeyType]; + } +} + +- (void)setSecureTextEntry:(BOOL)secureTextEntry +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setSecureTextEntry:secureTextEntry]; + } else { + [self.textInputTraits setSecureTextEntry:secureTextEntry]; + } +} + +- (BOOL)isSecureTextEntry +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView isSecureTextEntry]; + } else { + return [self.textInputTraits isSecureTextEntry]; + } +} + +#pragma mark - UITextView Delegate +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +{ + // Delegateify. + return [self _delegateShouldBeginEditing]; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + // Delegateify. + [self _delegateDidBeginEditing]; +} + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if (_isPreservingText) { + return false; + } + // Delegateify. + return [self _delegateShouldChangeTextInRange:range replacementText:text]; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + AS::MutexLocker l(_textKitLock); + + // Note we received a text changed event. + // This is used by _delegateDidChangeSelectionFromSelectedRange:toSelectedRange: to distinguish between selection changes that happen because of editing or pure selection changes. + _selectionChangedForEditedText = YES; + + // Update if the placeholder is visible. + [self _updateDisplayingPlaceholder]; + + // Invalidate, as our calculated size depends on the textview's seeded text. + [self invalidateCalculatedLayout]; + + // Delegateify. + [self _delegateDidUpdateText]; +} + +- (void)textViewDidChangeSelection:(UITextView *)textView +{ + // Typing attributes get reset when selection changes. Reapply them so they actually obey our header. + _textKitComponents.textView.typingAttributes = _typingAttributes; + + // If we're only changing selection to preserve it, don't notify about anything. + if (_isPreservingSelection) + return; + + // Note if we receive a -textDidChange: between now and when we delegatify. + // This is used by _delegateDidChangeSelectionFromSelectedRange:toSelectedRange: to distinguish between selection changes that happen because of editing or pure selection changes. + _selectionChangedForEditedText = NO; + + NSRange fromSelectedRange = _previousSelectedRange; + NSRange toSelectedRange = self.selectedRange; + _previousSelectedRange = toSelectedRange; + + // Delegateify. + [self _delegateDidChangeSelectionFromSelectedRange:fromSelectedRange toSelectedRange:toSelectedRange]; +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + // Delegateify. + [self _delegateDidFinishEditing]; +} + +#pragma mark - NSLayoutManager Delegate + +- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange +{ + return [_wordKerner layoutManager:layoutManager shouldGenerateGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange]; +} + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex +{ + return [_wordKerner layoutManager:layoutManager shouldUseAction:defaultAction forControlCharacterAtIndex:characterIndex]; +} + +- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex +{ + return [_wordKerner layoutManager:layoutManager boundingBoxForControlGlyphAtIndex:glyphIndex forTextContainer:textContainer proposedLineFragment:proposedRect glyphPosition:glyphPosition characterIndex:characterIndex]; +} + +- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect:(inout CGRect *)lineFragmentRect lineFragmentUsedRect:(inout CGRect *)lineFragmentUsedRect baselineOffset:(inout CGFloat *)baselineOffset inTextContainer:(NSTextContainer *)textContainer forGlyphRange:(NSRange)glyphRange { + CGFloat fontLineHeight; + UIFont *baseFont = _baseFont; + if (_typingAttributes[NSFontAttributeName] != nil) { + baseFont = _typingAttributes[NSFontAttributeName]; + } + if (baseFont == nil) { + fontLineHeight = 20.0; + } else { + CGFloat fontAscent = baseFont.ascender; + CGFloat fontDescent = ABS(baseFont.descender); + fontLineHeight = floor(fontAscent + fontDescent); + } + CGFloat lineHeight = fontLineHeight * 1.0; + CGFloat baselineNudge = (lineHeight - fontLineHeight) * 0.6f; + + CGRect rect = *lineFragmentRect; + rect.size.height = lineHeight; + + CGRect usedRect = *lineFragmentUsedRect; + usedRect.size.height = MAX(lineHeight, usedRect.size.height); + + *lineFragmentRect = rect; + *lineFragmentUsedRect = usedRect; + *baselineOffset = *baselineOffset + baselineNudge; + + return true; +} + +#pragma mark - Geometry +- (CGRect)frameForTextRange:(NSRange)textRange +{ + AS::MutexLocker l(_textKitLock); + + // Bail on invalid range. + if (NSMaxRange(textRange) > [_textKitComponents.textStorage length]) { + ASDisplayNodeAssert(NO, @"Invalid range"); + return CGRectZero; + } + + // Force glyph generation and layout. + [_textKitComponents.layoutManager ensureLayoutForTextContainer:_textKitComponents.textContainer]; + + NSRange glyphRange = [_textKitComponents.layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + CGRect textRect = [_textKitComponents.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textKitComponents.textContainer]; + return [_textKitComponents.textView convertRect:textRect toView:self.view]; +} + +#pragma mark - +- (BOOL)_delegateShouldBeginEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeShouldBeginEditing:)]) { + return [_delegate editableTextNodeShouldBeginEditing:self]; + } + return YES; +} + +- (void)_delegateDidBeginEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeDidBeginEditing:)]) + [_delegate editableTextNodeDidBeginEditing:self]; +} + +- (BOOL)_delegateShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if ([_delegate respondsToSelector:@selector(editableTextNode:shouldChangeTextInRange:replacementText:)]) { + return [_delegate editableTextNode:self shouldChangeTextInRange:range replacementText:text]; + } + + return YES; +} + +- (void)_delegateDidChangeSelectionFromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange +{ + // There are two reasons we're invoking the delegate on the next run of the runloop. + // 1. UITextView invokes its delegate methods when it's in the middle of text-processing. For example, -textViewDidChange: is invoked before you can truly rely on the changes being propagated throughout the Text Kit hierarchy. + // 2. This delegate method (-textViewDidChangeSelection:) is called both before -textViewDidChange: and before the layout manager/etc. has necessarily generated+laid out its glyphs. Because of the former, we need to wait until -textViewDidChange: has had an opportunity to be called so can accurately determine whether this selection change is due to editing (_selectionChangedForEditedText). + // Thus, to avoid calling out to client code in the middle of UITextView's processing, we call the delegate on the next run of the runloop, when all such internal processing is surely done. + dispatch_async(dispatch_get_main_queue(), ^{ + if ([_delegate respondsToSelector:@selector(editableTextNodeDidChangeSelection:fromSelectedRange:toSelectedRange:dueToEditing:)]) + [_delegate editableTextNodeDidChangeSelection:self fromSelectedRange:fromSelectedRange toSelectedRange:toSelectedRange dueToEditing:_selectionChangedForEditedText]; + }); +} + +- (void)_delegateDidUpdateText +{ + // Note that because -editableTextNodeDidUpdateText: passes no state, the current state of the receiver will be accessed. Thus, it's not useful to enqueue a second delegation call if the first hasn't happened yet -- doing so will result in the delegate receiving -editableTextNodeDidUpdateText: when the "updated text" has already been processed. This may sound innocuous, but because our delegation may cause additional updates to the textview's string, and because such updates discard spelling suggestions and autocompletions (like double-space to `.`), it can actually be quite dangerous! + if (_delegateDidUpdateEnqueued) + return; + + _delegateDidUpdateEnqueued = YES; + + // UITextView invokes its delegate methods when it's in the middle of text-processing. For example, -textViewDidChange: is invoked before you can truly rely on the changes being propagated throughout the Text Kit hierarchy. + // Thus, to avoid calling out to client code in the middle of UITextView's processing, we call the delegate on the next run of the runloop, when all such internal processing is surely done. + dispatch_async(dispatch_get_main_queue(), ^{ + _delegateDidUpdateEnqueued = NO; + if ([_delegate respondsToSelector:@selector(editableTextNodeDidUpdateText:)]) + [_delegate editableTextNodeDidUpdateText:self]; + }); +} + +- (void)_delegateDidFinishEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeDidFinishEditing:)]) + [_delegate editableTextNodeDidFinishEditing:self]; +} + +#pragma mark - UIAccessibilityContainer + +- (NSInteger)accessibilityElementCount +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElementCount since ASEditableTextNode is not loaded"); + return 0; + } + return 1; +} + +- (NSArray *)accessibilityElements +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since ASEditableTextNode is not loaded"); + return @[]; + } + return @[self.textView]; +} + +- (id)accessibilityElementAtIndex:(NSInteger)index +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElementAtIndex: since ASEditableTextNode is not loaded"); + return nil; + } + return self.textView; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element +{ + return 0; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.h b/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.h new file mode 100644 index 0000000000..c3d56eb7d8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.h @@ -0,0 +1,43 @@ +// +// ASExperimentalFeatures.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A bit mask of features. Make sure to update configuration.json when you add entries. + */ +typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { + ASExperimentalGraphicsContexts = 1 << 0, // exp_graphics_contexts + // If AS_ENABLE_TEXTNODE=0 or TextNode2 subspec is used this setting is a no op and ASTextNode2 + // will be used in all cases + ASExperimentalTextNode = 1 << 1, // exp_text_node + ASExperimentalInterfaceStateCoalescing = 1 << 2, // exp_interface_state_coalesce + ASExperimentalUnfairLock = 1 << 3, // exp_unfair_lock + ASExperimentalLayerDefaults = 1 << 4, // exp_infer_layer_defaults + ASExperimentalCollectionTeardown = 1 << 5, // exp_collection_teardown + ASExperimentalFramesetterCache = 1 << 6, // exp_framesetter_cache + ASExperimentalSkipClearData = 1 << 7, // exp_skip_clear_data + ASExperimentalDidEnterPreloadSkipASMLayout = 1 << 8, // exp_did_enter_preload_skip_asm_layout + ASExperimentalDisableAccessibilityCache = 1 << 9, // exp_disable_a11y_cache + ASExperimentalDispatchApply = 1 << 10, // exp_dispatch_apply + ASExperimentalImageDownloaderPriority = 1 << 11, // exp_image_downloader_priority + ASExperimentalTextDrawing = 1 << 12, // exp_text_drawing + ASExperimentalFeatureAll = 0xFFFFFFFF +}; + +/// Convert flags -> name array. +AS_EXTERN NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags); + +/// Convert name array -> flags. +AS_EXTERN ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray *array); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm b/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm new file mode 100644 index 0000000000..653db2c370 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm @@ -0,0 +1,52 @@ +// +// ASExperimentalFeatures.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags) +{ + NSArray *allNames = ASCreateOnce((@[@"exp_graphics_contexts", + @"exp_text_node", + @"exp_interface_state_coalesce", + @"exp_unfair_lock", + @"exp_infer_layer_defaults", + @"exp_collection_teardown", + @"exp_framesetter_cache", + @"exp_skip_clear_data", + @"exp_did_enter_preload_skip_asm_layout", + @"exp_disable_a11y_cache", + @"exp_dispatch_apply", + @"exp_image_downloader_priority", + @"exp_text_drawing"])); + if (flags == ASExperimentalFeatureAll) { + return allNames; + } + + // Go through all names, testing each bit. + NSUInteger i = 0; + return ASArrayByFlatMapping(allNames, NSString *name, ({ + (flags & (1 << i++)) ? name : nil; + })); +} + +// O(N^2) but with counts this small, it's probably faster +// than hashing the strings. +ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray *array) +{ + NSArray *allNames = ASExperimentalFeaturesGetNames(ASExperimentalFeatureAll); + ASExperimentalFeatures result = 0; + for (NSString *str in array) { + NSUInteger i = [allNames indexOfObject:str]; + if (i != NSNotFound) { + result |= (1 << i); + } + } + return result; +} diff --git a/submodules/AsyncDisplayKit/Source/ASImageNode+AnimatedImage.mm b/submodules/AsyncDisplayKit/Source/ASImageNode+AnimatedImage.mm new file mode 100644 index 0000000000..7e4ccc2d10 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASImageNode+AnimatedImage.mm @@ -0,0 +1,416 @@ +// +// ASImageNode+AnimatedImage.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#define ASAnimatedImageDebug 0 + +#ifndef MINIMAL_ASDK +@interface ASNetworkImageNode (Private) +- (void)_locked_setDefaultImage:(UIImage *)image; +@end +#endif + + +@implementation ASImageNode (AnimatedImage) + +#pragma mark - GIF support + +- (void)setAnimatedImage:(id )animatedImage +{ + ASLockScopeSelf(); + [self _locked_setAnimatedImage:animatedImage]; +} + +- (void)_locked_setAnimatedImage:(id )animatedImage +{ + ASAssertLocked(__instanceLock__); + + if (ASObjectIsEqual(_animatedImage, animatedImage) && (animatedImage == nil || animatedImage.playbackReady)) { + return; + } + + __block id previousAnimatedImage = _animatedImage; + + _animatedImage = animatedImage; + + if (animatedImage != nil) { + __weak ASImageNode *weakSelf = self; + if ([animatedImage respondsToSelector:@selector(setCoverImageReadyCallback:)]) { + animatedImage.coverImageReadyCallback = ^(UIImage *coverImage) { + // In this case the lock is already gone we have to call the unlocked version therefore + [weakSelf setCoverImageCompleted:coverImage]; + }; + } + + animatedImage.playbackReadyCallback = ^{ + // In this case the lock is already gone we have to call the unlocked version therefore + [weakSelf setShouldAnimate:YES]; + }; + if (animatedImage.playbackReady) { + [self _locked_setShouldAnimate:YES]; + } + } else { + // Clean up after ourselves. + + // Don't bother using a `_locked` version for setting contnst as it should be pretty safe calling it with + // reaquire the lock and would add overhead to introduce this version + self.contents = nil; + [self _locked_setCoverImage:nil]; + } + + // Push calling subclass to the next runloop cycle + // We have to schedule the block on the common modes otherwise the tracking mode will not be included and it will + // not fire e.g. while scrolling down + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^(void) { + [self animatedImageSet:animatedImage previousAnimatedImage:previousAnimatedImage]; + + // Animated image can take while to dealloc, do it off the main queue + if (previousAnimatedImage != nil) { + ASPerformBackgroundDeallocation(&previousAnimatedImage); + } + }); + // Don't need to wakeup the runloop as the current is already running + // CFRunLoopWakeUp(runLoop); // Should not be necessary +} + +- (void)animatedImageSet:(id )newAnimatedImage previousAnimatedImage:(id )previousAnimatedImage +{ + // Subclass hook should not be called with the lock held + ASAssertUnlocked(__instanceLock__); + + // Subclasses may override +} + +- (id )animatedImage +{ + ASLockScopeSelf(); + return _animatedImage; +} + +- (void)setAnimatedImagePaused:(BOOL)animatedImagePaused +{ + ASLockScopeSelf(); + + _animatedImagePaused = animatedImagePaused; + + [self _locked_setShouldAnimate:!animatedImagePaused]; +} + +- (BOOL)animatedImagePaused +{ + ASLockScopeSelf(); + return _animatedImagePaused; +} + +- (void)setCoverImageCompleted:(UIImage *)coverImage +{ + if (ASInterfaceStateIncludesDisplay(self.interfaceState)) { + ASLockScopeSelf(); + [self _locked_setCoverImageCompleted:coverImage]; + } +} + +- (void)_locked_setCoverImageCompleted:(UIImage *)coverImage +{ + ASAssertLocked(__instanceLock__); + + _displayLinkLock.lock(); + BOOL setCoverImage = (_displayLink == nil) || _displayLink.paused; + _displayLinkLock.unlock(); + + if (setCoverImage) { + [self _locked_setCoverImage:coverImage]; + } +} + +- (void)setCoverImage:(UIImage *)coverImage +{ + ASLockScopeSelf(); + [self _locked_setCoverImage:coverImage]; +} + +- (void)_locked_setCoverImage:(UIImage *)coverImage +{ + ASAssertLocked(__instanceLock__); + + //If we're a network image node, we want to set the default image so + //that it will correctly be restored if it exits the range. +#ifndef MINIMAL_ASDK + if ([self isKindOfClass:[ASNetworkImageNode class]]) { + [(ASNetworkImageNode *)self _locked_setDefaultImage:coverImage]; + } else if (_displayLink == nil || _displayLink.paused == YES) { + [self _locked_setImage:coverImage]; + } +#endif +} + +- (NSString *)animatedImageRunLoopMode +{ + AS::MutexLocker l(_displayLinkLock); + return _animatedImageRunLoopMode; +} + +- (void)setAnimatedImageRunLoopMode:(NSString *)runLoopMode +{ + AS::MutexLocker l(_displayLinkLock); + + if (runLoopMode == nil) { + runLoopMode = ASAnimatedImageDefaultRunLoopMode; + } + + if (_displayLink != nil) { + [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; + } + _animatedImageRunLoopMode = [runLoopMode copy]; +} + +- (void)setShouldAnimate:(BOOL)shouldAnimate +{ + ASLockScopeSelf(); + [self _locked_setShouldAnimate:shouldAnimate]; +} + +- (void)_locked_setShouldAnimate:(BOOL)shouldAnimate +{ + ASAssertLocked(__instanceLock__); + + // This test is explicitly done and not ASPerformBlockOnMainThread as this would perform the block immediately + // on main if called on main thread and we have to call methods locked or unlocked based on which thread we are on + if (ASDisplayNodeThreadIsMain()) { + if (shouldAnimate) { + [self _locked_startAnimating]; + } else { + [self _locked_stopAnimating]; + } + } else { + // We have to dispatch to the main thread and call the regular methods as the lock is already gone if the + // block is called + dispatch_async(dispatch_get_main_queue(), ^{ + if (shouldAnimate) { + [self startAnimating]; + } else { + [self stopAnimating]; + } + }); + } +} + +#pragma mark - Animating + +- (void)startAnimating +{ + ASDisplayNodeAssertMainThread(); + + ASLockScopeSelf(); + [self _locked_startAnimating]; +} + +- (void)_locked_startAnimating +{ + ASAssertLocked(__instanceLock__); + + // It should be safe to call self.interfaceState in this case as it will only grab the lock of the superclass + if (!ASInterfaceStateIncludesVisible(self.interfaceState)) { + return; + } + + if (_animatedImagePaused) { + return; + } + + if (_animatedImage.playbackReady == NO) { + return; + } + +#if ASAnimatedImageDebug + NSLog(@"starting animation: %p", self); +#endif + + // Get frame interval before holding display link lock to avoid deadlock + NSUInteger frameInterval = self.animatedImage.frameInterval; + AS::MutexLocker l(_displayLinkLock); + if (_displayLink == nil) { + _playHead = 0; + _displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)]; + _displayLink.frameInterval = frameInterval; + _lastSuccessfulFrameIndex = NSUIntegerMax; + + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; + } else { + _displayLink.paused = NO; + } +} + +- (void)stopAnimating +{ + ASDisplayNodeAssertMainThread(); + + ASLockScopeSelf(); + [self _locked_stopAnimating]; +} + +- (void)_locked_stopAnimating +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + +#if ASAnimatedImageDebug + NSLog(@"stopping animation: %p", self); +#endif + ASDisplayNodeAssertMainThread(); + AS::MutexLocker l(_displayLinkLock); + _displayLink.paused = YES; + self.lastDisplayLinkFire = 0; + + [_animatedImage clearAnimatedImageCache]; +} + +#pragma mark - ASDisplayNode + +- (void)didEnterVisibleState +{ + ASDisplayNodeAssertMainThread(); + [super didEnterVisibleState]; + + if (self.animatedImage.coverImageReady) { + [self setCoverImage:self.animatedImage.coverImage]; + } + if (self.animatedImage.playbackReady) { + [self startAnimating]; + } +} + +- (void)didExitVisibleState +{ + ASDisplayNodeAssertMainThread(); + [super didExitVisibleState]; + + [self stopAnimating]; +} + +- (void)didExitDisplayState +{ + ASDisplayNodeAssertMainThread(); +#if ASAnimatedImageDebug + NSLog(@"exiting display state: %p", self); +#endif + + // Check to see if we're an animated image before calling super in case someone + // decides they want to clear out the animatedImage itself on exiting the display + // state + BOOL isAnimatedImage = self.animatedImage != nil; + [super didExitDisplayState]; + + // Also clear out the contents we've set to be good citizens, we'll put it back in when we become visible. + if (isAnimatedImage) { + self.contents = nil; + [self setCoverImage:nil]; + } +} + +#pragma mark - Display Link Callbacks + +- (void)displayLinkFired:(CADisplayLink *)displayLink +{ + ASDisplayNodeAssertMainThread(); + + CFTimeInterval timeBetweenLastFire; + if (self.lastDisplayLinkFire == 0) { + timeBetweenLastFire = 0; + } else if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp; + } else { + timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire; + } + self.lastDisplayLinkFire = CACurrentMediaTime(); + + _playHead += timeBetweenLastFire; + + while (_playHead > self.animatedImage.totalDuration) { + // Set playhead to zero to keep from showing different frames on different playthroughs + _playHead = 0; + _playedLoops++; + } + + if (self.animatedImage.loopCount > 0 && _playedLoops >= self.animatedImage.loopCount) { + [self stopAnimating]; + return; + } + + NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead]; + if (frameIndex == _lastSuccessfulFrameIndex) { + return; + } + CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex]; + + if (frameImage == nil) { + //Pause the display link until we get a file ready notification + displayLink.paused = YES; + self.lastDisplayLinkFire = 0; + } else { + self.contents = (__bridge id)frameImage; + _lastSuccessfulFrameIndex = frameIndex; + [self displayDidFinish]; + } +} + +- (NSUInteger)frameIndexAtPlayHeadPosition:(CFTimeInterval)playHead +{ + ASDisplayNodeAssertMainThread(); + NSUInteger frameIndex = 0; + for (NSUInteger durationIndex = 0; durationIndex < self.animatedImage.frameCount; durationIndex++) { + playHead -= [self.animatedImage durationAtIndex:durationIndex]; + if (playHead < 0) { + return frameIndex; + } + frameIndex++; + } + + return frameIndex; +} + +@end + +#pragma mark - ASImageNode(AnimatedImageInvalidation) + +@implementation ASImageNode(AnimatedImageInvalidation) + +- (void)invalidateAnimatedImage +{ + AS::MutexLocker l(_displayLinkLock); +#if ASAnimatedImageDebug + if (_displayLink) { + NSLog(@"invalidating display link"); + } +#endif + [_displayLink invalidate]; + _displayLink = nil; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASImageNode.h b/submodules/AsyncDisplayKit/Source/ASImageNode.h new file mode 100644 index 0000000000..902591c4f6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASImageNode.h @@ -0,0 +1,219 @@ +// +// ASImageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifndef MINIMAL_ASDK +@protocol ASAnimatedImageProtocol; +#endif + +/** + * Image modification block. Use to transform an image before display. + * + * @param image The image to be displayed. + * + * @return A transformed image. + */ +typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image); + + +/** + * @abstract Draws images. + * @discussion Supports cropping, tinting, and arbitrary image modification blocks. + */ +@interface ASImageNode : ASControlNode + +/** + * @abstract The image to display. + * + * @discussion The node will efficiently display stretchable images by using + * the layer's contentsCenter property. Non-stretchable images work too, of + * course. + */ +@property (nullable) UIImage *image; + +/** + @abstract The placeholder color. + */ +@property (nullable, copy) UIColor *placeholderColor; + +/** + * @abstract Indicates whether efficient cropping of the receiver is enabled. + * + * @discussion Defaults to YES. See -setCropEnabled:recropImmediately:inBounds: for more + * information. + */ +@property (getter=isCropEnabled) BOOL cropEnabled; + +/** + * @abstract Indicates that efficient downsizing of backing store should *not* be enabled. + * + * @discussion Defaults to NO. @see ASCroppedImageBackingSizeAndDrawRectInBounds for more + * information. + */ +@property BOOL forceUpscaling; + +@property (nonatomic, assign) BOOL displayWithoutProcessing; + +/** + * @abstract Forces image to be rendered at forcedSize. + * @discussion Defaults to CGSizeZero to indicate that the forcedSize should not be used. + * Setting forcedSize to non-CGSizeZero will force the backing of the layer contents to + * be forcedSize (automatically adjusted for contentsSize). + */ +@property CGSize forcedSize; + +/** + * @abstract Enables or disables efficient cropping. + * + * @param cropEnabled YES to efficiently crop the receiver's contents such that + * contents outside of its bounds are not included; NO otherwise. + * + * @param recropImmediately If the receiver has an image, YES to redisplay the + * receiver immediately; NO otherwise. + * + * @param cropBounds The bounds into which the receiver will be cropped. Useful + * if bounds are to change in response to cropping (but have not yet done so). + * + * @discussion Efficient cropping is only performed when the receiver's view's + * contentMode is UIViewContentModeScaleAspectFill. By default, cropping is + * enabled. The crop alignment may be controlled via cropAlignmentFactor. + */ +- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds; + +/** + * @abstract A value that controls how the receiver's efficient cropping is aligned. + * + * @discussion This value defines a rectangle that is to be featured by the + * receiver. The rectangle is specified as a "unit rectangle," using + * fractions of the source image's width and height, e.g. CGRectMake(0.5, 0, + * 0.5, 1.0) will feature the full right half a photo. If the cropRect is + * empty, the content mode of the receiver will be used to determine its + * dimensions, and only the cropRect's origin will be used for positioning. The + * default value of this property is CGRectMake(0.5, 0.5, 0.0, 0.0). + */ +@property CGRect cropRect; + +/** + * @abstract An optional block which can perform drawing operations on image + * during the display phase. + * + * @discussion Can be used to add image effects (such as rounding, adding + * borders, or other pattern overlays) without extraneous display calls. + */ +@property (nullable) asimagenode_modification_block_t imageModificationBlock; + +/** + * @abstract Marks the receiver as needing display and performs a block after + * display has finished. + * + * @param displayCompletionBlock The block to be performed after display has + * finished. Its `canceled` property will be YES if display was prevented or + * canceled (via displaySuspended); NO otherwise. + * + * @discussion displayCompletionBlock will be performed on the main-thread. If + * `displaySuspended` is YES, `displayCompletionBlock` is will be + * performed immediately and `YES` will be passed for `canceled`. + */ +- (void)setNeedsDisplayWithCompletion:(nullable void (^)(BOOL canceled))displayCompletionBlock; + +#if TARGET_OS_TV +/** + * A bool to track if the current appearance of the node + * is the default focus appearance. + * Exposed here so the category methods can set it. + */ +@property BOOL isDefaultFocusAppearance; +#endif + +@end + +#if TARGET_OS_TV +@interface ASImageNode (tvOS) +@end +#endif + +#ifndef MINIMAL_ASDK +@interface ASImageNode (AnimatedImage) + +/** + * @abstract The animated image to playback + * + * @discussion Set this to an object which conforms to ASAnimatedImageProtocol + * to have the ASImageNode playback an animated image. + * @warning this method should not be overridden, it may not always be called as + * another method is used internally. If you need to know when the animatedImage + * is set, override @c animatedImageSet:previousAnimatedImage: + */ +@property (nullable) id animatedImage; + +/** + * @abstract Pause the playback of an animated image. + * + * @discussion Set to YES to pause playback of an animated image and NO to resume + * playback. + */ +@property BOOL animatedImagePaused; + +/** + * @abstract The runloop mode used to animate the image. + * + * @discussion Defaults to NSRunLoopCommonModes. Another commonly used mode is NSDefaultRunLoopMode. + * Setting NSDefaultRunLoopMode will cause animation to pause while scrolling (if the ASImageNode is + * in a scroll view), which may improve scroll performance in some use cases. + */ +@property (copy) NSString *animatedImageRunLoopMode; + +/** + * @abstract Method called when animated image has been set + * + * @discussion This method is for subclasses to override so they can know if an animated image + * has been set on the node. + */ +- (void)animatedImageSet:(nullable id )newAnimatedImage previousAnimatedImage:(nullable id )previousAnimatedImage ASDISPLAYNODE_REQUIRES_SUPER; + +@end +#endif + +@interface ASImageNode (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock AS_UNAVAILABLE(); + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock AS_UNAVAILABLE(); + +@end + +/** + * @abstract Image modification block that rounds (and optionally adds a border to) an image. + * + * @param borderWidth The width of the round border to draw, or zero if no border is desired. + * @param borderColor What colour border to draw. + * + * @see + * + * @return An ASImageNode image modification block. + */ +AS_EXTERN asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor * _Nullable borderColor); + +/** + * @abstract Image modification block that applies a tint color à la UIImage configured with + * renderingMode set to UIImageRenderingModeAlwaysTemplate. + * + * @param color The color to tint the image. + * + * @see + * + * @return An ASImageNode image modification block. + */ +AS_EXTERN asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASImageNode.mm b/submodules/AsyncDisplayKit/Source/ASImageNode.mm new file mode 100644 index 0000000000..e17597bde9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASImageNode.mm @@ -0,0 +1,784 @@ +// +// ASImageNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// TODO: It would be nice to remove this dependency; it's the only subclass using more than +FrameworkSubclasses.h +#import + +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + +typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); + +@interface ASImageNodeDrawParameters : NSObject { +@package + UIImage *_image; + BOOL _opaque; + CGRect _bounds; + CGFloat _contentsScale; + UIColor *_backgroundColor; + UIViewContentMode _contentMode; + BOOL _cropEnabled; + BOOL _forceUpscaling; + CGSize _forcedSize; + CGRect _cropRect; + CGRect _cropDisplayBounds; + asimagenode_modification_block_t _imageModificationBlock; + ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; + ASImageNodeDrawParametersBlock _didDrawBlock; +} + +@end + +@implementation ASImageNodeDrawParameters + +@end + +/** + * Contains all data that is needed to generate the content bitmap. + */ +@interface ASImageNodeContentsKey : NSObject + +@property (nonatomic) UIImage *image; +@property CGSize backingSize; +@property CGRect imageDrawRect; +@property BOOL isOpaque; +@property (nonatomic, copy) UIColor *backgroundColor; +@property (nonatomic) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext; +@property (nonatomic) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext; +@property (nonatomic) asimagenode_modification_block_t imageModificationBlock; + +@end + +@implementation ASImageNodeContentsKey + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + // Optimization opportunity: The `isKindOfClass` call here could be avoided by not using the NSObject `isEqual:` + // convention and instead using a custom comparison function that assumes all items are heterogeneous. + // However, profiling shows that our entire `isKindOfClass` expression is only ~1/40th of the total + // overheard of our caching, so it's likely not high-impact. + if ([object isKindOfClass:[ASImageNodeContentsKey class]]) { + ASImageNodeContentsKey *other = (ASImageNodeContentsKey *)object; + return [_image isEqual:other.image] + && CGSizeEqualToSize(_backingSize, other.backingSize) + && CGRectEqualToRect(_imageDrawRect, other.imageDrawRect) + && _isOpaque == other.isOpaque + && [_backgroundColor isEqual:other.backgroundColor] + && _willDisplayNodeContentWithRenderingContext == other.willDisplayNodeContentWithRenderingContext + && _didDisplayNodeContentWithRenderingContext == other.didDisplayNodeContentWithRenderingContext + && _imageModificationBlock == other.imageModificationBlock; + } else { + return NO; + } +} + +- (NSUInteger)hash +{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + NSUInteger imageHash; + CGSize backingSize; + CGRect imageDrawRect; + NSInteger isOpaque; + NSUInteger backgroundColorHash; + void *willDisplayNodeContentWithRenderingContext; + void *didDisplayNodeContentWithRenderingContext; + void *imageModificationBlock; +#pragma clang diagnostic pop + } data = { + _image.hash, + _backingSize, + _imageDrawRect, + _isOpaque, + _backgroundColor.hash, + (void *)_willDisplayNodeContentWithRenderingContext, + (void *)_didDisplayNodeContentWithRenderingContext, + (void *)_imageModificationBlock + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end + + +@implementation ASImageNode +{ +@private + UIImage *_image; + ASWeakMapEntry *_weakCacheEntry; // Holds a reference that keeps our contents in cache. + UIColor *_placeholderColor; + + void (^_displayCompletionBlock)(BOOL canceled); + + // Drawing + ASTextNode *_debugLabelNode; + + // Cropping. + BOOL _cropEnabled; // Defaults to YES. + BOOL _forceUpscaling; //Defaults to NO. + CGSize _forcedSize; //Defaults to CGSizeZero, indicating no forced size. + CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0) + CGRect _cropDisplayBounds; // Defaults to CGRectNull +} + +@synthesize image = _image; +@synthesize imageModificationBlock = _imageModificationBlock; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // TODO can this be removed? + self.contentsScale = ASScreenScale(); + self.contentMode = UIViewContentModeScaleAspectFill; + self.opaque = NO; + self.clipsToBounds = YES; + + // If no backgroundColor is set to the image node and it's a subview of UITableViewCell, UITableView is setting + // the opaque value of all subviews to YES if highlighting / selection is happening and does not set it back to the + // initial value. With setting a explicit backgroundColor we can prevent that change. + self.backgroundColor = [UIColor clearColor]; + + _cropEnabled = YES; + _forceUpscaling = NO; + _cropRect = CGRectMake(0.5, 0.5, 0, 0); + _cropDisplayBounds = CGRectNull; + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); +#ifndef MINIMAL_ASDK + _animatedImageRunLoopMode = ASAnimatedImageDefaultRunLoopMode; +#endif + + return self; +} + +- (void)dealloc +{ + // Invalidate all components around animated images +#ifndef MINIMAL_ASDK + [self invalidateAnimatedImage]; +#endif +} + +#pragma mark - Placeholder + +- (UIImage *)placeholderImage +{ + // FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set. + // This would completely eliminate the memory and performance cost of the backing store. + CGSize size = self.calculatedSize; + if ((size.width * size.height) < CGFLOAT_EPSILON) { + return nil; + } + + AS::MutexLocker l(__instanceLock__); + + ASGraphicsBeginImageContextWithOptions(size, NO, 1); + [self.placeholderColor setFill]; + UIRectFill(CGRectMake(0, 0, size.width, size.height)); + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + + return image; +} + +#pragma mark - Layout and Sizing + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + const auto image = ASLockedSelf(_image); + + if (image == nil) { + return [super calculateSizeThatFits:constrainedSize]; + } + + return image.size; +} + +#pragma mark - Setter / Getter + +- (void)setImage:(UIImage *)image +{ + AS::MutexLocker l(__instanceLock__); + [self _locked_setImage:image]; +} + +- (void)_locked_setImage:(UIImage *)image +{ + ASAssertLocked(__instanceLock__); + if (ASObjectIsEqual(_image, image)) { + return; + } + + UIImage *oldImage = _image; + _image = image; + + if (image != nil) { + // We explicitly call setNeedsDisplay in this case, although we know setNeedsDisplay will be called with lock held. + // Therefore we have to be careful in methods that are involved with setNeedsDisplay to not run into a deadlock + [self setNeedsDisplay]; + + if (_displayWithoutProcessing && ASDisplayNodeThreadIsMain()) { + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetResizableContents(self, image); + } else { + self.contents = (id)image.CGImage; + } + return; + } + } else { + self.contents = nil; + } + + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + CGSize oldImageSize = oldImage.size; + BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width + || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(&oldImage); + } +} + +- (UIImage *)image +{ + return ASLockedSelf(_image); +} + +- (UIColor *)placeholderColor +{ + return ASLockedSelf(_placeholderColor); +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) { + _placeholderEnabled = (placeholderColor != nil); + } +} + +#pragma mark - Drawing + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + ASLockScopeSelf(); + + ASImageNodeDrawParameters *drawParameters = [[ASImageNodeDrawParameters alloc] init]; + drawParameters->_image = _image; + drawParameters->_bounds = [self threadSafeBounds]; + drawParameters->_opaque = self.opaque; + drawParameters->_contentsScale = _contentsScaleForDisplay; + drawParameters->_backgroundColor = self.backgroundColor; + drawParameters->_contentMode = self.contentMode; + drawParameters->_cropEnabled = _cropEnabled; + drawParameters->_forceUpscaling = _forceUpscaling; + drawParameters->_forcedSize = _forcedSize; + drawParameters->_cropRect = _cropRect; + drawParameters->_cropDisplayBounds = _cropDisplayBounds; + drawParameters->_imageModificationBlock = _imageModificationBlock; + drawParameters->_willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + drawParameters->_didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + + // Hack for now to retain the weak entry that was created while this drawing happened + drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){ + ASLockScopeSelf(); + _weakCacheEntry = entry; + }; + + return drawParameters; +} + ++ (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled +{ + ASImageNodeDrawParameters *drawParameter = (ASImageNodeDrawParameters *)parameter; + + UIImage *image = drawParameter->_image; + if (image == nil) { + return nil; + } + + if (true) { + return image; + } + + CGRect drawParameterBounds = drawParameter->_bounds; + BOOL forceUpscaling = drawParameter->_forceUpscaling; + CGSize forcedSize = drawParameter->_forcedSize; + BOOL cropEnabled = drawParameter->_cropEnabled; + BOOL isOpaque = drawParameter->_opaque; + UIColor *backgroundColor = drawParameter->_backgroundColor; + UIViewContentMode contentMode = drawParameter->_contentMode; + CGFloat contentsScale = drawParameter->_contentsScale; + CGRect cropDisplayBounds = drawParameter->_cropDisplayBounds; + CGRect cropRect = drawParameter->_cropRect; + asimagenode_modification_block_t imageModificationBlock = drawParameter->_imageModificationBlock; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = drawParameter->_willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = drawParameter->_didDisplayNodeContentWithRenderingContext; + + BOOL hasValidCropBounds = cropEnabled && !CGRectIsEmpty(cropDisplayBounds); + CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds); + + + ASDisplayNodeAssert(contentsScale > 0, @"invalid contentsScale at display time"); + + // if the image is resizable, bail early since the image has likely already been configured + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + if (imageModificationBlock != NULL) { + image = imageModificationBlock(image); + } + return image; + } + + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(std::floor(bounds.size.width * contentsScale), std::floor(bounds.size.height * contentsScale)); + + BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill || + contentMode == UIViewContentModeScaleAspectFit || + contentMode == UIViewContentModeCenter; + + CGSize backingSize = CGSizeZero; + CGRect imageDrawRect = CGRectZero; + + if (boundsSizeInPixels.width * contentsScale < 1.0f || boundsSizeInPixels.height * contentsScale < 1.0f || + imageSizeInPixels.width < 1.0f || imageSizeInPixels.height < 1.0f) { + return nil; + } + + + // If we're not supposed to do any cropping, just decode image at original size + if (!cropEnabled || !contentModeSupported || stretchable) { + backingSize = imageSizeInPixels; + imageDrawRect = (CGRect){.size = backingSize}; + } else { + if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) { + //scale forced size + forcedSize.width *= contentsScale; + forcedSize.height *= contentsScale; + } + ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels, + boundsSizeInPixels, + contentMode, + cropRect, + forceUpscaling, + forcedSize, + &backingSize, + &imageDrawRect); + } + + if (backingSize.width <= 0.0f || backingSize.height <= 0.0f || + imageDrawRect.size.width <= 0.0f || imageDrawRect.size.height <= 0.0f) { + return nil; + } + + ASImageNodeContentsKey *contentsKey = [[ASImageNodeContentsKey alloc] init]; + contentsKey.image = image; + contentsKey.backingSize = backingSize; + contentsKey.imageDrawRect = imageDrawRect; + contentsKey.isOpaque = isOpaque; + contentsKey.backgroundColor = backgroundColor; + contentsKey.willDisplayNodeContentWithRenderingContext = willDisplayNodeContentWithRenderingContext; + contentsKey.didDisplayNodeContentWithRenderingContext = didDisplayNodeContentWithRenderingContext; + contentsKey.imageModificationBlock = imageModificationBlock; + + if (isCancelled()) { + return nil; + } + + ASWeakMapEntry *entry = [self.class contentsForkey:contentsKey + drawParameters:parameter + isCancelled:isCancelled]; + // If nil, we were cancelled. + if (entry == nil) { + return nil; + } + + if (drawParameter->_didDrawBlock) { + drawParameter->_didDrawBlock(entry); + } + + return entry.value; +} + +static ASWeakMap *cache = nil; + ++ (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + static dispatch_once_t onceToken; + static AS::Mutex *cacheLock = nil; + dispatch_once(&onceToken, ^{ + cacheLock = new AS::Mutex(); + }); + + { + AS::MutexLocker l(*cacheLock); + if (!cache) { + cache = [[ASWeakMap alloc] init]; + } + ASWeakMapEntry *entry = [cache entryForKey:key]; + if (entry != nil) { + return entry; + } + } + + // cache miss + UIImage *contents = [self createContentsForkey:key drawParameters:drawParameters isCancelled:isCancelled]; + if (contents == nil) { // If nil, we were cancelled + return nil; + } + + { + AS::MutexLocker l(*cacheLock); + return [cache setObject:contents forKey:key]; + } +} + ++ (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + // The following `ASGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an + // A5 processor for a 400x800 backingSize. + // Check for cancellation before we call it. + if (isCancelled()) { + return nil; + } + + // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds + // will do its rounding on pixel instead of point boundaries + ASGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0); + + BOOL contextIsClean = YES; + + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context && key.willDisplayNodeContentWithRenderingContext) { + key.willDisplayNodeContentWithRenderingContext(context, drawParameters); + contextIsClean = NO; + } + + // if view is opaque, fill the context with background color + if (key.isOpaque && key.backgroundColor) { + [key.backgroundColor setFill]; + UIRectFill({ .size = key.backingSize }); + contextIsClean = NO; + } + + // iOS 9 appears to contain a thread safety regression when drawing the same CGImageRef on + // multiple threads concurrently. In fact, instead of crashing, it appears to deadlock. + // The issue is present in Mac OS X El Capitan and has been seen hanging Pro apps like Adobe Premiere, + // as well as iOS games, and a small number of ASDK apps that provide the same image reference + // to many separate ASImageNodes. A workaround is to set .displaysAsynchronously = NO for the nodes + // that may get the same pointer for a given UI asset image, etc. + // FIXME: We should replace @synchronized here, probably using a global, locked NSMutableSet, and + // only if the object already exists in the set we should create a semaphore to signal waiting threads + // upon removal of the object from the set when the operation completes. + // Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer. + // Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068 + + UIImage *image = key.image; + BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage))); + CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal; + + @synchronized(image) { + [image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1]; + } + + if (context && key.didDisplayNodeContentWithRenderingContext) { + key.didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + + // Check cancellation one last time before forming image. + if (isCancelled()) { + ASGraphicsEndImageContext(); + return nil; + } + + UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); + + if (key.imageModificationBlock) { + result = key.imageModificationBlock(result); + } + + return result; +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + __instanceLock__.lock(); + UIImage *image = _image; + void (^displayCompletionBlock)(BOOL canceled) = _displayCompletionBlock; + BOOL shouldPerformDisplayCompletionBlock = (image && displayCompletionBlock); + + // Clear the ivar now. The block is retained and will be executed shortly. + if (shouldPerformDisplayCompletionBlock) { + _displayCompletionBlock = nil; + } + + BOOL hasDebugLabel = (_debugLabelNode != nil); + __instanceLock__.unlock(); + + // Update the debug label if necessary + if (hasDebugLabel) { + // For debugging purposes we don't care about locking for now + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(std::floor(self.bounds.size.width * self.contentsScale), std::floor(self.bounds.size.height * self.contentsScale)); + CGFloat pixelCountRatio = (imageSizeInPixels.width * imageSizeInPixels.height) / (boundsSizeInPixels.width * boundsSizeInPixels.height); + if (pixelCountRatio != 1.0) { + NSString *scaleString = [NSString stringWithFormat:@"%.2fx", pixelCountRatio]; + _debugLabelNode.attributedText = [[NSAttributedString alloc] initWithString:scaleString attributes:[self debugLabelAttributes]]; + _debugLabelNode.hidden = NO; + } else { + _debugLabelNode.hidden = YES; + _debugLabelNode.attributedText = nil; + } + } + + // If we've got a block to perform after displaying, do it. + if (shouldPerformDisplayCompletionBlock) { + displayCompletionBlock(NO); + } +} + +- (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock +{ + if (self.displaySuspended) { + if (displayCompletionBlock) + displayCompletionBlock(YES); + return; + } + + // Stash the block and call-site queue. We'll invoke it in -displayDidFinish. + { + AS::MutexLocker l(__instanceLock__); + if (_displayCompletionBlock != displayCompletionBlock) { + _displayCompletionBlock = displayCompletionBlock; + } + } + + [self setNeedsDisplay]; +} + +#pragma mark Interface State + +- (void)clearContents +{ + [super clearContents]; + + AS::MutexLocker l(__instanceLock__); + _weakCacheEntry = nil; // release contents from the cache. +} + +#pragma mark - Cropping + +- (BOOL)isCropEnabled +{ + AS::MutexLocker l(__instanceLock__); + return _cropEnabled; +} + +- (void)setCropEnabled:(BOOL)cropEnabled +{ + [self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds]; +} + +- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds +{ + __instanceLock__.lock(); + if (_cropEnabled == cropEnabled) { + __instanceLock__.unlock(); + return; + } + + _cropEnabled = cropEnabled; + _cropDisplayBounds = cropBounds; + + UIImage *image = _image; + __instanceLock__.unlock(); + + // If we have an image to display, display it, respecting our recrop flag. + if (image != nil) { + ASPerformBlockOnMainThread(^{ + if (recropImmediately) + [self displayImmediately]; + else + [self setNeedsDisplay]; + }); + } +} + +- (CGRect)cropRect +{ + AS::MutexLocker l(__instanceLock__); + return _cropRect; +} + +- (void)setCropRect:(CGRect)cropRect +{ + { + AS::MutexLocker l(__instanceLock__); + if (CGRectEqualToRect(_cropRect, cropRect)) { + return; + } + + _cropRect = cropRect; + } + + // TODO: this logic needs to be updated to respect cropRect. + CGSize boundsSize = self.bounds.size; + CGSize imageSize = self.image.size; + + BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height)); + + // Re-display if we need to. + ASPerformBlockOnMainThread(^{ + if (self.nodeLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage) + [self setNeedsDisplay]; + }); +} + +- (BOOL)forceUpscaling +{ + AS::MutexLocker l(__instanceLock__); + return _forceUpscaling; +} + +- (void)setForceUpscaling:(BOOL)forceUpscaling +{ + AS::MutexLocker l(__instanceLock__); + _forceUpscaling = forceUpscaling; +} + +- (CGSize)forcedSize +{ + AS::MutexLocker l(__instanceLock__); + return _forcedSize; +} + +- (void)setForcedSize:(CGSize)forcedSize +{ + AS::MutexLocker l(__instanceLock__); + _forcedSize = forcedSize; +} + +- (asimagenode_modification_block_t)imageModificationBlock +{ + AS::MutexLocker l(__instanceLock__); + return _imageModificationBlock; +} + +- (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock +{ + AS::MutexLocker l(__instanceLock__); + _imageModificationBlock = imageModificationBlock; +} + +#pragma mark - Debug + +- (void)layout +{ + [super layout]; + + if (_debugLabelNode) { + CGSize boundsSize = self.bounds.size; + CGSize debugLabelSize = [_debugLabelNode layoutThatFits:ASSizeRangeMake(CGSizeZero, boundsSize)].size; + CGPoint debugLabelOrigin = CGPointMake(boundsSize.width - debugLabelSize.width, + boundsSize.height - debugLabelSize.height); + _debugLabelNode.frame = (CGRect) {debugLabelOrigin, debugLabelSize}; + } +} + +- (NSDictionary *)debugLabelAttributes +{ + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor redColor] + }; +} + +@end + +#pragma mark - Extras + +asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor) +{ + return ^(UIImage *originalImage) { + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}]; + + // Make the image round + [roundOutline addClip]; + + // Draw the original image + [originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + + // Draw a border on top. + if (borderWidth > 0.0) { + [borderColor setStroke]; + [roundOutline setLineWidth:borderWidth]; + [roundOutline stroke]; + } + + return ASGraphicsGetImageAndEndCurrentContext(); + }; +} + +asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color) +{ + return ^(UIImage *originalImage) { + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + + // Set color and render template + [color setFill]; + UIImage *templateImage = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [templateImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + + UIImage *modifiedImage = ASGraphicsGetImageAndEndCurrentContext(); + + // if the original image was stretchy, keep it stretchy + if (!UIEdgeInsetsEqualToEdgeInsets(originalImage.capInsets, UIEdgeInsetsZero)) { + modifiedImage = [modifiedImage resizableImageWithCapInsets:originalImage.capInsets resizingMode:originalImage.resizingMode]; + } + + return modifiedImage; + }; +} diff --git a/submodules/AsyncDisplayKit/Source/ASImageNode.mm.orig b/submodules/AsyncDisplayKit/Source/ASImageNode.mm.orig new file mode 100644 index 0000000000..0106295a55 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASImageNode.mm.orig @@ -0,0 +1,791 @@ +// +// ASImageNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// TODO: It would be nice to remove this dependency; it's the only subclass using more than +FrameworkSubclasses.h +#import + +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + +typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); + +@interface ASImageNodeDrawParameters : NSObject { +@package + UIImage *_image; + BOOL _opaque; + CGRect _bounds; + CGFloat _contentsScale; + UIColor *_backgroundColor; + UIViewContentMode _contentMode; + BOOL _cropEnabled; + BOOL _forceUpscaling; + CGSize _forcedSize; + CGRect _cropRect; + CGRect _cropDisplayBounds; + asimagenode_modification_block_t _imageModificationBlock; + ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; + ASImageNodeDrawParametersBlock _didDrawBlock; +} + +@end + +@implementation ASImageNodeDrawParameters + +@end + +/** + * Contains all data that is needed to generate the content bitmap. + */ +@interface ASImageNodeContentsKey : NSObject + +@property (nonatomic) UIImage *image; +@property CGSize backingSize; +@property CGRect imageDrawRect; +@property BOOL isOpaque; +@property (nonatomic, copy) UIColor *backgroundColor; +@property (nonatomic) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext; +@property (nonatomic) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext; +@property (nonatomic) asimagenode_modification_block_t imageModificationBlock; + +@end + +@implementation ASImageNodeContentsKey + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + // Optimization opportunity: The `isKindOfClass` call here could be avoided by not using the NSObject `isEqual:` + // convention and instead using a custom comparison function that assumes all items are heterogeneous. + // However, profiling shows that our entire `isKindOfClass` expression is only ~1/40th of the total + // overheard of our caching, so it's likely not high-impact. + if ([object isKindOfClass:[ASImageNodeContentsKey class]]) { + ASImageNodeContentsKey *other = (ASImageNodeContentsKey *)object; + return [_image isEqual:other.image] + && CGSizeEqualToSize(_backingSize, other.backingSize) + && CGRectEqualToRect(_imageDrawRect, other.imageDrawRect) + && _isOpaque == other.isOpaque + && [_backgroundColor isEqual:other.backgroundColor] + && _willDisplayNodeContentWithRenderingContext == other.willDisplayNodeContentWithRenderingContext + && _didDisplayNodeContentWithRenderingContext == other.didDisplayNodeContentWithRenderingContext + && _imageModificationBlock == other.imageModificationBlock; + } else { + return NO; + } +} + +- (NSUInteger)hash +{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + NSUInteger imageHash; + CGSize backingSize; + CGRect imageDrawRect; + NSInteger isOpaque; + NSUInteger backgroundColorHash; + void *willDisplayNodeContentWithRenderingContext; + void *didDisplayNodeContentWithRenderingContext; + void *imageModificationBlock; +#pragma clang diagnostic pop + } data = { + _image.hash, + _backingSize, + _imageDrawRect, + _isOpaque, + _backgroundColor.hash, + (void *)_willDisplayNodeContentWithRenderingContext, + (void *)_didDisplayNodeContentWithRenderingContext, + (void *)_imageModificationBlock + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end + + +@implementation ASImageNode +{ +@private + UIImage *_image; + ASWeakMapEntry *_weakCacheEntry; // Holds a reference that keeps our contents in cache. + UIColor *_placeholderColor; + + void (^_displayCompletionBlock)(BOOL canceled); + + // Drawing + ASTextNode *_debugLabelNode; + + // Cropping. + BOOL _cropEnabled; // Defaults to YES. + BOOL _forceUpscaling; //Defaults to NO. + CGSize _forcedSize; //Defaults to CGSizeZero, indicating no forced size. + CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0) + CGRect _cropDisplayBounds; // Defaults to CGRectNull +} + +@synthesize image = _image; +@synthesize imageModificationBlock = _imageModificationBlock; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // TODO can this be removed? + self.contentsScale = ASScreenScale(); + self.contentMode = UIViewContentModeScaleAspectFill; + self.opaque = NO; + self.clipsToBounds = YES; + + // If no backgroundColor is set to the image node and it's a subview of UITableViewCell, UITableView is setting + // the opaque value of all subviews to YES if highlighting / selection is happening and does not set it back to the + // initial value. With setting a explicit backgroundColor we can prevent that change. + self.backgroundColor = [UIColor clearColor]; + + _cropEnabled = YES; + _forceUpscaling = NO; + _cropRect = CGRectMake(0.5, 0.5, 0, 0); + _cropDisplayBounds = CGRectNull; + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); +#ifndef MINIMAL_ASDK + _animatedImageRunLoopMode = ASAnimatedImageDefaultRunLoopMode; +#endif + + return self; +} + +- (void)dealloc +{ + // Invalidate all components around animated images +#ifndef MINIMAL_ASDK + [self invalidateAnimatedImage]; +#endif +} + +#pragma mark - Placeholder + +- (UIImage *)placeholderImage +{ + // FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set. + // This would completely eliminate the memory and performance cost of the backing store. + CGSize size = self.calculatedSize; + if ((size.width * size.height) < CGFLOAT_EPSILON) { + return nil; + } + + ASDN::MutexLocker l(__instanceLock__); + + ASGraphicsBeginImageContextWithOptions(size, NO, 1); + [self.placeholderColor setFill]; + UIRectFill(CGRectMake(0, 0, size.width, size.height)); + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + + return image; +} + +#pragma mark - Layout and Sizing + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + let image = ASLockedSelf(_image); + + if (image == nil) { + return [super calculateSizeThatFits:constrainedSize]; + } + + return image.size; +} + +#pragma mark - Setter / Getter + +- (void)setImage:(UIImage *)image +{ + ASDN::MutexLocker l(__instanceLock__); + [self _locked_setImage:image]; +} + +- (void)_locked_setImage:(UIImage *)image +{ + ASAssertLocked(__instanceLock__); + if (ASObjectIsEqual(_image, image)) { + return; + } + + UIImage *oldImage = _image; + _image = image; + + if (image != nil) { + // We explicitly call setNeedsDisplay in this case, although we know setNeedsDisplay will be called with lock held. + // Therefore we have to be careful in methods that are involved with setNeedsDisplay to not run into a deadlock + [self setNeedsDisplay]; + +<<<<<<< HEAD + if (_displayWithoutProcessing && ASDisplayNodeThreadIsMain()) { + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetupLayerContentsWithResizableImage(self.layer, image); + } else { + self.contents = (id)image.CGImage; + } + return; +======= + // For debugging purposes we don't care about locking for now + if ([ASImageNode shouldShowImageScalingOverlay] && _debugLabelNode == nil) { + // do not use ASPerformBlockOnMainThread here, if it performs the block synchronously it will continue + // holding the lock while calling addSubnode. + dispatch_async(dispatch_get_main_queue(), ^{ + _debugLabelNode = [[ASTextNode alloc] init]; + _debugLabelNode.layerBacked = YES; + [self addSubnode:_debugLabelNode]; + }); +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + } + } else { + self.contents = nil; + } + + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + CGSize oldImageSize = oldImage.size; + BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width + || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(&oldImage); + } +} + +- (UIImage *)image +{ + return ASLockedSelf(_image); +} + +- (UIColor *)placeholderColor +{ + return ASLockedSelf(_placeholderColor); +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) { + _placeholderEnabled = (placeholderColor != nil); + } +} + +#pragma mark - Drawing + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + ASLockScopeSelf(); + + ASImageNodeDrawParameters *drawParameters = [[ASImageNodeDrawParameters alloc] init]; + drawParameters->_image = _image; + drawParameters->_bounds = [self threadSafeBounds]; + drawParameters->_opaque = self.opaque; + drawParameters->_contentsScale = _contentsScaleForDisplay; + drawParameters->_backgroundColor = self.backgroundColor; + drawParameters->_contentMode = self.contentMode; + drawParameters->_cropEnabled = _cropEnabled; + drawParameters->_forceUpscaling = _forceUpscaling; + drawParameters->_forcedSize = _forcedSize; + drawParameters->_cropRect = _cropRect; + drawParameters->_cropDisplayBounds = _cropDisplayBounds; + drawParameters->_imageModificationBlock = _imageModificationBlock; + drawParameters->_willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + drawParameters->_didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + + // Hack for now to retain the weak entry that was created while this drawing happened + drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){ + ASLockScopeSelf(); + _weakCacheEntry = entry; + }; + + return drawParameters; +} + ++ (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled +{ + ASImageNodeDrawParameters *drawParameter = (ASImageNodeDrawParameters *)parameter; + + UIImage *image = drawParameter->_image; + if (image == nil) { + return nil; + } + + if (true) { + return image; + } + + CGRect drawParameterBounds = drawParameter->_bounds; + BOOL forceUpscaling = drawParameter->_forceUpscaling; + CGSize forcedSize = drawParameter->_forcedSize; + BOOL cropEnabled = drawParameter->_cropEnabled; + BOOL isOpaque = drawParameter->_opaque; + UIColor *backgroundColor = drawParameter->_backgroundColor; + UIViewContentMode contentMode = drawParameter->_contentMode; + CGFloat contentsScale = drawParameter->_contentsScale; + CGRect cropDisplayBounds = drawParameter->_cropDisplayBounds; + CGRect cropRect = drawParameter->_cropRect; + asimagenode_modification_block_t imageModificationBlock = drawParameter->_imageModificationBlock; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = drawParameter->_willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = drawParameter->_didDisplayNodeContentWithRenderingContext; + + BOOL hasValidCropBounds = cropEnabled && !CGRectIsEmpty(cropDisplayBounds); + CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds); + + + ASDisplayNodeAssert(contentsScale > 0, @"invalid contentsScale at display time"); + + // if the image is resizable, bail early since the image has likely already been configured + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + if (imageModificationBlock != NULL) { + image = imageModificationBlock(image); + } + return image; + } + + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(std::floor(bounds.size.width * contentsScale), std::floor(bounds.size.height * contentsScale)); + + BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill || + contentMode == UIViewContentModeScaleAspectFit || + contentMode == UIViewContentModeCenter; + + CGSize backingSize = CGSizeZero; + CGRect imageDrawRect = CGRectZero; + + if (boundsSizeInPixels.width * contentsScale < 1.0f || boundsSizeInPixels.height * contentsScale < 1.0f || + imageSizeInPixels.width < 1.0f || imageSizeInPixels.height < 1.0f) { + return nil; + } + + + // If we're not supposed to do any cropping, just decode image at original size + if (!cropEnabled || !contentModeSupported || stretchable) { + backingSize = imageSizeInPixels; + imageDrawRect = (CGRect){.size = backingSize}; + } else { + if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) { + //scale forced size + forcedSize.width *= contentsScale; + forcedSize.height *= contentsScale; + } + ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels, + boundsSizeInPixels, + contentMode, + cropRect, + forceUpscaling, + forcedSize, + &backingSize, + &imageDrawRect); + } + + if (backingSize.width <= 0.0f || backingSize.height <= 0.0f || + imageDrawRect.size.width <= 0.0f || imageDrawRect.size.height <= 0.0f) { + return nil; + } + + ASImageNodeContentsKey *contentsKey = [[ASImageNodeContentsKey alloc] init]; + contentsKey.image = image; + contentsKey.backingSize = backingSize; + contentsKey.imageDrawRect = imageDrawRect; + contentsKey.isOpaque = isOpaque; + contentsKey.backgroundColor = backgroundColor; + contentsKey.willDisplayNodeContentWithRenderingContext = willDisplayNodeContentWithRenderingContext; + contentsKey.didDisplayNodeContentWithRenderingContext = didDisplayNodeContentWithRenderingContext; + contentsKey.imageModificationBlock = imageModificationBlock; + + if (isCancelled()) { + return nil; + } + + ASWeakMapEntry *entry = [self.class contentsForkey:contentsKey + drawParameters:parameter + isCancelled:isCancelled]; + // If nil, we were cancelled. + if (entry == nil) { + return nil; + } + + if (drawParameter->_didDrawBlock) { + drawParameter->_didDrawBlock(entry); + } + + return entry.value; +} + +static ASWeakMap *cache = nil; +// Allocate cacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& cacheLock = *new ASDN::StaticMutex; + ++ (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + { + ASDN::StaticMutexLocker l(cacheLock); + if (!cache) { + cache = [[ASWeakMap alloc] init]; + } + ASWeakMapEntry *entry = [cache entryForKey:key]; + if (entry != nil) { + return entry; + } + } + + // cache miss + UIImage *contents = [self createContentsForkey:key drawParameters:drawParameters isCancelled:isCancelled]; + if (contents == nil) { // If nil, we were cancelled + return nil; + } + + { + ASDN::StaticMutexLocker l(cacheLock); + return [cache setObject:contents forKey:key]; + } +} + ++ (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + // The following `ASGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an + // A5 processor for a 400x800 backingSize. + // Check for cancellation before we call it. + if (isCancelled()) { + return nil; + } + + // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds + // will do its rounding on pixel instead of point boundaries + ASGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0); + + BOOL contextIsClean = YES; + + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context && key.willDisplayNodeContentWithRenderingContext) { + key.willDisplayNodeContentWithRenderingContext(context, drawParameters); + contextIsClean = NO; + } + + // if view is opaque, fill the context with background color + if (key.isOpaque && key.backgroundColor) { + [key.backgroundColor setFill]; + UIRectFill({ .size = key.backingSize }); + contextIsClean = NO; + } + + // iOS 9 appears to contain a thread safety regression when drawing the same CGImageRef on + // multiple threads concurrently. In fact, instead of crashing, it appears to deadlock. + // The issue is present in Mac OS X El Capitan and has been seen hanging Pro apps like Adobe Premiere, + // as well as iOS games, and a small number of ASDK apps that provide the same image reference + // to many separate ASImageNodes. A workaround is to set .displaysAsynchronously = NO for the nodes + // that may get the same pointer for a given UI asset image, etc. + // FIXME: We should replace @synchronized here, probably using a global, locked NSMutableSet, and + // only if the object already exists in the set we should create a semaphore to signal waiting threads + // upon removal of the object from the set when the operation completes. + // Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer. + // Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068 + + UIImage *image = key.image; + BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage))); + CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal; + + @synchronized(image) { + [image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1]; + } + + if (context && key.didDisplayNodeContentWithRenderingContext) { + key.didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + + // Check cancellation one last time before forming image. + if (isCancelled()) { + ASGraphicsEndImageContext(); + return nil; + } + + UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); + + if (key.imageModificationBlock) { + result = key.imageModificationBlock(result); + } + + return result; +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + __instanceLock__.lock(); + void (^displayCompletionBlock)(BOOL canceled) = _displayCompletionBlock; + UIImage *image = _image; + BOOL hasDebugLabel = (_debugLabelNode != nil); + __instanceLock__.unlock(); + + // Update the debug label if necessary + if (hasDebugLabel) { + // For debugging purposes we don't care about locking for now + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(std::floor(self.bounds.size.width * self.contentsScale), std::floor(self.bounds.size.height * self.contentsScale)); + CGFloat pixelCountRatio = (imageSizeInPixels.width * imageSizeInPixels.height) / (boundsSizeInPixels.width * boundsSizeInPixels.height); + if (pixelCountRatio != 1.0) { + NSString *scaleString = [NSString stringWithFormat:@"%.2fx", pixelCountRatio]; + _debugLabelNode.attributedText = [[NSAttributedString alloc] initWithString:scaleString attributes:[self debugLabelAttributes]]; + _debugLabelNode.hidden = NO; + } else { + _debugLabelNode.hidden = YES; + _debugLabelNode.attributedText = nil; + } + } + + // If we've got a block to perform after displaying, do it. + if (image && displayCompletionBlock) { + + displayCompletionBlock(NO); + + __instanceLock__.lock(); + _displayCompletionBlock = nil; + __instanceLock__.unlock(); + } +} + +- (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock +{ + if (self.displaySuspended) { + if (displayCompletionBlock) + displayCompletionBlock(YES); + return; + } + + // Stash the block and call-site queue. We'll invoke it in -displayDidFinish. + { + ASDN::MutexLocker l(__instanceLock__); + if (_displayCompletionBlock != displayCompletionBlock) { + _displayCompletionBlock = displayCompletionBlock; + } + } + + [self setNeedsDisplay]; +} + +#pragma mark Interface State + +- (void)clearContents +{ + [super clearContents]; + + ASDN::MutexLocker l(__instanceLock__); + _weakCacheEntry = nil; // release contents from the cache. +} + +#pragma mark - Cropping + +- (BOOL)isCropEnabled +{ + ASDN::MutexLocker l(__instanceLock__); + return _cropEnabled; +} + +- (void)setCropEnabled:(BOOL)cropEnabled +{ + [self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds]; +} + +- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds +{ + __instanceLock__.lock(); + if (_cropEnabled == cropEnabled) { + __instanceLock__.unlock(); + return; + } + + _cropEnabled = cropEnabled; + _cropDisplayBounds = cropBounds; + + UIImage *image = _image; + __instanceLock__.unlock(); + + // If we have an image to display, display it, respecting our recrop flag. + if (image != nil) { + ASPerformBlockOnMainThread(^{ + if (recropImmediately) + [self displayImmediately]; + else + [self setNeedsDisplay]; + }); + } +} + +- (CGRect)cropRect +{ + ASDN::MutexLocker l(__instanceLock__); + return _cropRect; +} + +- (void)setCropRect:(CGRect)cropRect +{ + { + ASDN::MutexLocker l(__instanceLock__); + if (CGRectEqualToRect(_cropRect, cropRect)) { + return; + } + + _cropRect = cropRect; + } + + // TODO: this logic needs to be updated to respect cropRect. + CGSize boundsSize = self.bounds.size; + CGSize imageSize = self.image.size; + + BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height)); + + // Re-display if we need to. + ASPerformBlockOnMainThread(^{ + if (self.nodeLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage) + [self setNeedsDisplay]; + }); +} + +- (BOOL)forceUpscaling +{ + ASDN::MutexLocker l(__instanceLock__); + return _forceUpscaling; +} + +- (void)setForceUpscaling:(BOOL)forceUpscaling +{ + ASDN::MutexLocker l(__instanceLock__); + _forceUpscaling = forceUpscaling; +} + +- (CGSize)forcedSize +{ + ASDN::MutexLocker l(__instanceLock__); + return _forcedSize; +} + +- (void)setForcedSize:(CGSize)forcedSize +{ + ASDN::MutexLocker l(__instanceLock__); + _forcedSize = forcedSize; +} + +- (asimagenode_modification_block_t)imageModificationBlock +{ + ASDN::MutexLocker l(__instanceLock__); + return _imageModificationBlock; +} + +- (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock +{ + ASDN::MutexLocker l(__instanceLock__); + _imageModificationBlock = imageModificationBlock; +} + +#pragma mark - Debug + +- (void)layout +{ + [super layout]; + + if (_debugLabelNode) { + CGSize boundsSize = self.bounds.size; + CGSize debugLabelSize = [_debugLabelNode layoutThatFits:ASSizeRangeMake(CGSizeZero, boundsSize)].size; + CGPoint debugLabelOrigin = CGPointMake(boundsSize.width - debugLabelSize.width, + boundsSize.height - debugLabelSize.height); + _debugLabelNode.frame = (CGRect) {debugLabelOrigin, debugLabelSize}; + } +} + +- (NSDictionary *)debugLabelAttributes +{ + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor redColor] + }; +} + +@end + +#pragma mark - Extras + +asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor) +{ + return ^(UIImage *originalImage) { + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}]; + + // Make the image round + [roundOutline addClip]; + + // Draw the original image + [originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + + // Draw a border on top. + if (borderWidth > 0.0) { + [borderColor setStroke]; + [roundOutline setLineWidth:borderWidth]; + [roundOutline stroke]; + } + + return ASGraphicsGetImageAndEndCurrentContext(); + }; +} + +asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color) +{ + return ^(UIImage *originalImage) { + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + + // Set color and render template + [color setFill]; + UIImage *templateImage = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [templateImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + + UIImage *modifiedImage = ASGraphicsGetImageAndEndCurrentContext(); + + // if the original image was stretchy, keep it stretchy + if (!UIEdgeInsetsEqualToEdgeInsets(originalImage.capInsets, UIEdgeInsetsZero)) { + modifiedImage = [modifiedImage resizableImageWithCapInsets:originalImage.capInsets resizingMode:originalImage.resizingMode]; + } + + return modifiedImage; + }; +} diff --git a/submodules/AsyncDisplayKit/Source/ASLocking.h b/submodules/AsyncDisplayKit/Source/ASLocking.h new file mode 100644 index 0000000000..3e284dc26c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLocking.h @@ -0,0 +1,158 @@ +// +// ASLocking.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +#define kLockSetCapacity 32 + +/** + * An extension of NSLocking that supports -tryLock. + */ +@protocol ASLocking + +/// Try to take lock without blocking. Returns whether the lock was taken. +- (BOOL)tryLock; + +@end + +/** + * A set of locks acquired during ASLockSequence. + */ +typedef struct { + unsigned count; + CFTypeRef _Nullable locks[kLockSetCapacity]; +} ASLockSet; + +/** + * Declare a lock set that is automatically unlocked at the end of scope. + * + * We use this instead of a scope-locking macro because we want to be able + * to step through the lock sequence block in the debugger. + */ +#define ASScopedLockSet __unused ASLockSet __attribute__((cleanup(ASUnlockSet))) + +/** + * A block that attempts to add a lock to a lock sequence. + * Such a block is provided to the caller of ASLockSequence. + * + * Returns whether the lock was added. You should return + * NO from your lock sequence body if it returns NO. + * + * For instance, you might write `return addLock(l1) && addLock(l2)`. + * + * @param lock The lock to attempt to add. + * @return YES if the lock was added, NO otherwise. + */ +typedef BOOL(^ASAddLockBlock)(id lock); + +/** + * A block that attempts to lock multiple locks in sequence. + * Such a block is provided by the caller of ASLockSequence. + * + * The block may be run multiple times, if not all locks are immediately + * available. Therefore the block should be idempotent. + * + * The block should attempt to invoke addLock multiple times with + * different locks. It should return NO as soon as any addLock + * operation fails. + * + * For instance, you might write `return addLock(l1) && addLock(l2)`. + * + * @param addLock A block you can call to attempt to add a lock. + * @return YES if all locks were added, NO otherwise. + */ +typedef BOOL(^ASLockSequenceBlock)(NS_NOESCAPE ASAddLockBlock addLock); + +/** + * Unlock and release all of the locks in this lock set. + */ +NS_INLINE void ASUnlockSet(ASLockSet *lockSet) { + for (unsigned i = 0; i < lockSet->count; i++) { + CFTypeRef lock = lockSet->locks[i]; + [(__bridge id)lock unlock]; + CFRelease(lock); + } +} + +/** + * Take multiple locks "simultaneously," avoiding deadlocks + * caused by lock ordering. + * + * The block you provide should attempt to take a series of locks, + * using the provided `addLock` block. As soon as any addLock fails, + * you should return NO. + * + * For example: + * ASLockSequence(^(ASAddLockBlock addLock) ^{ + * return addLock(l0) && addLock(l1); + * }); + * + * Note: This function doesn't protect from lock ordering deadlocks if + * one of the locks is already locked (recursive.) Only locks taken + * inside this function are guaranteed not to cause a deadlock. + */ +NS_INLINE ASLockSet ASLockSequence(NS_NOESCAPE ASLockSequenceBlock body) +{ + __block ASLockSet locks = (ASLockSet){0, {}}; + BOOL (^addLock)(id) = ^(id obj) { + + // nil lock = ignore. + if (!obj) { + return YES; + } + + // If they go over capacity, assert and return YES. + // If we return NO, they will enter an infinite loop. + if (locks.count == kLockSetCapacity) { + ASDisplayNodeCFailAssert(@"Locking more than %d locks at once is not supported.", kLockSetCapacity); + return YES; + } + + if ([obj tryLock]) { + locks.locks[locks.count++] = (__bridge_retained CFTypeRef)obj; + return YES; + } + return NO; + }; + + /** + * Repeatedly try running their block, passing in our `addLock` + * until it succeeds. If it fails, unlock all and yield the thread + * to reduce spinning. + */ + while (true) { + if (body(addLock)) { + // Success + return locks; + } else { + ASUnlockSet(&locks); + locks.count = 0; + sched_yield(); + } + } +} + +/** + * These Foundation classes already implement -tryLock. + */ + +@interface NSLock (ASLocking) +@end + +@interface NSRecursiveLock (ASLocking) +@end + +@interface NSConditionLock (ASLocking) +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.h b/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.h new file mode 100644 index 0000000000..391b6bb696 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.h @@ -0,0 +1,56 @@ +// +// ASMainThreadDeallocation.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (ASMainThreadIvarTeardown) + +/** + * Call this from -dealloc to schedule this instance's + * ivars for main thread deallocation as needed. + * + * This method includes a check for whether it's on the main thread, + * and it will do nothing in that case. + */ +- (void)scheduleIvarsForMainThreadDeallocation; + +@end + +@interface NSObject (ASNeedsMainThreadDeallocation) + +/** + * Override this property to indicate that instances of this + * class need to be deallocated on the main thread. + * You do not access this property yourself. + * + * The NSObject implementation returns YES if the class name has + * a prefix UI, AV, or CA. This property is also overridden to + * return fixed values for other common classes, such as UIImage, + * UIGestureRecognizer, and UIResponder. + */ +@property (class, readonly) BOOL needsMainThreadDeallocation; + +@end + +@interface NSProxy (ASNeedsMainThreadDeallocation) + +/** + * Override this property to indicate that instances of this + * class need to be deallocated on the main thread. + * You do not access this property yourself. + * + * The NSProxy implementation returns NO because + * proxies almost always hold weak references. + */ +@property (class, readonly) BOOL needsMainThreadDeallocation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm b/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm new file mode 100644 index 0000000000..1cf90ada05 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm @@ -0,0 +1,206 @@ +// +// ASMainThreadDeallocation.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import + +#import +#import + +@implementation NSObject (ASMainThreadIvarTeardown) + +- (void)scheduleIvarsForMainThreadDeallocation +{ + if (ASDisplayNodeThreadIsMain()) { + return; + } + + NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation]; + + // Unwrap the ivar array + unsigned int count = 0; + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + Ivar ivars[count]; + [ivarsObj getValue:ivars]; + + for (Ivar ivar : ivars) { + id value = object_getIvar(self, ivar); + if (value == nil) { + continue; + } + + if ([object_getClass(value) needsMainThreadDeallocation]) { + as_log_debug(ASMainThreadDeallocationLog(), "%@: Trampolining ivar '%s' value %@ for main deallocation.", self, ivar_getName(ivar), value); + + // Release the ivar's reference before handing the object to the queue so we + // don't risk holding onto it longer than the queue does. + object_setIvar(self, ivar, nil); + + ASPerformMainThreadDeallocation(&value); + } else { + as_log_debug(ASMainThreadDeallocationLog(), "%@: Not trampolining ivar '%s' value %@.", self, ivar_getName(ivar), value); + } + } +} + +/** + * Returns an NSValue-wrapped array of all the ivars in this class or its superclasses + * up through ASDisplayNode, that we expect may need to be deallocated on main. + * + * This method caches its results. + * + * Result is of type NSValue<[Ivar]> + */ ++ (NSValue * _Nonnull)_ivarsThatMayNeedMainDeallocation NS_RETURNS_RETAINED +{ + static NSCache *ivarsCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ivarsCache = [[NSCache alloc] init]; + }); + + NSValue *result = [ivarsCache objectForKey:self]; + if (result != nil) { + return result; + } + + // Cache miss. + unsigned int resultCount = 0; + static const int kMaxDealloc2MainIvarsPerClassTree = 64; + Ivar resultIvars[kMaxDealloc2MainIvarsPerClassTree]; + + // Get superclass results first. + Class c = class_getSuperclass(self); + if (c != [NSObject class]) { + NSValue *ivarsObj = [c _ivarsThatMayNeedMainDeallocation]; + // Unwrap the ivar array and append it to our working array + unsigned int count = 0; + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + ASDisplayNodeCAssert(resultCount + count < kMaxDealloc2MainIvarsPerClassTree, @"More than %d dealloc2main ivars are not supported. Count: %d", kMaxDealloc2MainIvarsPerClassTree, resultCount + count); + [ivarsObj getValue:resultIvars + resultCount]; + resultCount += count; + } + + // Now gather ivars from this particular class. + unsigned int allMyIvarsCount; + Ivar *allMyIvars = class_copyIvarList(self, &allMyIvarsCount); + + for (NSUInteger i = 0; i < allMyIvarsCount; i++) { + Ivar ivar = allMyIvars[i]; + + // NOTE: Would be great to exclude weak/unowned ivars, since we don't + // release them. Unfortunately the objc_ivar_management access is private and + // class_getWeakIvarLayout does not have a well-defined structure. + + const char *type = ivar_getTypeEncoding(ivar); + + if (type != NULL && strcmp(type, @encode(id)) == 0) { + // If it's `id` we have to include it just in case. + resultIvars[resultCount] = ivar; + resultCount += 1; + as_log_verbose(ASMainThreadDeallocationLog(), "%@: Marking ivar '%s' for possible main deallocation due to type id", self, ivar_getName(ivar)); + } else { + // If it's an ivar with a static type, check the type. + Class c = ASGetClassFromType(type); + if ([c needsMainThreadDeallocation]) { + resultIvars[resultCount] = ivar; + resultCount += 1; + as_log_verbose(ASMainThreadDeallocationLog(), "%@: Marking ivar '%s' for main deallocation due to class %@", self, ivar_getName(ivar), c); + } else { + as_log_verbose(ASMainThreadDeallocationLog(), "%@: Skipping ivar '%s' for main deallocation.", self, ivar_getName(ivar)); + } + } + } + free(allMyIvars); + + // Encode the type (array of Ivars) into a string and wrap it in an NSValue + char arrayType[32]; + snprintf(arrayType, 32, "[%u^{objc_ivar}]", resultCount); + result = [NSValue valueWithBytes:resultIvars objCType:arrayType]; + + [ivarsCache setObject:result forKey:self]; + return result; +} + +@end + +@implementation NSObject (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + const auto name = class_getName(self); + if (0 == strncmp(name, "AV", 2) || 0 == strncmp(name, "UI", 2) || 0 == strncmp(name, "CA", 2)) { + return YES; + } + return NO; +} + +@end + +@implementation CALayer (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +@end + +@implementation UIColor (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return NO; +} + +@end + +@implementation UIGestureRecognizer (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +@end + +@implementation UIImage (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return NO; +} + +@end + +@implementation UIResponder (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +@end + +@implementation NSProxy (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return NO; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASMapNode.h b/submodules/AsyncDisplayKit/Source/ASMapNode.h new file mode 100644 index 0000000000..9ddc2b6ffc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMapNode.h @@ -0,0 +1,92 @@ +// +// ASMapNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if TARGET_OS_IOS && AS_USE_MAPKIT +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Map Annotation options. + * The default behavior is to ignore the annotations' positions, use the region or options specified instead. + * Swift: to select the default behavior, use []. + */ +typedef NS_OPTIONS(NSUInteger, ASMapNodeShowAnnotationsOptions) +{ + /** The annotations' positions are ignored, use the region or options specified instead. */ + ASMapNodeShowAnnotationsOptionsIgnored = 0, + /** The annotations' positions are used to calculate the region to show in the map, equivalent to showAnnotations:animated. */ + ASMapNodeShowAnnotationsOptionsZoomed = 1 << 0, + /** This will only have an effect if combined with the Zoomed state with liveMap turned on.*/ + ASMapNodeShowAnnotationsOptionsAnimated = 1 << 1 +}; + +@interface ASMapNode : ASImageNode + +/** + The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change.

This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise).

Changes to the region and camera options will only be animated when when the liveMap mode is enabled, otherwise these options will be applied statically to the new snapshot.

The options object is used to specify properties even when the liveMap mode is enabled, allowing seamless transitions between the snapshot and liveMap (as well as back to the snapshot). + */ +@property (nonatomic) MKMapSnapshotOptions *options; + +/** The region is simply the sub-field on the options object. If the objects object is reset, + this will in effect be overwritten and become the value of the .region property on that object. + Defaults to MKCoordinateRegionForMapRect(MKMapRectWorld). + */ +@property (nonatomic) MKCoordinateRegion region; + +/** + This is the MKMapView that is the live map part of ASMapNode. This will be nil if .liveMap = NO. Note, MKMapView is *not* thread-safe. + */ +@property (nullable, readonly) MKMapView *mapView; + +/** + Set this to YES to turn the snapshot into an interactive MKMapView and vice versa. Defaults to NO. This property may be set on a background thread before the node is loaded, and will automatically be actioned, once the node is loaded. + */ +@property (getter=isLiveMap) BOOL liveMap; + +/** + @abstract Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. + @default Default value is YES. + @discussion If mapSize is set then this will be set to NO, since the size will be the same in all orientations. + */ +@property BOOL needsMapReloadOnBoundsChange; + +/** + Set the delegate of the MKMapView. This can be set even before mapView is created and will be set on the map in the case that the liveMap mode is engaged. + + If the live map view has been created, this may only be set on the main thread. + */ +@property (nonatomic, weak) id mapDelegate; + +/** + * @abstract The annotations to display on the map. + */ +@property (copy) NSArray> *annotations; + +/** + * @abstract This property specifies how to show the annotations. + * @default Default value is ASMapNodeShowAnnotationsIgnored + */ +@property ASMapNodeShowAnnotationsOptions showAnnotationsOptions; + +/** + * @abstract The block which should return annotation image for static map based on provided annotation. + * @discussion This block is executed on an arbitrary serial queue. If this block is nil, standard pin is used. + */ +@property (nullable) UIImage * _Nullable (^imageForStaticMapAnnotationBlock)(id annotation, CGPoint *centerOffset); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASMapNode.h.orig b/submodules/AsyncDisplayKit/Source/ASMapNode.h.orig new file mode 100644 index 0000000000..9ae005aa43 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMapNode.h.orig @@ -0,0 +1,99 @@ +// +// ASMapNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK + +======= +#import +#import + +#if TARGET_OS_IOS && AS_USE_MAPKIT +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Map Annotation options. + * The default behavior is to ignore the annotations' positions, use the region or options specified instead. + * Swift: to select the default behavior, use []. + */ +typedef NS_OPTIONS(NSUInteger, ASMapNodeShowAnnotationsOptions) +{ + /** The annotations' positions are ignored, use the region or options specified instead. */ + ASMapNodeShowAnnotationsOptionsIgnored = 0, + /** The annotations' positions are used to calculate the region to show in the map, equivalent to showAnnotations:animated. */ + ASMapNodeShowAnnotationsOptionsZoomed = 1 << 0, + /** This will only have an effect if combined with the Zoomed state with liveMap turned on.*/ + ASMapNodeShowAnnotationsOptionsAnimated = 1 << 1 +}; + +@interface ASMapNode : ASImageNode + +/** + The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change.

This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise).

Changes to the region and camera options will only be animated when when the liveMap mode is enabled, otherwise these options will be applied statically to the new snapshot.

The options object is used to specify properties even when the liveMap mode is enabled, allowing seamless transitions between the snapshot and liveMap (as well as back to the snapshot). + */ +@property (nonatomic) MKMapSnapshotOptions *options; + +/** The region is simply the sub-field on the options object. If the objects object is reset, + this will in effect be overwritten and become the value of the .region property on that object. + Defaults to MKCoordinateRegionForMapRect(MKMapRectWorld). + */ +@property (nonatomic) MKCoordinateRegion region; + +/** + This is the MKMapView that is the live map part of ASMapNode. This will be nil if .liveMap = NO. Note, MKMapView is *not* thread-safe. + */ +@property (nullable, readonly) MKMapView *mapView; + +/** + Set this to YES to turn the snapshot into an interactive MKMapView and vice versa. Defaults to NO. This property may be set on a background thread before the node is loaded, and will automatically be actioned, once the node is loaded. + */ +@property (getter=isLiveMap) BOOL liveMap; + +/** + @abstract Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. + @default Default value is YES. + @discussion If mapSize is set then this will be set to NO, since the size will be the same in all orientations. + */ +@property BOOL needsMapReloadOnBoundsChange; + +/** + Set the delegate of the MKMapView. This can be set even before mapView is created and will be set on the map in the case that the liveMap mode is engaged. + + If the live map view has been created, this may only be set on the main thread. + */ +@property (nonatomic, weak) id mapDelegate; + +/** + * @abstract The annotations to display on the map. + */ +@property (copy) NSArray> *annotations; + +/** + * @abstract This property specifies how to show the annotations. + * @default Default value is ASMapNodeShowAnnotationsIgnored + */ +@property ASMapNodeShowAnnotationsOptions showAnnotationsOptions; + +/** + * @abstract The block which should return annotation image for static map based on provided annotation. + * @discussion This block is executed on an arbitrary serial queue. If this block is nil, standard pin is used. + */ +@property (nullable) UIImage * _Nullable (^imageForStaticMapAnnotationBlock)(id annotation, CGPoint *centerOffset); + +@end + +NS_ASSUME_NONNULL_END + +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASMapNode.mm b/submodules/AsyncDisplayKit/Source/ASMapNode.mm new file mode 100644 index 0000000000..1c204ab974 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMapNode.mm @@ -0,0 +1,442 @@ +// +// ASMapNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if TARGET_OS_IOS && AS_USE_MAPKIT + +#import + +#import +#import +#import +#import +#import +#import +#import + +@interface ASMapNode() +{ + MKMapSnapshotter *_snapshotter; + BOOL _snapshotAfterLayout; + NSArray *_annotations; +} +@end + +@implementation ASMapNode + +@synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; +@synthesize mapDelegate = _mapDelegate; +@synthesize options = _options; +@synthesize liveMap = _liveMap; +@synthesize showAnnotationsOptions = _showAnnotationsOptions; + +#pragma mark - Lifecycle +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + self.clipsToBounds = YES; + self.userInteractionEnabled = YES; + + _needsMapReloadOnBoundsChange = YES; + _liveMap = NO; + _annotations = @[]; + _showAnnotationsOptions = ASMapNodeShowAnnotationsOptionsIgnored; + return self; +} + +- (void)didLoad +{ + [super didLoad]; + if (self.isLiveMap) { + [self addLiveMap]; + } +} + +- (void)dealloc +{ + [self destroySnapshotter]; +} + +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!self.isLiveMap, @"ASMapNode can not be layer backed whilst .liveMap = YES, set .liveMap = NO to use layer backing."); + [super setLayerBacked:layerBacked]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + ASPerformBlockOnMainThread(^{ + if (self.isLiveMap) { + [self addLiveMap]; + } else { + [self takeSnapshot]; + } + }); +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + ASPerformBlockOnMainThread(^{ + if (self.isLiveMap) { + [self removeLiveMap]; + } + }); +} + +#pragma mark - Settings + +- (BOOL)isLiveMap +{ + ASLockScopeSelf(); + return _liveMap; +} + +- (void)setLiveMap:(BOOL)liveMap +{ + ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature."); + ASLockScopeSelf(); + if (liveMap == _liveMap) { + return; + } + _liveMap = liveMap; + if (self.nodeLoaded) { + liveMap ? [self addLiveMap] : [self removeLiveMap]; + } +} + +- (BOOL)needsMapReloadOnBoundsChange +{ + ASLockScopeSelf(); + return _needsMapReloadOnBoundsChange; +} + +- (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange +{ + ASLockScopeSelf(); + _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; +} + +- (MKMapSnapshotOptions *)options +{ + ASLockScopeSelf(); + if (!_options) { + _options = [[MKMapSnapshotOptions alloc] init]; + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); + CGSize calculatedSize = self.calculatedSize; + if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) { + _options.size = calculatedSize; + } + } + return _options; +} + +- (void)setOptions:(MKMapSnapshotOptions *)options +{ + ASLockScopeSelf(); + if (!_options || ![options isEqual:_options]) { + _options = options; + if (self.isLiveMap) { + [self applySnapshotOptions]; + } else if (_snapshotter) { + [self destroySnapshotter]; + [self takeSnapshot]; + } + } +} + +- (MKCoordinateRegion)region +{ + return self.options.region; +} + +- (void)setRegion:(MKCoordinateRegion)region +{ + MKMapSnapshotOptions * options = [self.options copy]; + options.region = region; + self.options = options; +} + +- (id)mapDelegate +{ + return ASLockedSelf(_mapDelegate); +} + +- (void)setMapDelegate:(id)mapDelegate { + ASLockScopeSelf(); + _mapDelegate = mapDelegate; + + if (_mapView) { + ASDisplayNodeAssertMainThread(); + _mapView.delegate = mapDelegate; + } +} + +#pragma mark - Snapshotter + +- (void)takeSnapshot +{ + // If our size is zero, we want to avoid calling a default sized snapshot. Set _snapshotAfterLayout to YES + // so if layout changes in the future, we'll try snapshotting again. + ASLayout *layout = self.calculatedLayout; + if (layout == nil || CGSizeEqualToSize(CGSizeZero, layout.size)) { + _snapshotAfterLayout = YES; + return; + } + + _snapshotAfterLayout = NO; + + if (!_snapshotter) { + [self setUpSnapshotter]; + } + + if (_snapshotter.isLoading) { + return; + } + + __weak __typeof__(self) weakSelf = self; + [_snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + completionHandler:^(MKMapSnapshot *snapshot, NSError *error) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (!error) { + UIImage *image = snapshot.image; + NSArray *annotations = strongSelf.annotations; + if (annotations.count > 0) { + // Only create a graphics context if we have annotations to draw. + // The MKMapSnapshotter is currently not capable of rendering annotations automatically. + + CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); + + ASGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); + [image drawAtPoint:CGPointZero]; + + UIImage *pinImage; + CGPoint pinCenterOffset = CGPointZero; + + // Get a standard annotation view pin if there is no custom annotation block. + if (!strongSelf.imageForStaticMapAnnotationBlock) { + pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; + } + + for (id annotation in annotations) { + if (strongSelf.imageForStaticMapAnnotationBlock) { + // Get custom annotation image from custom annotation block. + pinImage = strongSelf.imageForStaticMapAnnotationBlock(annotation, &pinCenterOffset); + if (!pinImage) { + // just for case block returned nil, which can happen + pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; + } + } + + CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; + if (CGRectContainsPoint(finalImageRect, point)) { + CGSize pinSize = pinImage.size; + point.x -= pinSize.width / 2.0; + point.y -= pinSize.height / 2.0; + point.x += pinCenterOffset.x; + point.y += pinCenterOffset.y; + [pinImage drawAtPoint:point]; + } + } + + image = ASGraphicsGetImageAndEndCurrentContext(); + } + + strongSelf.image = image; + } + }]; +} + ++ (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset NS_RETURNS_RETAINED +{ + static MKAnnotationView *pin; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; + }); + *centerOffset = pin.centerOffset; + return pin.image; +} + +- (void)setUpSnapshotter +{ + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; +} + +- (void)destroySnapshotter +{ + [_snapshotter cancel]; + _snapshotter = nil; +} + +- (void)applySnapshotOptions +{ + MKMapSnapshotOptions *options = self.options; + [_mapView setCamera:options.camera animated:YES]; + [_mapView setRegion:options.region animated:YES]; + [_mapView setMapType:options.mapType]; + _mapView.showsBuildings = options.showsBuildings; + _mapView.showsPointsOfInterest = options.showsPointsOfInterest; +} + +#pragma mark - Actions +- (void)addLiveMap +{ + ASDisplayNodeAssertMainThread(); + if (!_mapView) { + __weak ASMapNode *weakSelf = self; + _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; + _mapView.delegate = weakSelf.mapDelegate; + [weakSelf applySnapshotOptions]; + [_mapView addAnnotations:_annotations]; + [weakSelf setNeedsLayout]; + [weakSelf.view addSubview:_mapView]; + + ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; + [_mapView showAnnotations:_mapView.annotations animated:animated]; + } + } +} + +- (void)removeLiveMap +{ + [_mapView removeFromSuperview]; + _mapView = nil; +} + +- (NSArray *)annotations +{ + ASLockScopeSelf(); + return _annotations; +} + +- (void)setAnnotations:(NSArray *)annotations +{ + annotations = [annotations copy] ? : @[]; + + ASLockScopeSelf(); + _annotations = annotations; + ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; + if (self.isLiveMap) { + [_mapView removeAnnotations:_mapView.annotations]; + [_mapView addAnnotations:annotations]; + + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; + [_mapView showAnnotations:_mapView.annotations animated:animated]; + } + } else { + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + self.region = [self regionToFitAnnotations:annotations]; + } + else { + [self takeSnapshot]; + } + } +} + +- (MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotations +{ + if([annotations count] == 0) + return MKCoordinateRegionForMapRect(MKMapRectWorld); + + CLLocationCoordinate2D topLeftCoord = CLLocationCoordinate2DMake(-90, 180); + CLLocationCoordinate2D bottomRightCoord = CLLocationCoordinate2DMake(90, -180); + + for (id annotation in annotations) { + topLeftCoord = CLLocationCoordinate2DMake(std::fmax(topLeftCoord.latitude, annotation.coordinate.latitude), + std::fmin(topLeftCoord.longitude, annotation.coordinate.longitude)); + bottomRightCoord = CLLocationCoordinate2DMake(std::fmin(bottomRightCoord.latitude, annotation.coordinate.latitude), + std::fmax(bottomRightCoord.longitude, annotation.coordinate.longitude)); + } + + MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5, + topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5), + MKCoordinateSpanMake(std::fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 2, + std::fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 2)); + + return region; +} + +-(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { + return ASLockedSelf(_showAnnotationsOptions); +} + +-(void)setShowAnnotationsOptions:(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { + ASLockScopeSelf(); + _showAnnotationsOptions = showAnnotationsOptions; +} + +#pragma mark - Layout +- (void)setSnapshotSizeWithReloadIfNeeded:(CGSize)snapshotSize +{ + if (snapshotSize.height > 0 && snapshotSize.width > 0 && !CGSizeEqualToSize(self.options.size, snapshotSize)) { + _options.size = snapshotSize; + if (_snapshotter) { + [self destroySnapshotter]; + [self takeSnapshot]; + } + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // FIXME: Need a better way to allow maps to take up the right amount of space in a layout (sizeRange, etc) + // These fallbacks protect against inheriting a constrainedSize that contains a CGFLOAT_MAX value. + if (!ASIsCGSizeValidForLayout(constrainedSize)) { + //ASDisplayNodeAssert(NO, @"Invalid width or height in ASMapNode"); + constrainedSize = CGSizeZero; + } + [self setSnapshotSizeWithReloadIfNeeded:constrainedSize]; + return constrainedSize; +} + +- (void)calculatedLayoutDidChange +{ + [super calculatedLayoutDidChange]; + + if (_snapshotAfterLayout) { + [self takeSnapshot]; + } +} + +// -layout isn't usually needed over -layoutSpecThatFits, but this way we can avoid a needless node wrapper for MKMapView. +- (void)layout +{ + [super layout]; + if (self.isLiveMap) { + _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); + } else { + // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter. + if (_needsMapReloadOnBoundsChange) { + [self setSnapshotSizeWithReloadIfNeeded:self.bounds.size]; + // FIXME: Adding a check for Preload here seems to cause intermittent map load failures, but shouldn't. + // if (ASInterfaceStateIncludesPreload(self.interfaceState)) { + } + } +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +@end +#endif // TARGET_OS_IOS && AS_USE_MAPKIT diff --git a/submodules/AsyncDisplayKit/Source/ASMapNode.mm.orig b/submodules/AsyncDisplayKit/Source/ASMapNode.mm.orig new file mode 100644 index 0000000000..1014f2f32c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMapNode.mm.orig @@ -0,0 +1,456 @@ +// +// ASMapNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK + +#import + +#if TARGET_OS_IOS +======= +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +#import + +#if TARGET_OS_IOS && AS_USE_MAPKIT + +#import + +#import +#import +#import +#import +#import +#import +#import + +@interface ASMapNode() +{ + MKMapSnapshotter *_snapshotter; + BOOL _snapshotAfterLayout; + NSArray *_annotations; +} +@end + +@implementation ASMapNode + +@synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; +@synthesize mapDelegate = _mapDelegate; +@synthesize options = _options; +@synthesize liveMap = _liveMap; +@synthesize showAnnotationsOptions = _showAnnotationsOptions; + +#pragma mark - Lifecycle +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + self.clipsToBounds = YES; + self.userInteractionEnabled = YES; + + _needsMapReloadOnBoundsChange = YES; + _liveMap = NO; + _annotations = @[]; + _showAnnotationsOptions = ASMapNodeShowAnnotationsOptionsIgnored; + return self; +} + +- (void)didLoad +{ + [super didLoad]; + if (self.isLiveMap) { + [self addLiveMap]; + } +} + +- (void)dealloc +{ + [self destroySnapshotter]; +} + +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!self.isLiveMap, @"ASMapNode can not be layer backed whilst .liveMap = YES, set .liveMap = NO to use layer backing."); + [super setLayerBacked:layerBacked]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + ASPerformBlockOnMainThread(^{ + if (self.isLiveMap) { + [self addLiveMap]; + } else { + [self takeSnapshot]; + } + }); +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + ASPerformBlockOnMainThread(^{ + if (self.isLiveMap) { + [self removeLiveMap]; + } + }); +} + +#pragma mark - Settings + +- (BOOL)isLiveMap +{ + ASLockScopeSelf(); + return _liveMap; +} + +- (void)setLiveMap:(BOOL)liveMap +{ + ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature."); + ASLockScopeSelf(); + if (liveMap == _liveMap) { + return; + } + _liveMap = liveMap; + if (self.nodeLoaded) { + liveMap ? [self addLiveMap] : [self removeLiveMap]; + } +} + +- (BOOL)needsMapReloadOnBoundsChange +{ + ASLockScopeSelf(); + return _needsMapReloadOnBoundsChange; +} + +- (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange +{ + ASLockScopeSelf(); + _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; +} + +- (MKMapSnapshotOptions *)options +{ + ASLockScopeSelf(); + if (!_options) { + _options = [[MKMapSnapshotOptions alloc] init]; + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); + CGSize calculatedSize = self.calculatedSize; + if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) { + _options.size = calculatedSize; + } + } + return _options; +} + +- (void)setOptions:(MKMapSnapshotOptions *)options +{ + ASLockScopeSelf(); + if (!_options || ![options isEqual:_options]) { + _options = options; + if (self.isLiveMap) { + [self applySnapshotOptions]; + } else if (_snapshotter) { + [self destroySnapshotter]; + [self takeSnapshot]; + } + } +} + +- (MKCoordinateRegion)region +{ + return self.options.region; +} + +- (void)setRegion:(MKCoordinateRegion)region +{ + MKMapSnapshotOptions * options = [self.options copy]; + options.region = region; + self.options = options; +} + +- (id)mapDelegate +{ + return ASLockedSelf(_mapDelegate); +} + +- (void)setMapDelegate:(id)mapDelegate { + ASLockScopeSelf(); + _mapDelegate = mapDelegate; + + if (_mapView) { + ASDisplayNodeAssertMainThread(); + _mapView.delegate = mapDelegate; + } +} + +#pragma mark - Snapshotter + +- (void)takeSnapshot +{ + // If our size is zero, we want to avoid calling a default sized snapshot. Set _snapshotAfterLayout to YES + // so if layout changes in the future, we'll try snapshotting again. + ASLayout *layout = self.calculatedLayout; + if (layout == nil || CGSizeEqualToSize(CGSizeZero, layout.size)) { + _snapshotAfterLayout = YES; + return; + } + + _snapshotAfterLayout = NO; + + if (!_snapshotter) { + [self setUpSnapshotter]; + } + + if (_snapshotter.isLoading) { + return; + } + + __weak __typeof__(self) weakSelf = self; + [_snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + completionHandler:^(MKMapSnapshot *snapshot, NSError *error) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (!error) { + UIImage *image = snapshot.image; + NSArray *annotations = strongSelf.annotations; + if (annotations.count > 0) { + // Only create a graphics context if we have annotations to draw. + // The MKMapSnapshotter is currently not capable of rendering annotations automatically. + + CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); + + ASGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); + [image drawAtPoint:CGPointZero]; + + UIImage *pinImage; + CGPoint pinCenterOffset = CGPointZero; + + // Get a standard annotation view pin if there is no custom annotation block. + if (!strongSelf.imageForStaticMapAnnotationBlock) { + pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; + } + + for (id annotation in annotations) { + if (strongSelf.imageForStaticMapAnnotationBlock) { + // Get custom annotation image from custom annotation block. + pinImage = strongSelf.imageForStaticMapAnnotationBlock(annotation, &pinCenterOffset); + if (!pinImage) { + // just for case block returned nil, which can happen + pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; + } + } + + CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; + if (CGRectContainsPoint(finalImageRect, point)) { + CGSize pinSize = pinImage.size; + point.x -= pinSize.width / 2.0; + point.y -= pinSize.height / 2.0; + point.x += pinCenterOffset.x; + point.y += pinCenterOffset.y; + [pinImage drawAtPoint:point]; + } + } + + image = ASGraphicsGetImageAndEndCurrentContext(); + } + + strongSelf.image = image; + } + }]; +} + ++ (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset NS_RETURNS_RETAINED +{ + static MKAnnotationView *pin; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; + }); + *centerOffset = pin.centerOffset; + return pin.image; +} + +- (void)setUpSnapshotter +{ + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; +} + +- (void)destroySnapshotter +{ + [_snapshotter cancel]; + _snapshotter = nil; +} + +- (void)applySnapshotOptions +{ + MKMapSnapshotOptions *options = self.options; + [_mapView setCamera:options.camera animated:YES]; + [_mapView setRegion:options.region animated:YES]; + [_mapView setMapType:options.mapType]; + _mapView.showsBuildings = options.showsBuildings; + _mapView.showsPointsOfInterest = options.showsPointsOfInterest; +} + +#pragma mark - Actions +- (void)addLiveMap +{ + ASDisplayNodeAssertMainThread(); + if (!_mapView) { + __weak ASMapNode *weakSelf = self; + _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; + _mapView.delegate = weakSelf.mapDelegate; + [weakSelf applySnapshotOptions]; + [_mapView addAnnotations:_annotations]; + [weakSelf setNeedsLayout]; + [weakSelf.view addSubview:_mapView]; + + ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; + [_mapView showAnnotations:_mapView.annotations animated:animated]; + } + } +} + +- (void)removeLiveMap +{ + [_mapView removeFromSuperview]; + _mapView = nil; +} + +- (NSArray *)annotations +{ + ASLockScopeSelf(); + return _annotations; +} + +- (void)setAnnotations:(NSArray *)annotations +{ + annotations = [annotations copy] ? : @[]; + + ASLockScopeSelf(); + _annotations = annotations; + ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; + if (self.isLiveMap) { + [_mapView removeAnnotations:_mapView.annotations]; + [_mapView addAnnotations:annotations]; + + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; + [_mapView showAnnotations:_mapView.annotations animated:animated]; + } + } else { + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + self.region = [self regionToFitAnnotations:annotations]; + } + else { + [self takeSnapshot]; + } + } +} + +- (MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotations +{ + if([annotations count] == 0) + return MKCoordinateRegionForMapRect(MKMapRectWorld); + + CLLocationCoordinate2D topLeftCoord = CLLocationCoordinate2DMake(-90, 180); + CLLocationCoordinate2D bottomRightCoord = CLLocationCoordinate2DMake(90, -180); + + for (id annotation in annotations) { + topLeftCoord = CLLocationCoordinate2DMake(std::fmax(topLeftCoord.latitude, annotation.coordinate.latitude), + std::fmin(topLeftCoord.longitude, annotation.coordinate.longitude)); + bottomRightCoord = CLLocationCoordinate2DMake(std::fmin(bottomRightCoord.latitude, annotation.coordinate.latitude), + std::fmax(bottomRightCoord.longitude, annotation.coordinate.longitude)); + } + + MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5, + topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5), + MKCoordinateSpanMake(std::fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 2, + std::fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 2)); + + return region; +} + +-(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { + return ASLockedSelf(_showAnnotationsOptions); +} + +-(void)setShowAnnotationsOptions:(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { + ASLockScopeSelf(); + _showAnnotationsOptions = showAnnotationsOptions; +} + +#pragma mark - Layout +- (void)setSnapshotSizeWithReloadIfNeeded:(CGSize)snapshotSize +{ + if (snapshotSize.height > 0 && snapshotSize.width > 0 && !CGSizeEqualToSize(self.options.size, snapshotSize)) { + _options.size = snapshotSize; + if (_snapshotter) { + [self destroySnapshotter]; + [self takeSnapshot]; + } + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // FIXME: Need a better way to allow maps to take up the right amount of space in a layout (sizeRange, etc) + // These fallbacks protect against inheriting a constrainedSize that contains a CGFLOAT_MAX value. + if (!ASIsCGSizeValidForLayout(constrainedSize)) { + //ASDisplayNodeAssert(NO, @"Invalid width or height in ASMapNode"); + constrainedSize = CGSizeZero; + } + [self setSnapshotSizeWithReloadIfNeeded:constrainedSize]; + return constrainedSize; +} + +- (void)calculatedLayoutDidChange +{ + [super calculatedLayoutDidChange]; + + if (_snapshotAfterLayout) { + [self takeSnapshot]; + } +} + +// -layout isn't usually needed over -layoutSpecThatFits, but this way we can avoid a needless node wrapper for MKMapView. +- (void)layout +{ + [super layout]; + if (self.isLiveMap) { + _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); + } else { + // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter. + if (_needsMapReloadOnBoundsChange) { + [self setSnapshotSizeWithReloadIfNeeded:self.bounds.size]; + // FIXME: Adding a check for Preload here seems to cause intermittent map load failures, but shouldn't. + // if (ASInterfaceStateIncludesPreload(self.interfaceState)) { + } + } +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +@end +<<<<<<< HEAD +#endif + +#endif +======= +#endif // TARGET_OS_IOS && AS_USE_MAPKIT +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e diff --git a/submodules/AsyncDisplayKit/Source/ASMultiplexImageNode.h b/submodules/AsyncDisplayKit/Source/ASMultiplexImageNode.h new file mode 100644 index 0000000000..42342422a9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMultiplexImageNode.h @@ -0,0 +1,271 @@ +// +// ASMultiplexImageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASMultiplexImageNodeDelegate; +@protocol ASMultiplexImageNodeDataSource; + +typedef id ASImageIdentifier; + +AS_EXTERN NSString *const ASMultiplexImageNodeErrorDomain; + +/** + * ASMultiplexImageNode error codes. + */ +typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { + /** + * Indicates that the data source didn't provide a source for an image identifier. + */ + ASMultiplexImageNodeErrorCodeNoSourceForImage = 0, + + /** + * Indicates that the best image identifier changed before a download for a worse identifier began. + */ + ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged, + + /** + * Indicates that the Photos framework returned no image and no error. + * This may happen if the image is in iCloud and the user did not specify `allowsNetworkAccess` + * in their image request. + */ + ASMultiplexImageNodeErrorCodePhotosImageManagerFailedWithoutError, + + /** + * Indicates that the image node could not retrieve the PHAsset for a given asset identifier. + * This typically means that the user has not given Photos framework permissions yet or the asset + * has been removed from the device. + */ + ASMultiplexImageNodeErrorCodePHAssetIsUnavailable +}; + + +/** + * @abstract ASMultiplexImageNode is an image node that can load and display multiple versions of an image. For + * example, it can display a low-resolution version of an image while the high-resolution version is loading. + * + * @discussion ASMultiplexImageNode begins loading images when its resource can either return a UIImage directly, or a URL the image node should load. + */ +@interface ASMultiplexImageNode : ASImageNode + +/** + * @abstract The designated initializer. + * @param cache The object that implements a cache of images for the image node. + * @param downloader The object that implements image downloading for the image node. + * @discussion If `cache` is nil, the receiver will not attempt to retrieve images from a cache before downloading them. + * @return An initialized ASMultiplexImageNode. + */ +- (instancetype)initWithCache:(nullable id)cache downloader:(nullable id)downloader NS_DESIGNATED_INITIALIZER; + +/** + * @abstract The delegate, which must conform to the protocol. + */ +@property (nonatomic, weak) id delegate; + +/** + * @abstract The data source, which must conform to the protocol. + * @discussion This value is required for ASMultiplexImageNode to load images. + */ +@property (nonatomic, weak) id dataSource; + +/** + * @abstract Whether the receiver should download more than just its highest-quality image. Defaults to NO. + * + * @discussion ASMultiplexImageNode immediately loads and displays the first image specified in (its + * highest-quality image). If that image is not immediately available or cached, the node can download and display + * lesser-quality images. Set `downloadsIntermediateImages` to YES to enable this behaviour. + */ +@property (nonatomic) BOOL downloadsIntermediateImages; + +/** + * @abstract An array of identifiers representing various versions of an image for ASMultiplexImageNode to display. + * + * @discussion An identifier can be any object that conforms to NSObject and NSCopying. The array should be in + * decreasing order of image quality -- that is, the first identifier in the array represents the best version. + * + * @see for more information on the image loading process. + */ +@property (nonatomic, copy) NSArray *imageIdentifiers; + +/** + * @abstract Notify the receiver SSAA that its data source has new UIImages or NSURLs available for . + * + * @discussion If a higher-quality image than is currently displayed is now available, it will be loaded. + */ +- (void)reloadImageIdentifierSources; + +/** + * @abstract The identifier for the last image that the receiver loaded, or nil. + * + * @discussion This value may differ from if the image hasn't yet been displayed. + */ +@property (nullable, nonatomic, readonly) ASImageIdentifier loadedImageIdentifier; + +/** + * @abstract The identifier for the image that the receiver is currently displaying, or nil. + */ +@property (nullable, nonatomic, readonly) ASImageIdentifier displayedImageIdentifier; + +/** + * @abstract If the downloader implements progressive image rendering and this value is YES progressive renders of the + * image will be displayed as the image downloads. Regardless of this properties value, progress renders will + * only occur when the node is visible. Defaults to YES. + */ +@property (nonatomic) BOOL shouldRenderProgressImages; + +/** + * @abstract The image manager that this image node should use when requesting images from the Photos framework. If this is `nil` (the default), then `PHImageManager.defaultManager` is used. + + * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. + */ +@property (nullable, nonatomic) PHImageManager *imageManager API_AVAILABLE(ios(8.0), tvos(10.0)); +@end + + +#pragma mark - +/** + * The methods declared by the ASMultiplexImageNodeDelegate protocol allow the adopting delegate to respond to + * notifications such as began, progressed and finished downloading, updated and displayed an image. + */ +@protocol ASMultiplexImageNodeDelegate + +@optional +/** + * @abstract Notification that the image node began downloading an image. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that is downloading. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didStartDownloadOfImageWithIdentifier:(id)imageIdentifier; + +/** + * @abstract Notification that the image node's download progressed. + * @param imageNode The sender. + * @param downloadProgress The progress of the download. Value is between 0.0 and 1.0. + * @param imageIdentifier The identifier for the image that is downloading. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode + didUpdateDownloadProgress:(CGFloat)downloadProgress + forImageWithIdentifier:(ASImageIdentifier)imageIdentifier; + +/** + * @abstract Notification that the image node's download has finished. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that finished downloading. + * @param error The error that occurred while downloading, if one occurred; nil otherwise. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode +didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier + error:(nullable NSError *)error; + +/** + * @abstract Notification that the image node's image was updated. + * @param imageNode The sender. + * @param image The new image, ready for display. + * @param imageIdentifier The identifier for `image`. + * @param previousImage The old, previously-loaded image. + * @param previousImageIdentifier The identifier for `previousImage`. + * @note This method does not indicate that `image` has been displayed. + * @see <[ASMultiplexImageNodeDelegate multiplexImageNode:didDisplayUpdatedImage:withIdentifier:]>. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode + didUpdateImage:(nullable UIImage *)image + withIdentifier:(nullable ASImageIdentifier)imageIdentifier + fromImage:(nullable UIImage *)previousImage + withIdentifier:(nullable ASImageIdentifier)previousImageIdentifier; + +/** + * @abstract Notification that the image node displayed a new image. + * @param imageNode The sender. + * @param image The new image, now being displayed. + * @param imageIdentifier The identifier for `image`. + * @discussion This method is only called when `image` changes, and not on subsequent redisplays of the same image. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode + didDisplayUpdatedImage:(nullable UIImage *)image + withIdentifier:(nullable ASImageIdentifier)imageIdentifier; + +/** + * @abstract Notification that the image node finished displaying an image. + * @param imageNode The sender. + * @discussion This method is called every time an image is displayed, whether or not it has changed. + */ +- (void)multiplexImageNodeDidFinishDisplay:(ASMultiplexImageNode *)imageNode; + +@end + + +#pragma mark - +/** + * The ASMultiplexImageNodeDataSource protocol is adopted by an object that provides the multiplex image node, + * for each image identifier, an image or a URL the image node should load. + */ +@protocol ASMultiplexImageNodeDataSource + +@optional +/** + * @abstract An image for the specified identifier. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that should be returned. + * @discussion If the image is already available to the data source, this method should be used in lieu of providing the + * URL to the image via -multiplexImageNode:URLForImageIdentifier:. + * @return A UIImage corresponding to `imageIdentifier`, or nil if none is available. + */ +- (nullable UIImage *)multiplexImageNode:(ASMultiplexImageNode *)imageNode imageForImageIdentifier:(ASImageIdentifier)imageIdentifier; + +/** + * @abstract An image URL for the specified identifier. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that will be downloaded. + * @discussion Supported URLs include HTTP, HTTPS, AssetsLibrary, and FTP URLs as well as Photos framework URLs (see note). + * + * If the image is already available to the data source, it should be provided via <[ASMultiplexImageNodeDataSource + * multiplexImageNode:imageForImageIdentifier:]> instead. + * @return An NSURL for the image identified by `imageIdentifier`, or nil if none is available. + * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. + */ +- (nullable NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(ASImageIdentifier)imageIdentifier; + +/** + * @abstract A PHAsset for the specific asset local identifier + * @param imageNode The sender. + * @param assetLocalIdentifier The local identifier for a PHAsset that this image node is loading. + * + * @discussion This optional method can improve image performance if your data source already has the PHAsset available. + * If this method is not implemented, or returns nil, the image node will request the asset from the Photos framework. + * @note This method may be called from any thread. + * @return A PHAsset corresponding to `assetLocalIdentifier`, or nil if none is available. + */ +- (nullable PHAsset *)multiplexImageNode:(ASMultiplexImageNode *)imageNode assetForLocalIdentifier:(NSString *)assetLocalIdentifier API_AVAILABLE(ios(8.0), tvos(10.0)); +@end + +#pragma mark - +@interface NSURL (ASPhotosFrameworkURLs) + +/** + * @abstract Create an NSURL that specifies an image from the Photos framework. + * + * @discussion When implementing `-multiplexImageNode:URLForImageIdentifier:`, you can return a URL + * created by this method and the image node will attempt to load the image from the Photos framework. + * @note The `synchronous` flag in `options` is ignored. + * @note The `Opportunistic` delivery mode is not supported and will be treated as `HighQualityFormat`. + */ ++ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier + targetSize:(CGSize)targetSize + contentMode:(PHImageContentMode)contentMode + options:(PHImageRequestOptions *)options NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT API_AVAILABLE(ios(8.0), tvos(10.0)); + +@end + +NS_ASSUME_NONNULL_END +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASMultiplexImageNode.mm b/submodules/AsyncDisplayKit/Source/ASMultiplexImageNode.mm new file mode 100644 index 0000000000..476ace1a38 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMultiplexImageNode.mm @@ -0,0 +1,962 @@ +// +// ASMultiplexImageNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import + +#if TARGET_OS_IOS && AS_USE_ASSETS_LIBRARY +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import + +#if AS_USE_PHOTOS +#import +#endif + +#if AS_PIN_REMOTE_IMAGE +#import +#else +#import +#endif + +using AS::MutexLocker; + +NSString *const ASMultiplexImageNodeErrorDomain = @"ASMultiplexImageNodeErrorDomain"; + +#if AS_USE_ASSETS_LIBRARY +static NSString *const kAssetsLibraryURLScheme = @"assets-library"; +#endif + +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + +/** + @abstract Signature for the block to be performed after an image has loaded. + @param image The image that was loaded, or nil if no image was loaded. + @param imageIdentifier The identifier of the image that was loaded, or nil if no image was loaded. + @param error An error describing why an image couldn't be loaded, if it failed to load; nil otherwise. + */ +typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdentifier, NSError *error); + +@interface ASMultiplexImageNode () +{ +@private + // Core. + id _cache; + + id _downloader; + struct { + unsigned int downloaderImplementsSetProgress:1; + unsigned int downloaderImplementsSetPriority:1; + unsigned int downloaderImplementsDownloadWithPriority:1; + } _downloaderFlags; + + __weak id _delegate; + struct { + unsigned int downloadStart:1; + unsigned int downloadProgress:1; + unsigned int downloadFinish:1; + unsigned int updatedImageDisplayFinish:1; + unsigned int updatedImage:1; + unsigned int displayFinish:1; + } _delegateFlags; + + __weak id _dataSource; + struct { + unsigned int image:1; + unsigned int URL:1; + unsigned int asset:1; + } _dataSourceFlags; + + // Image flags. + BOOL _downloadsIntermediateImages; // Defaults to NO. + AS::Mutex _imageIdentifiersLock; + NSArray *_imageIdentifiers; + id _loadedImageIdentifier; + id _loadingImageIdentifier; + id _displayedImageIdentifier; + __weak NSOperation *_phImageRequestOperation; + + // Networking. + AS::RecursiveMutex _downloadIdentifierLock; + id _downloadIdentifier; + + // Properties + BOOL _shouldRenderProgressImages; + + //set on init only + BOOL _cacheSupportsClearing; +} + +//! @abstract Read-write redeclaration of property declared in ASMultiplexImageNode.h. +@property (nonatomic, copy) id loadedImageIdentifier; + +//! @abstract The image identifier that's being loaded by _loadNextImageWithCompletion:. +@property (nonatomic, copy) id loadingImageIdentifier; + +/** + @abstract Returns the next image identifier that should be downloaded. + @discussion This method obeys and reflects the value of `downloadsIntermediateImages`. + @result The next image identifier, from `_imageIdentifiers`, that should be downloaded, or nil if no image should be downloaded next. + */ +- (id)_nextImageIdentifierToDownload; + +/** + @abstract Returns the best image that is immediately available from our datasource without downloading or hitting the cache. + @param imageIdentifierOut Upon return, the image identifier for the returned image; nil otherwise. + @discussion This method exclusively uses the data source's -multiplexImageNode:imageForIdentifier: method to return images. It does not fetch from the cache or kick off downloading. + @result The best UIImage available immediately; nil if no image is immediately available. + */ +- (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut; + +/** + @abstract Loads and displays the next image in the receiver's loading sequence. + @discussion This method obeys `downloadsIntermediateImages`. This method has no effect if nothing further should be loaded, as indicated by `_nextImageIdentifierToDownload`. This method will load the next image from the data-source, if possible; otherwise, the session's image cache will be queried for the desired image, and as a last resort, the image will be downloaded. + */ +- (void)_loadNextImage; + +/** + @abstract Fetches the image corresponding to the given imageIdentifier from the given URL from the session's image cache. + @param imageIdentifier The identifier for the image to be fetched. May not be nil. + @param imageURL The URL of the image to fetch. May not be nil. + @param completionBlock The block to be performed when the image has been fetched from the cache, if possible. May not be nil. + @discussion This method queries both the session's in-memory and on-disk caches (with preference for the in-memory cache). + */ +- (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock; + +#if TARGET_OS_IOS && AS_USE_ASSETS_LIBRARY +/** + @abstract Loads the image corresponding to the given assetURL from the device's Assets Library. + @param imageIdentifier The identifier for the image to be loaded. May not be nil. + @param assetURL The assets-library URL (e.g., "assets-library://identifier") of the image to load, from ALAsset. May not be nil. + @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil. + */ +- (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; +#endif + +#if AS_USE_PHOTOS +/** + @abstract Loads the image corresponding to the given image request from the Photos framework. + @param imageIdentifier The identifier for the image to be loaded. May not be nil. + @param request The photos image request to load. May not be nil. + @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil. + */ +- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock API_AVAILABLE(ios(8.0), tvos(10.0)); +#endif + +/** + @abstract Downloads the image corresponding to the given imageIdentifier from the given URL. + @param imageIdentifier The identifier for the image to be downloaded. May not be nil. + @param imageURL The URL of the image to downloaded. May not be nil. + @param completionBlock The block to be performed when the image has been downloaded, if possible. May not be nil. + */ +- (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; + +@end + +@implementation ASMultiplexImageNode + +#pragma mark - Getting Started / Tearing Down +- (instancetype)initWithCache:(id)cache downloader:(id)downloader +{ + if (!(self = [super init])) + return nil; + + _cache = (id)cache; + _downloader = (id)downloader; + + _downloaderFlags.downloaderImplementsSetProgress = [downloader respondsToSelector:@selector(setProgressImageBlock:callbackQueue:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsDownloadWithPriority = [downloader respondsToSelector:@selector(downloadImageWithURL:priority:callbackQueue:downloadProgress:completion:)]; + + _cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; + + _shouldRenderProgressImages = YES; + + self.shouldBypassEnsureDisplay = YES; + + return self; +} + +- (instancetype)init +{ +#if AS_PIN_REMOTE_IMAGE + return [self initWithCache:[ASPINRemoteImageDownloader sharedDownloader] downloader:[ASPINRemoteImageDownloader sharedDownloader]]; +#else + return [self initWithCache:nil downloader:[ASBasicImageDownloader sharedImageDownloader]]; +#endif +} + +- (void)dealloc +{ + [_phImageRequestOperation cancel]; +} + +#pragma mark - ASDisplayNode Overrides + +- (void)clearContents +{ + [super clearContents]; // This actually clears the contents, so we need to do this first for our displayedImageIdentifier to be meaningful. + [self _setDisplayedImageIdentifier:nil withImage:nil]; + + // NOTE: We intentionally do not cancel image downloads until `clearPreloadedData`. +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + + [_phImageRequestOperation cancel]; + + [self _setDownloadIdentifier:nil]; + + if (_cacheSupportsClearing && self.loadedImageIdentifier != nil) { + NSURL *URL = [_dataSource multiplexImageNode:self URLForImageIdentifier:self.loadedImageIdentifier]; + if (URL != nil) { + [_cache clearFetchedImageFromCacheWithURL:URL]; + } + } + + // setting this to nil makes the node fetch images the next time its display starts + _loadedImageIdentifier = nil; + [self _setImage:nil]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + + [self _loadImageIdentifiers]; +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + // We may now be displaying the loaded identifier, if they're different. + UIImage *displayedImage = self.image; + if (displayedImage) { + if (!ASObjectIsEqual(_displayedImageIdentifier, _loadedImageIdentifier)) + [self _setDisplayedImageIdentifier:_loadedImageIdentifier withImage:displayedImage]; + + // Delegateify + if (_delegateFlags.displayFinish) { + if (ASDisplayNodeThreadIsMain()) + [_delegate multiplexImageNodeDidFinishDisplay:self]; + else { + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + [strongSelf.delegate multiplexImageNodeDidFinishDisplay:strongSelf]; + }); + } + } + } +} + +- (BOOL)placeholderShouldPersist +{ + return (self.image == nil && self.animatedImage == nil && self.imageIdentifiers.count > 0); +} + +/* displayWillStartAsynchronously in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary + in ASNetworkImageNode as well. */ +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + [super displayWillStartAsynchronously:asynchronously]; + [self didEnterPreloadState]; + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityImminent]; +} + +/* didEnterVisibleState / didExitVisibleState in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary + in ASNetworkImageNode as well. */ +- (void)didEnterVisibleState +{ + [super didEnterVisibleState]; + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityVisible]; + [self _updateProgressImageBlockOnDownloaderIfNeeded]; +} + +- (void)didExitVisibleState +{ + [super didExitVisibleState]; + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityPreload]; + [self _updateProgressImageBlockOnDownloaderIfNeeded]; +} + +- (void)didExitDisplayState +{ + [super didExitDisplayState]; + if (ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityPreload]; + } +} + +#pragma mark - Core + +- (void)setImage:(UIImage *)image +{ + ASDisplayNodeAssert(NO, @"Setting the image directly on an ASMultiplexImageNode is unsafe. It will be cleared in didExitPreloadRange and will have no way to restore in didEnterPreloadRange"); + super.image = image; +} + +- (void)_setImage:(UIImage *)image +{ + super.image = image; +} + +- (void)setDelegate:(id )delegate +{ + if (_delegate == delegate) + return; + + _delegate = delegate; + _delegateFlags.downloadStart = [_delegate respondsToSelector:@selector(multiplexImageNode:didStartDownloadOfImageWithIdentifier:)]; + _delegateFlags.downloadProgress = [_delegate respondsToSelector:@selector(multiplexImageNode:didUpdateDownloadProgress:forImageWithIdentifier:)]; + _delegateFlags.downloadFinish = [_delegate respondsToSelector:@selector(multiplexImageNode:didFinishDownloadingImageWithIdentifier:error:)]; + _delegateFlags.updatedImageDisplayFinish = [_delegate respondsToSelector:@selector(multiplexImageNode:didDisplayUpdatedImage:withIdentifier:)]; + _delegateFlags.updatedImage = [_delegate respondsToSelector:@selector(multiplexImageNode:didUpdateImage:withIdentifier:fromImage:withIdentifier:)]; + _delegateFlags.displayFinish = [_delegate respondsToSelector:@selector(multiplexImageNodeDidFinishDisplay:)]; +} + + +- (void)setDataSource:(id )dataSource +{ + if (_dataSource == dataSource) + return; + + _dataSource = dataSource; + _dataSourceFlags.image = [_dataSource respondsToSelector:@selector(multiplexImageNode:imageForImageIdentifier:)]; + _dataSourceFlags.URL = [_dataSource respondsToSelector:@selector(multiplexImageNode:URLForImageIdentifier:)]; + if (AS_AVAILABLE_IOS_TVOS(9, 10)) { + _dataSourceFlags.asset = [_dataSource respondsToSelector:@selector(multiplexImageNode:assetForLocalIdentifier:)]; + } +} + + +- (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages +{ + [self lock]; + if (shouldRenderProgressImages == _shouldRenderProgressImages) { + [self unlock]; + return; + } + + _shouldRenderProgressImages = shouldRenderProgressImages; + + [self unlock]; + [self _updateProgressImageBlockOnDownloaderIfNeeded]; +} + +- (BOOL)shouldRenderProgressImages +{ + return ASLockedSelf(_shouldRenderProgressImages); +} + +#pragma mark - + +#pragma mark - + +- (NSArray *)imageIdentifiers +{ + MutexLocker l(_imageIdentifiersLock); + return _imageIdentifiers; +} + +- (void)setImageIdentifiers:(NSArray *)imageIdentifiers +{ + { + MutexLocker l(_imageIdentifiersLock); + if (ASObjectIsEqual(_imageIdentifiers, imageIdentifiers)) { + return; + } + + _imageIdentifiers = [[NSArray alloc] initWithArray:imageIdentifiers copyItems:YES]; + } + + [self setNeedsPreload]; +} + +- (void)reloadImageIdentifierSources +{ + // setting this to nil makes the node think it has not downloaded any images + _loadedImageIdentifier = nil; + [self _loadImageIdentifiers]; +} + +#pragma mark - + + +#pragma mark - Core Internal +- (void)_setDisplayedImageIdentifier:(id)displayedImageIdentifier withImage:(UIImage *)image +{ + ASDisplayNodeAssertMainThread(); + + if (ASObjectIsEqual(_displayedImageIdentifier, displayedImageIdentifier)) { + return; + } + + _displayedImageIdentifier = displayedImageIdentifier; + + // Delegateify. + // Note that we're using the params here instead of self.image and _displayedImageIdentifier because those can change before the async block below executes. + if (_delegateFlags.updatedImageDisplayFinish) { + if (ASDisplayNodeThreadIsMain()) + [_delegate multiplexImageNode:self didDisplayUpdatedImage:image withIdentifier:displayedImageIdentifier]; + else { + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + [strongSelf.delegate multiplexImageNode:strongSelf didDisplayUpdatedImage:image withIdentifier:displayedImageIdentifier]; + }); + } + } +} + +- (void)_setDownloadIdentifier:(id)downloadIdentifier +{ + MutexLocker l(_downloadIdentifierLock); + if (ASObjectIsEqual(downloadIdentifier, _downloadIdentifier)) + return; + + if (_downloadIdentifier) { + [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; + } + _downloadIdentifier = downloadIdentifier; +} + +#pragma mark - Image Loading Machinery + +- (void)_loadImageIdentifiers +{ + // Grab the best possible image we can load right now. + id bestImmediatelyAvailableImageIdentifier = nil; + UIImage *bestImmediatelyAvailableImage = [self _bestImmediatelyAvailableImageFromDataSource:&bestImmediatelyAvailableImageIdentifier]; + as_log_verbose(ASImageLoadingLog(), "%@ Best immediately available image identifier is %@", self, bestImmediatelyAvailableImageIdentifier); + + // Load it. This kicks off cache fetching/downloading, as appropriate. + [self _finishedLoadingImage:bestImmediatelyAvailableImage forIdentifier:bestImmediatelyAvailableImageIdentifier error:nil]; +} + +- (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut +{ + MutexLocker l(_imageIdentifiersLock); + + // If we don't have any identifiers to load or don't implement the image DS method, bail. + if ([_imageIdentifiers count] == 0 || !_dataSourceFlags.image) { + return nil; + } + + // Grab the best available image from the data source. + UIImage *existingImage = self.image; + for (id imageIdentifier in _imageIdentifiers) { + // If this image is already loaded, don't request it from the data source again because + // the data source may generate a new instance of UIImage that returns NO for isEqual: + // and we'll end up in an infinite loading loop. + UIImage *image = ASObjectIsEqual(imageIdentifier, _loadedImageIdentifier) ? existingImage : [_dataSource multiplexImageNode:self imageForImageIdentifier:imageIdentifier]; + if (image) { + if (imageIdentifierOut) { + *imageIdentifierOut = imageIdentifier; + } + + return image; + } + } + + return nil; +} + +#pragma mark - + +- (void)_updatePriorityOnDownloaderIfNeededWithDefaultPriority:(ASImageDownloaderPriority)defaultPriority +{ + ASAssertUnlocked(_downloadIdentifierLock); + + if (_downloaderFlags.downloaderImplementsSetPriority) { + // Read our interface state before locking so that we don't lock super while holding our lock. + ASInterfaceState interfaceState = self.interfaceState; + MutexLocker l(_downloadIdentifierLock); + + if (_downloadIdentifier != nil) { + ASImageDownloaderPriority priority = defaultPriority; + if (ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + priority = ASImageDownloaderPriorityWithInterfaceState(interfaceState); + } + + [_downloader setPriority:priority withDownloadIdentifier:_downloadIdentifier]; + } + } +} + +- (void)_updateProgressImageBlockOnDownloaderIfNeeded +{ + ASAssertUnlocked(_downloadIdentifierLock); + + BOOL shouldRenderProgressImages = self.shouldRenderProgressImages; + + // Read our interface state before locking so that we don't lock super while holding our lock. + ASInterfaceState interfaceState = self.interfaceState; + MutexLocker l(_downloadIdentifierLock); + + if (!_downloaderFlags.downloaderImplementsSetProgress || _downloadIdentifier == nil) { + return; + } + + ASImageDownloaderProgressImage progress = nil; + if (shouldRenderProgressImages && ASInterfaceStateIncludesVisible(interfaceState)) { + __weak __typeof__(self) weakSelf = self; + progress = ^(UIImage * _Nonnull progressImage, CGFloat progress, id _Nullable downloadIdentifier) { + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + MutexLocker l(strongSelf->_downloadIdentifierLock); + //Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; + } + [strongSelf _setImage:progressImage]; + }; + } + [_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier]; +} + +- (void)_clearImage +{ + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + UIImage *image = self.image; + CGSize imageSize = image.size; + BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || + imageSize.height > kMinReleaseImageOnBackgroundSize.height; + [self _setImage:nil]; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(&image); + } +} + +#pragma mark - +- (id)_nextImageIdentifierToDownload +{ + MutexLocker l(_imageIdentifiersLock); + + // If we've already loaded the best identifier, we've got nothing else to do. + id bestImageIdentifier = _imageIdentifiers.firstObject; + if (!bestImageIdentifier || ASObjectIsEqual(_loadedImageIdentifier, bestImageIdentifier)) { + return nil; + } + + id nextImageIdentifierToDownload = nil; + + // If we're not supposed to download intermediate images, load the best identifier we've got. + if (!_downloadsIntermediateImages) { + nextImageIdentifierToDownload = bestImageIdentifier; + } + // Otherwise, load progressively. + else { + NSUInteger loadedIndex = [_imageIdentifiers indexOfObject:_loadedImageIdentifier]; + + // If nothing has loaded yet, load the worst identifier. + if (loadedIndex == NSNotFound) { + nextImageIdentifierToDownload = [_imageIdentifiers lastObject]; + } + // Otherwise, load the next best identifier (if there is one) + else if (loadedIndex > 0) { + nextImageIdentifierToDownload = _imageIdentifiers[loadedIndex - 1]; + } + } + + return nextImageIdentifierToDownload; +} + +- (void)_loadNextImage +{ + // Determine the next identifier to load (if any). + id nextImageIdentifier = [self _nextImageIdentifierToDownload]; + if (!nextImageIdentifier) { + [self _finishedLoadingImage:nil forIdentifier:nil error:nil]; + return; + } + + as_activity_create_for_scope("Load next image for multiplex image node"); + as_log_verbose(ASImageLoadingLog(), "Loading image for %@ ident: %@", self, nextImageIdentifier); + self.loadingImageIdentifier = nextImageIdentifier; + + __weak __typeof__(self) weakSelf = self; + ASMultiplexImageLoadCompletionBlock finishedLoadingBlock = ^(UIImage *image, id imageIdentifier, NSError *error) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + // Only nil out the loading identifier if the loading identifier hasn't changed. + if (ASObjectIsEqual(strongSelf.loadingImageIdentifier, nextImageIdentifier)) { + strongSelf.loadingImageIdentifier = nil; + } + [strongSelf _finishedLoadingImage:image forIdentifier:imageIdentifier error:error]; + }; + + // Ask our data-source if it's got this image. + if (_dataSourceFlags.image) { + UIImage *image = [_dataSource multiplexImageNode:self imageForImageIdentifier:nextImageIdentifier]; + if (image) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from data source for %@ ident: %@", self, nextImageIdentifier); + finishedLoadingBlock(image, nextImageIdentifier, nil); + return; + } + } + + NSURL *nextImageURL = (_dataSourceFlags.URL) ? [_dataSource multiplexImageNode:self URLForImageIdentifier:nextImageIdentifier] : nil; + // If we fail to get a URL for the image, we have no source and can't proceed. + if (!nextImageURL) { + as_log_error(ASImageLoadingLog(), "Could not acquire URL %@ ident: (%@)", self, nextImageIdentifier); + finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeNoSourceForImage userInfo:nil]); + return; + } + +#if TARGET_OS_IOS && AS_USE_ASSETS_LIBRARY + // If it's an assets-library URL, we need to fetch it from the assets library. + if ([[nextImageURL scheme] isEqualToString:kAssetsLibraryURLScheme]) { + // Load the asset. + [self _loadALAssetWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from assets library for %@ %@", weakSelf, nextImageIdentifier); + finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); + }]; + + return; + } +#endif + +#if AS_USE_PHOTOS + if (AS_AVAILABLE_IOS_TVOS(9, 10)) { + // Likewise, if it's a Photos asset, we need to fetch it accordingly. + if (ASPhotosFrameworkImageRequest *request = [ASPhotosFrameworkImageRequest requestWithURL:nextImageURL]) { + [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from Photos for %@ %@", weakSelf, nextImageIdentifier); + finishedLoadingBlock(image, nextImageIdentifier, error); + }]; + + return; + } + } +#endif + + // Otherwise, it's a web URL that we can download. + // First, check the cache. + [self _fetchImageWithIdentifierFromCache:nextImageIdentifier URL:nextImageURL completion:^(UIImage *imageFromCache) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + // If we had a cache-hit, we're done. + if (imageFromCache) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from cache for %@ id: %@ img: %@", strongSelf, nextImageIdentifier, imageFromCache); + finishedLoadingBlock(imageFromCache, nextImageIdentifier, nil); + return; + } + + // If the next image to load has changed, bail. + if (!ASObjectIsEqual([strongSelf _nextImageIdentifierToDownload], nextImageIdentifier)) { + finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged userInfo:nil]); + return; + } + + // Otherwise, we've got to download it. + [strongSelf _downloadImageWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) { + __typeof__(self) strongSelf = weakSelf; + if (downloadedImage) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from download for %@ id: %@ img: %@", strongSelf, nextImageIdentifier, downloadedImage); + } else { + as_log_error(ASImageLoadingLog(), "Error downloading image for %@ id: %@ err: %@", strongSelf, nextImageIdentifier, error); + } + finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); + }]; + }]; +} +#if TARGET_OS_IOS && AS_USE_ASSETS_LIBRARY +- (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(assetURL, @"assetURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + // ALAssetsLibrary was replaced in iOS 8 and deprecated in iOS 9. + // We'll drop support very soon. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init]; + + [assetLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) { + ALAssetRepresentation *representation = [asset defaultRepresentation]; + CGImageRef coreGraphicsImage = [representation fullScreenImage]; + + UIImage *downloadedImage = (coreGraphicsImage ? [UIImage imageWithCGImage:coreGraphicsImage] : nil); + completionBlock(downloadedImage, nil); + } failureBlock:^(NSError *error) { + completionBlock(nil, error); + }]; +#pragma clang diagnostic pop +} +#endif + +#if AS_USE_PHOTOS +- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(request, @"request is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + /* + * Locking rationale: + * As of iOS 9, Photos.framework will eventually deadlock if you hit it with concurrent fetch requests. rdar://22984886 + * Concurrent image requests are OK, but metadata requests aren't, so we limit ourselves to one at a time. + */ + static NSLock *phRequestLock; + static NSOperationQueue *phImageRequestQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + phRequestLock = [NSLock new]; + phImageRequestQueue = [NSOperationQueue new]; + phImageRequestQueue.maxConcurrentOperationCount = 10; + phImageRequestQueue.name = @"org.AsyncDisplayKit.MultiplexImageNode.phImageRequestQueue"; + }); + + // Each ASMultiplexImageNode can have max 1 inflight Photos image request operation + [_phImageRequestOperation cancel]; + + __weak __typeof(self) weakSelf = self; + NSOperation *newImageRequestOp = [NSBlockOperation blockOperationWithBlock:^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf == nil) { return; } + + PHAsset *imageAsset = nil; + + // Try to get the asset immediately from the data source. + if (_dataSourceFlags.asset) { + imageAsset = [strongSelf.dataSource multiplexImageNode:strongSelf assetForLocalIdentifier:request.assetIdentifier]; + } + + // Fall back to locking and getting the PHAsset. + if (imageAsset == nil) { + [phRequestLock lock]; + // -[PHFetchResult dealloc] plays a role in the deadlock mentioned above, so we make sure the PHFetchResult is deallocated inside the critical section + @autoreleasepool { + imageAsset = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil].firstObject; + } + [phRequestLock unlock]; + } + + if (imageAsset == nil) { + NSError *error = [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodePHAssetIsUnavailable userInfo:nil]; + completionBlock(nil, error); + return; + } + + PHImageRequestOptions *options = [request.options copy]; + + // We don't support opportunistic delivery – one request, one image. + if (options.deliveryMode == PHImageRequestOptionsDeliveryModeOpportunistic) { + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + } + + if (options.deliveryMode == PHImageRequestOptionsDeliveryModeHighQualityFormat) { + // Without this flag the result will be delivered on the main queue, which is pointless + // But synchronous -> HighQualityFormat so we only use it if high quality format is specified + options.synchronous = YES; + } + + PHImageManager *imageManager = strongSelf.imageManager ? : PHImageManager.defaultManager; + [imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { + NSError *error = info[PHImageErrorKey]; + + if (error == nil && image == nil) { + error = [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodePhotosImageManagerFailedWithoutError userInfo:nil]; + } + + if (NSThread.isMainThread) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + completionBlock(image, error); + }); + } else { + completionBlock(image, error); + } + }]; + }]; + // If you don't set this, iOS will sometimes infer NSQualityOfServiceUserInteractive and promote the entire queue to that level, damaging system responsiveness + newImageRequestOp.qualityOfService = NSQualityOfServiceUserInitiated; + _phImageRequestOperation = newImageRequestOp; + [phImageRequestQueue addOperation:newImageRequestOp]; +} +#endif + +- (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(imageURL, @"imageURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + if (_cache) { + [_cache cachedImageWithURL:imageURL callbackQueue:dispatch_get_main_queue() completion:^(id imageContainer) { + completionBlock([imageContainer asdk_image]); + }]; + } + // If we don't have a cache, just fail immediately. + else { + completionBlock(nil); + } +} + +- (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(imageURL, @"imageURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + // Delegate (start) + if (_delegateFlags.downloadStart) + [_delegate multiplexImageNode:self didStartDownloadOfImageWithIdentifier:imageIdentifier]; + + __weak __typeof__(self) weakSelf = self; + ASImageDownloaderProgress downloadProgressBlock = NULL; + if (_delegateFlags.downloadProgress) { + downloadProgressBlock = ^(CGFloat progress) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + [strongSelf.delegate multiplexImageNode:strongSelf didUpdateDownloadProgress:progress forImageWithIdentifier:imageIdentifier]; + }; + } + + ASImageDownloaderCompletion completion = ^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { + // We dereference iVars directly, so we can't have weakSelf going nil on us. + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + MutexLocker l(strongSelf->_downloadIdentifierLock); + //Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; + } + + completionBlock([imageContainer asdk_image], error); + + // Delegateify. + if (strongSelf->_delegateFlags.downloadFinish) + [strongSelf->_delegate multiplexImageNode:weakSelf didFinishDownloadingImageWithIdentifier:imageIdentifier error:error]; + }; + + // Download! + ASPerformBlockOnBackgroundThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + dispatch_queue_t callbackQueue = dispatch_get_main_queue(); + + id downloadIdentifier; + if (strongSelf->_downloaderFlags.downloaderImplementsDownloadWithPriority + && ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + + /* + Decide a priority based on the current interface state of this node. + It can happen that this method was called when the node entered preload state + but the interface state, at this point, tells us that the node is (going to be) visible, + If that's the case, we jump to a higher priority directly. + */ + ASImageDownloaderPriority priority = ASImageDownloaderPriorityWithInterfaceState(strongSelf.interfaceState); + downloadIdentifier = [strongSelf->_downloader downloadImageWithURL:imageURL + priority:priority + callbackQueue:callbackQueue + downloadProgress:downloadProgressBlock + completion:completion]; + } else { + /* + Kick off a download with default priority. + The actual "default" value is decided by the downloader. + ASBasicImageDownloader and ASPINRemoteImageDownloader both use ASImageDownloaderPriorityImminent + which is mapped to NSURLSessionTaskPriorityDefault. + + This means that preload and display nodes use the same priority + and their requests are put into the same pool. + */ + downloadIdentifier = [strongSelf->_downloader downloadImageWithURL:imageURL + callbackQueue:callbackQueue + downloadProgress:downloadProgressBlock + completion:completion]; + } + + [strongSelf _setDownloadIdentifier:downloadIdentifier]; + [strongSelf _updateProgressImageBlockOnDownloaderIfNeeded]; + }); +} + +#pragma mark - +- (void)_finishedLoadingImage:(UIImage *)image forIdentifier:(id)imageIdentifier error:(NSError *)error +{ + // If we failed to load, we stop the loading process. + // Note that if we bailed before we began downloading because the best identifier changed, we don't bail, but rather just begin loading the best image identifier. + if (error && !([error.domain isEqual:ASMultiplexImageNodeErrorDomain] && error.code == ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged)) + return; + + + _imageIdentifiersLock.lock(); + NSUInteger imageIdentifierCount = [_imageIdentifiers count]; + _imageIdentifiersLock.unlock(); + + // Update our image if we got one, or if we're not supposed to display one at all. + // We explicitly perform this check because our datasource often doesn't give back immediately available images, even though we might have downloaded one already. + // Because we seed this call with bestImmediatelyAvailableImageFromDataSource, we must be careful not to trample an existing image. + if (image || imageIdentifierCount == 0) { + as_log_verbose(ASImageLoadingLog(), "[%p] loaded -> displaying (%@, %@)", self, imageIdentifier, image); + id previousIdentifier = self.loadedImageIdentifier; + UIImage *previousImage = self.image; + + self.loadedImageIdentifier = imageIdentifier; + [self _setImage:image]; + + if (_delegateFlags.updatedImage) { + [_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier]; + } + + } + + // Load our next image, if we have one to load. + if ([self _nextImageIdentifierToDownload]) + [self _loadNextImage]; +} + +@end + +#if AS_USE_PHOTOS +@implementation NSURL (ASPhotosFrameworkURLs) + ++ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options NS_RETURNS_RETAINED +{ + ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:assetLocalIdentifier]; + request.options = options; + request.contentMode = contentMode; + request.targetSize = targetSize; + return request.url; +} + +@end +#endif +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASNavigationController.h b/submodules/AsyncDisplayKit/Source/ASNavigationController.h new file mode 100644 index 0000000000..4f7ac12df4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNavigationController.h @@ -0,0 +1,34 @@ +// +// ASNavigationController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * ASNavigationController + * + * @discussion ASNavigationController is a drop in replacement for UINavigationController + * which improves memory efficiency by implementing the @c ASManagesChildVisibilityDepth protocol. + * You can use ASNavigationController with regular UIViewControllers, as well as ASViewControllers. + * It is safe to subclass or use even where AsyncDisplayKit is not adopted. + * + * @see ASManagesChildVisibilityDepth + */ +@interface ASNavigationController : UINavigationController + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASNavigationController.mm b/submodules/AsyncDisplayKit/Source/ASNavigationController.mm new file mode 100644 index 0000000000..c92ddd7d27 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNavigationController.mm @@ -0,0 +1,115 @@ +// +// ASNavigationController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import +#import +#import + +@implementation ASNavigationController +{ + BOOL _parentManagesVisibilityDepth; + NSInteger _visibilityDepth; +} + +ASVisibilityDidMoveToParentViewController; + +ASVisibilityViewWillAppear; + +ASVisibilityViewDidDisappearImplementation; + +ASVisibilitySetVisibilityDepth; + +ASVisibilityDepthImplementation; + +- (void)visibilityDepthDidChange +{ + for (UIViewController *viewController in self.viewControllers) { + if ([viewController conformsToProtocol:@protocol(ASVisibilityDepth)]) { + [(id )viewController visibilityDepthDidChange]; + } + } +} + +- (NSInteger)visibilityDepthOfChildViewController:(UIViewController *)childViewController +{ + NSUInteger viewControllerIndex = [self.viewControllers indexOfObjectIdenticalTo:childViewController]; + if (viewControllerIndex == NSNotFound) { + //If childViewController is not actually a child, return NSNotFound which is also a really large number. + return NSNotFound; + } + + if (viewControllerIndex == self.viewControllers.count - 1) { + //view controller is at the top, just return our own visibility depth. + return [self visibilityDepth]; + } else if (viewControllerIndex == 0) { + //view controller is the root view controller. Can be accessed by holding the back button. + return [self visibilityDepth] + 1; + } + + return [self visibilityDepth] + self.viewControllers.count - 1 - viewControllerIndex; +} + +#pragma mark - UIKit overrides + +- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + as_activity_create_for_scope("Pop multiple from ASNavigationController"); + NSArray *viewControllers = [super popToViewController:viewController animated:animated]; + as_log_info(ASNodeLog(), "Popped %@ to %@, removing %@", self, viewController, ASGetDescriptionValueString(viewControllers)); + + [self visibilityDepthDidChange]; + return viewControllers; +} + +- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated +{ + as_activity_create_for_scope("Pop to root of ASNavigationController"); + NSArray *viewControllers = [super popToRootViewControllerAnimated:animated]; + as_log_info(ASNodeLog(), "Popped view controllers %@ from %@", ASGetDescriptionValueString(viewControllers), self); + + [self visibilityDepthDidChange]; + return viewControllers; +} + +- (void)setViewControllers:(NSArray *)viewControllers +{ + // NOTE: As of now this method calls through to setViewControllers:animated: so no need to log/activity here. + + [super setViewControllers:viewControllers]; + [self visibilityDepthDidChange]; +} + +- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated +{ + as_activity_create_for_scope("Set view controllers of ASNavigationController"); + as_log_info(ASNodeLog(), "Set view controllers of %@ to %@ animated: %d", self, ASGetDescriptionValueString(viewControllers), animated); + [super setViewControllers:viewControllers animated:animated]; + [self visibilityDepthDidChange]; +} + +- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + as_activity_create_for_scope("Push view controller on ASNavigationController"); + as_log_info(ASNodeLog(), "Pushing %@ onto %@", viewController, self); + [super pushViewController:viewController animated:animated]; + [self visibilityDepthDidChange]; +} + +- (UIViewController *)popViewControllerAnimated:(BOOL)animated +{ + as_activity_create_for_scope("Pop view controller from ASNavigationController"); + UIViewController *viewController = [super popViewControllerAnimated:animated]; + as_log_info(ASNodeLog(), "Popped %@ from %@", viewController, self); + [self visibilityDepthDidChange]; + return viewController; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASNetworkImageLoadInfo.h b/submodules/AsyncDisplayKit/Source/ASNetworkImageLoadInfo.h new file mode 100644 index 0000000000..55c4b49a7a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNetworkImageLoadInfo.h @@ -0,0 +1,39 @@ +// +// ASNetworkImageLoadInfo.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, ASNetworkImageSourceType) { + ASNetworkImageSourceUnspecified = 0, + ASNetworkImageSourceSynchronousCache, + ASNetworkImageSourceAsynchronousCache, + ASNetworkImageSourceFileURL, + ASNetworkImageSourceDownload, +}; + +AS_SUBCLASSING_RESTRICTED +@interface ASNetworkImageLoadInfo : NSObject + +/// The type of source from which the image was loaded. +@property (readonly) ASNetworkImageSourceType sourceType; + +/// The image URL that was downloaded. +@property (readonly) NSURL *url; + +/// The download identifier, if one was provided. +@property (nullable, readonly) id downloadIdentifier; + +/// The userInfo object provided by the downloader, if one was provided. +@property (nullable, readonly) id userInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASNetworkImageLoadInfo.mm b/submodules/AsyncDisplayKit/Source/ASNetworkImageLoadInfo.mm new file mode 100644 index 0000000000..4c3d553e74 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNetworkImageLoadInfo.mm @@ -0,0 +1,32 @@ +// +// ASNetworkImageLoadInfo.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASNetworkImageLoadInfo + +- (instancetype)initWithURL:(NSURL *)url sourceType:(ASNetworkImageSourceType)sourceType downloadIdentifier:(id)downloadIdentifier userInfo:(id)userInfo +{ + if (self = [super init]) { + _url = [url copy]; + _sourceType = sourceType; + _downloadIdentifier = downloadIdentifier; + _userInfo = userInfo; + } + return self; +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + return self; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASNetworkImageNode.h b/submodules/AsyncDisplayKit/Source/ASNetworkImageNode.h new file mode 100644 index 0000000000..f67826e86c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNetworkImageNode.h @@ -0,0 +1,241 @@ +// +// ASNetworkImageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASNetworkImageNodeDelegate, ASImageCacheProtocol, ASImageDownloaderProtocol; +@class ASNetworkImageLoadInfo; + + +/** + * ASNetworkImageNode is a simple image node that can download and display an image from the network, with support for a + * placeholder image (). The currently-displayed image is always available in the inherited ASImageNode + * property. + * + * @see ASMultiplexImageNode for a more powerful counterpart to this class. + */ +@interface ASNetworkImageNode : ASImageNode + +/** + * The designated initializer. Cache and Downloader are WEAK references. + * + * @param cache The object that implements a cache of images for the image node. Weak reference. + * @param downloader The object that implements image downloading for the image node. Must not be nil. Weak reference. + * + * @discussion If `cache` is nil, the receiver will not attempt to retrieve images from a cache before downloading them. + * + * @return An initialized ASNetworkImageNode. + */ +- (instancetype)initWithCache:(nullable id)cache downloader:(id)downloader NS_DESIGNATED_INITIALIZER; + +/** + * Convenience initializer. + * + * @return An ASNetworkImageNode configured to use the NSURLSession-powered ASBasicImageDownloader, and no extra cache. + */ +- (instancetype)init; + +/** + * The delegate, which must conform to the protocol. + */ +@property (nullable, weak) id delegate; + +/** + * The delegate will receive callbacks on main thread. Default to YES. + */ +@property (class) BOOL useMainThreadDelegateCallbacks; + +/** + * The image to display. + * + * @discussion By setting an image to the image property the ASNetworkImageNode will act like a plain ASImageNode. + * As soon as the URL is set the ASNetworkImageNode will act like an ASNetworkImageNode and the image property + * will be managed internally. This means the image property will be cleared out and replaced by the placeholder + * () image while loading and the final image after the new image data was downloaded and processed. + * If you want to use a placholder image functionality use the defaultImage property instead. + */ +@property (nullable) UIImage *image; + +/** + * A placeholder image to display while the URL is loading. This is slightly different than placeholderImage in the + * ASDisplayNode superclass as defaultImage will *not* be displayed synchronously. If you wish to have the image + * displayed synchronously, use @c placeholderImage. + */ +@property (nullable) UIImage *defaultImage; + +/** + * The URL of a new image to download and display. + * + * @discussion By setting an URL, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. + */ +@property (nullable, copy) NSURL *URL; + +/** + * An array of URLs of increasing cost to download. + * + * @discussion By setting an array of URLs, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. + * + * @deprecated This API has been removed for now due to the increased complexity to the class that it brought. + * Please use .URL instead. + */ +@property (nullable, copy) NSArray *URLs ASDISPLAYNODE_DEPRECATED_MSG("Please use URL instead."); + +/** + * Download and display a new image. + * + * @param URL The URL of a new image to download and display. + * @param reset Whether to display a placeholder () while loading the new image. + * + * @discussion By setting an URL, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. + */ +- (void)setURL:(nullable NSURL *)URL resetToDefault:(BOOL)reset; + +/** + * If is a local file, set this property to YES to take advantage of UIKit's image caching. Defaults to YES. + */ +@property BOOL shouldCacheImage; + +/** + * If the downloader implements progressive image rendering and this value is YES progressive renders of the + * image will be displayed as the image downloads. Regardless of this properties value, progress renders will + * only occur when the node is visible. Defaults to YES. + */ +@property BOOL shouldRenderProgressImages; + +/** + * The image quality of the current image. + * + * If the URL is set, this is a number between 0 and 1 and can be used to track + * progressive progress. Calculated by dividing number of bytes / expected number of total bytes. + * This is zero until the first progressive render or the final display. + * + * If the URL is unset, this is 1 if defaultImage or image is set to non-nil. + * + */ +@property (readonly) CGFloat currentImageQuality; + +/** + * The currentImageQuality (value between 0 and 1) of the last image that completed displaying. + */ +@property (readonly) CGFloat renderedImageQuality; + +@end + + +#pragma mark - + +/** + * The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to + * notifications such as finished decoding and downloading an image. + */ +@protocol ASNetworkImageNodeDelegate +@optional + +/** + * Notification that the image node started to load + * + * @param imageNode The sender. + * + * @discussion Called on the main thread. + */ +- (void)imageNodeDidStartFetchingData:(ASNetworkImageNode *)imageNode; + +/** + * Notification that the image node will load image from cache + * + * @param imageNode The sender. + * + * @discussion Called on the main thread. + */ +- (void)imageNodeWillLoadImageFromCache:(ASNetworkImageNode *)imageNode; + +/** + * Notification that the image node finished loading image from cache + * + * @param imageNode The sender. + * + * @discussion Called on the main thread. + */ +- (void)imageNodeDidLoadImageFromCache:(ASNetworkImageNode *)imageNode; + +/** + * Notification that the image node will load image from network + * + * @param imageNode The sender. + * + * @discussion Called on the main thread. + */ +- (void)imageNodeWillLoadImageFromNetwork:(ASNetworkImageNode *)imageNode; + +/** + * Notification that the image node will start display + * + * @param imageNode The sender. + * + * @discussion Called on the main thread. + */ +- (void)imageNodeWillStartDisplayAsynchronously:(ASNetworkImageNode *)imageNode; + +/** + * Notification that the image node finished downloading an image, with additional info. + * If implemented, this method will be called instead of `imageNode:didLoadImage:`. + * + * @param imageNode The sender. + * @param image The newly-loaded image. + * @param info Additional information about the image load. + * + * @discussion Called on the main thread if useMainThreadDelegateCallbacks=YES (the default), otherwise on a background thread. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageLoadInfo *)info; + +/** + * Notification that the image node finished downloading an image. + * + * @param imageNode The sender. + * @param image The newly-loaded image. + * + * @discussion Called on the main thread if useMainThreadDelegateCallbacks=YES (the default), otherwise on a background thread. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image; + +/** + * Notification that the image node failed to download the image. + * + * @param imageNode The sender. + * @param error The error with details. + * + * @discussion Called on the main thread if useMainThreadDelegateCallbacks=YES (the default), otherwise on a background thread. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didFailWithError:(NSError *)error; + +/** + * Notification that the image node finished decoding an image. + * + * @param imageNode The sender. + * + * @discussion Called on the main thread. + */ +- (void)imageNodeDidFinishDecoding:(ASNetworkImageNode *)imageNode; + + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASNetworkImageNode.mm b/submodules/AsyncDisplayKit/Source/ASNetworkImageNode.mm new file mode 100644 index 0000000000..29a600d8e8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNetworkImageNode.mm @@ -0,0 +1,892 @@ +// +// ASNetworkImageNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import + +#if AS_PIN_REMOTE_IMAGE +#import +#endif + +@interface ASNetworkImageNode () +{ + // Only access any of these while locked. + __weak id _delegate; + + NSURL *_URL; + UIImage *_defaultImage; + + NSInteger _cacheSentinel; + id _downloadIdentifier; + // The download identifier that we have set a progress block on, if any. + id _downloadIdentifierForProgressBlock; + + BOOL _imageLoaded; + BOOL _imageWasSetExternally; + CGFloat _currentImageQuality; + CGFloat _renderedImageQuality; + BOOL _shouldRenderProgressImages; + + struct { + unsigned int delegateWillStartDisplayAsynchronously:1; + unsigned int delegateWillLoadImageFromCache:1; + unsigned int delegateWillLoadImageFromNetwork:1; + unsigned int delegateDidStartFetchingData:1; + unsigned int delegateDidFailWithError:1; + unsigned int delegateDidFinishDecoding:1; + unsigned int delegateDidLoadImage:1; + unsigned int delegateDidLoadImageFromCache:1; + unsigned int delegateDidLoadImageWithInfo:1; + } _delegateFlags; + + + // Immutable and set on init only. We don't need to lock in this case. + __weak id _downloader; + struct { + unsigned int downloaderImplementsSetProgress:1; + unsigned int downloaderImplementsSetPriority:1; + unsigned int downloaderImplementsAnimatedImage:1; + unsigned int downloaderImplementsCancelWithResume:1; + unsigned int downloaderImplementsDownloadWithPriority:1; + } _downloaderFlags; + + // Immutable and set on init only. We don't need to lock in this case. + __weak id _cache; + struct { + unsigned int cacheSupportsClearing:1; + unsigned int cacheSupportsSynchronousFetch:1; + } _cacheFlags; +} + +@end + +@implementation ASNetworkImageNode + +static std::atomic_bool _useMainThreadDelegateCallbacks(true); + +@dynamic image; + +- (instancetype)initWithCache:(id)cache downloader:(id)downloader +{ + if (!(self = [super init])) + return nil; + + _cache = (id)cache; + _downloader = (id)downloader; + + _downloaderFlags.downloaderImplementsSetProgress = [downloader respondsToSelector:@selector(setProgressImageBlock:callbackQueue:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsAnimatedImage = [downloader respondsToSelector:@selector(animatedImageWithData:)]; + _downloaderFlags.downloaderImplementsCancelWithResume = [downloader respondsToSelector:@selector(cancelImageDownloadWithResumePossibilityForIdentifier:)]; + _downloaderFlags.downloaderImplementsDownloadWithPriority = [downloader respondsToSelector:@selector(downloadImageWithURL:priority:callbackQueue:downloadProgress:completion:)]; + + _cacheFlags.cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; + _cacheFlags.cacheSupportsSynchronousFetch = [cache respondsToSelector:@selector(synchronouslyFetchedCachedImageWithURL:)]; + + _shouldCacheImage = YES; + _shouldRenderProgressImages = YES; + self.shouldBypassEnsureDisplay = YES; + + return self; +} + +- (instancetype)init +{ +#if AS_PIN_REMOTE_IMAGE + return [self initWithCache:[ASPINRemoteImageDownloader sharedDownloader] downloader:[ASPINRemoteImageDownloader sharedDownloader]]; +#else + return [self initWithCache:nil downloader:[ASBasicImageDownloader sharedImageDownloader]]; +#endif +} + +- (void)dealloc +{ + [self _cancelImageDownloadWithResumePossibility:NO]; +} + +- (dispatch_queue_t)callbackQueue +{ + return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); +} + +#pragma mark - Public methods -- must lock + +/// Setter for public image property. It has the side effect of setting an internal _imageWasSetExternally that prevents setting an image internally. Setting an image internally should happen with the _setImage: method +- (void)setImage:(UIImage *)image +{ + ASLockScopeSelf(); + [self _locked_setImage:image]; +} + +- (void)_locked_setImage:(UIImage *)image +{ + ASAssertLocked(__instanceLock__); + + BOOL imageWasSetExternally = (image != nil); + BOOL shouldCancelAndClear = imageWasSetExternally && (imageWasSetExternally != _imageWasSetExternally); + _imageWasSetExternally = imageWasSetExternally; + if (shouldCancelAndClear) { + ASDisplayNodeAssertNil(_URL, @"Directly setting an image on an ASNetworkImageNode causes it to behave like an ASImageNode instead of an ASNetworkImageNode. If this is what you want, set the URL to nil first."); + _URL = nil; + [self _locked_cancelDownloadAndClearImageWithResumePossibility:NO]; + } + + // If our image is being set externally, the image quality is 100% + if (imageWasSetExternally) { + [self _setCurrentImageQuality:1.0]; + } + + [self _locked__setImage:image]; +} + +/// Setter for private image property. See @c _locked_setImage why this is needed +- (void)_setImage:(UIImage *)image +{ + ASLockScopeSelf(); + [self _locked__setImage:image]; +} + +- (void)_locked__setImage:(UIImage *)image +{ + ASAssertLocked(__instanceLock__); + [super _locked_setImage:image]; +} + +// Deprecated +- (void)setURLs:(NSArray *)URLs +{ + [self setURL:[URLs firstObject]]; +} + +// Deprecated +- (NSArray *)URLs +{ + return @[self.URL]; +} + +- (void)setURL:(NSURL *)URL +{ + [self setURL:URL resetToDefault:YES]; +} + +- (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset +{ + { + ASLockScopeSelf(); + + if (ASObjectIsEqual(URL, _URL)) { + return; + } + + URL = [URL copy]; + + ASDisplayNodeAssert(_imageWasSetExternally == NO, @"Setting a URL to an ASNetworkImageNode after setting an image changes its behavior from an ASImageNode to an ASNetworkImageNode. If this is what you want, set the image to nil first."); + + _imageWasSetExternally = NO; + + [self _locked_cancelImageDownloadWithResumePossibility:NO]; + + _imageLoaded = NO; + + _URL = URL; + + // If URL is nil and URL was not equal to _URL (checked at the top), then we previously had a URL but it's been nil'd out. + BOOL hadURL = (URL == nil); + if (reset || hadURL) { + [self _setCurrentImageQuality:(hadURL ? 0.0 : 1.0)]; + [self _locked__setImage:_defaultImage]; + } + } + + [self setNeedsPreload]; +} + +- (NSURL *)URL +{ + return ASLockedSelf(_URL); +} + +- (void)setDefaultImage:(UIImage *)defaultImage +{ + ASLockScopeSelf(); + + [self _locked_setDefaultImage:defaultImage]; +} + +- (void)_locked_setDefaultImage:(UIImage *)defaultImage +{ + if (ASObjectIsEqual(defaultImage, _defaultImage)) { + return; + } + + _defaultImage = defaultImage; + + if (!_imageLoaded) { + [self _setCurrentImageQuality:((_URL == nil) ? 0.0 : 1.0)]; + [self _locked__setImage:defaultImage]; + } +} + +- (UIImage *)defaultImage +{ + return ASLockedSelf(_defaultImage); +} + +- (void)setCurrentImageQuality:(CGFloat)currentImageQuality +{ + ASLockScopeSelf(); + _currentImageQuality = currentImageQuality; +} + +- (CGFloat)currentImageQuality +{ + return ASLockedSelf(_currentImageQuality); +} + +/** + * Always use these methods internally to update the current image quality + * We want to maintain the order that currentImageQuality is set regardless of the calling thread, + * so we always have to dispatch to the main thread to ensure that we queue the operations in the correct order. + * (see comment in displayDidFinish) + */ +- (void)_setCurrentImageQuality:(CGFloat)imageQuality +{ + dispatch_async(dispatch_get_main_queue(), ^{ + self.currentImageQuality = imageQuality; + }); +} + +- (void)setRenderedImageQuality:(CGFloat)renderedImageQuality +{ + ASLockScopeSelf(); + _renderedImageQuality = renderedImageQuality; +} + +- (CGFloat)renderedImageQuality +{ + ASLockScopeSelf(); + return _renderedImageQuality; +} + +- (void)setDelegate:(id)delegate +{ + ASLockScopeSelf(); + _delegate = delegate; + + _delegateFlags.delegateWillStartDisplayAsynchronously = [delegate respondsToSelector:@selector(imageNodeWillStartDisplayAsynchronously:)]; + _delegateFlags.delegateWillLoadImageFromCache = [delegate respondsToSelector:@selector(imageNodeWillLoadImageFromCache:)]; + _delegateFlags.delegateWillLoadImageFromNetwork = [delegate respondsToSelector:@selector(imageNodeWillLoadImageFromNetwork:)]; + _delegateFlags.delegateDidStartFetchingData = [delegate respondsToSelector:@selector(imageNodeDidStartFetchingData:)]; + _delegateFlags.delegateDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)]; + _delegateFlags.delegateDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]; + _delegateFlags.delegateDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)]; + _delegateFlags.delegateDidLoadImageFromCache = [delegate respondsToSelector:@selector(imageNodeDidLoadImageFromCache:)]; + _delegateFlags.delegateDidLoadImageWithInfo = [delegate respondsToSelector:@selector(imageNode:didLoadImage:info:)]; +} + +- (id)delegate +{ + ASLockScopeSelf(); + return _delegate; +} + +- (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages +{ + if (ASLockedSelfCompareAssign(_shouldRenderProgressImages, shouldRenderProgressImages)) { + [self _updateProgressImageBlockOnDownloaderIfNeeded]; + } +} + +- (BOOL)shouldRenderProgressImages +{ + ASLockScopeSelf(); + return _shouldRenderProgressImages; +} + +- (BOOL)placeholderShouldPersist +{ + ASLockScopeSelf(); + return (self.image == nil && self.animatedImage == nil && _URL != nil); +} + +/* displayWillStartAsynchronously: in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary + in ASMultiplexImageNode as well. */ +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + [super displayWillStartAsynchronously:asynchronously]; + + id delegate; + BOOL notifyDelegate; + { + ASLockScopeSelf(); + notifyDelegate = _delegateFlags.delegateWillStartDisplayAsynchronously; + delegate = _delegate; + } + if (notifyDelegate) { + [delegate imageNodeWillStartDisplayAsynchronously:self]; + } + + if (asynchronously == NO && _cacheFlags.cacheSupportsSynchronousFetch) { + ASLockScopeSelf(); + + NSURL *url = _URL; + if (_imageLoaded == NO && url && _downloadIdentifier == nil) { + UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:url] asdk_image]; + if (result) { + [self _setCurrentImageQuality:1.0]; + [self _locked__setImage:result]; + _imageLoaded = YES; + + // Call out to the delegate. + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASUnlockScope(self); + const auto info = [[ASNetworkImageLoadInfo alloc] initWithURL:url sourceType:ASNetworkImageSourceSynchronousCache downloadIdentifier:nil userInfo:nil]; + [delegate imageNode:self didLoadImage:result info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASUnlockScope(self); + [delegate imageNode:self didLoadImage:result]; + } + } + } + } + + if (self.image == nil) { + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityImminent]; + } +} + +/* visibileStateDidChange in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary + in ASMultiplexImageNode as well. */ +- (void)didEnterVisibleState +{ + [super didEnterVisibleState]; + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityVisible]; + [self _updateProgressImageBlockOnDownloaderIfNeeded]; +} + +- (void)didExitVisibleState +{ + [super didExitVisibleState]; + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityPreload]; + [self _updateProgressImageBlockOnDownloaderIfNeeded]; +} + +- (void)didExitDisplayState +{ + [super didExitDisplayState]; + if (ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityPreload]; + } +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + + // If the image was set explicitly we don't want to remove it while exiting the preload state + if (ASLockedSelf(_imageWasSetExternally)) { + return; + } + + [self _cancelDownloadAndClearImageWithResumePossibility:YES]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + + // Image was set externally no need to load an image + [self _lazilyLoadImageIfNecessary]; +} + ++ (void)setUseMainThreadDelegateCallbacks:(BOOL)useMainThreadDelegateCallbacks +{ + _useMainThreadDelegateCallbacks = useMainThreadDelegateCallbacks; +} + ++ (BOOL)useMainThreadDelegateCallbacks +{ + return _useMainThreadDelegateCallbacks; +} + +#pragma mark - Progress + +- (void)handleProgressImage:(UIImage *)progressImage progress:(CGFloat)progress downloadIdentifier:(nullable id)downloadIdentifier +{ + ASLockScopeSelf(); + + // Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; + } + + as_log_verbose(ASImageLoadingLog(), "Received progress image for %@ q: %.2g id: %@", self, progress, progressImage); + [self _setCurrentImageQuality:progress]; + [self _locked__setImage:progressImage]; +} + +- (void)_updatePriorityOnDownloaderIfNeededWithDefaultPriority:(ASImageDownloaderPriority)defaultPriority +{ + if (_downloaderFlags.downloaderImplementsSetPriority) { + ASLockScopeSelf(); + + if (_downloadIdentifier != nil) { + ASImageDownloaderPriority priority = defaultPriority; + if (ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + priority = ASImageDownloaderPriorityWithInterfaceState(_interfaceState); + } + + [_downloader setPriority:priority withDownloadIdentifier:_downloadIdentifier]; + } + } +} + +- (void)_updateProgressImageBlockOnDownloaderIfNeeded +{ + // If the downloader doesn't do progress, we are done. + if (_downloaderFlags.downloaderImplementsSetProgress == NO) { + return; + } + + // Read state. + [self lock]; + BOOL shouldRender = _shouldRenderProgressImages && ASInterfaceStateIncludesVisible(_interfaceState); + id oldDownloadIDForProgressBlock = _downloadIdentifierForProgressBlock; + id newDownloadIDForProgressBlock = shouldRender ? _downloadIdentifier : nil; + BOOL clearAndReattempt = NO; + [self unlock]; + + // If we're already bound to the correct download, we're done. + if (ASObjectIsEqual(oldDownloadIDForProgressBlock, newDownloadIDForProgressBlock)) { + return; + } + + // Unbind from the previous download. + if (oldDownloadIDForProgressBlock != nil) { + as_log_verbose(ASImageLoadingLog(), "Disabled progress images for %@ id: %@", self, oldDownloadIDForProgressBlock); + [_downloader setProgressImageBlock:nil callbackQueue:[self callbackQueue] withDownloadIdentifier:oldDownloadIDForProgressBlock]; + } + + // Bind to the current download. + if (newDownloadIDForProgressBlock != nil) { + __weak __typeof(self) weakSelf = self; + as_log_verbose(ASImageLoadingLog(), "Enabled progress images for %@ id: %@", self, newDownloadIDForProgressBlock); + [_downloader setProgressImageBlock:^(UIImage * _Nonnull progressImage, CGFloat progress, id _Nullable downloadIdentifier) { + [weakSelf handleProgressImage:progressImage progress:progress downloadIdentifier:downloadIdentifier]; + } callbackQueue:[self callbackQueue] withDownloadIdentifier:newDownloadIDForProgressBlock]; + } + + // Update state local state with lock held. + { + ASLockScopeSelf(); + // Check if the oldDownloadIDForProgressBlock still is the same as the _downloadIdentifierForProgressBlock + if (_downloadIdentifierForProgressBlock == oldDownloadIDForProgressBlock) { + _downloadIdentifierForProgressBlock = newDownloadIDForProgressBlock; + } else if (newDownloadIDForProgressBlock != nil) { + // If this is not the case another thread did change the _downloadIdentifierForProgressBlock already so + // we have to deregister the newDownloadIDForProgressBlock that we registered above + clearAndReattempt = YES; + } + } + + if (clearAndReattempt) { + // In this case another thread changed the _downloadIdentifierForProgressBlock before we finished registering + // the new progress block for newDownloadIDForProgressBlock ID. Let's clear it now and reattempt to register + if (newDownloadIDForProgressBlock) { + [_downloader setProgressImageBlock:nil callbackQueue:[self callbackQueue] withDownloadIdentifier:newDownloadIDForProgressBlock]; + } + [self _updateProgressImageBlockOnDownloaderIfNeeded]; + } +} + +- (void)_cancelDownloadAndClearImageWithResumePossibility:(BOOL)storeResume +{ + ASLockScopeSelf(); + [self _locked_cancelDownloadAndClearImageWithResumePossibility:storeResume]; +} + +- (void)_locked_cancelDownloadAndClearImageWithResumePossibility:(BOOL)storeResume +{ + ASAssertLocked(__instanceLock__); + + [self _locked_cancelImageDownloadWithResumePossibility:storeResume]; + + [self _locked_setAnimatedImage:nil]; + [self _setCurrentImageQuality:0.0]; + [self _locked__setImage:_defaultImage]; + + _imageLoaded = NO; + + if (_cacheFlags.cacheSupportsClearing) { + if (_URL != nil) { + as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URL); + [_cache clearFetchedImageFromCacheWithURL:_URL]; + } + } +} + +- (void)_cancelImageDownloadWithResumePossibility:(BOOL)storeResume +{ + ASLockScopeSelf(); + [self _locked_cancelImageDownloadWithResumePossibility:storeResume]; +} + +- (void)_locked_cancelImageDownloadWithResumePossibility:(BOOL)storeResume +{ + ASAssertLocked(__instanceLock__); + + if (!_downloadIdentifier) { + return; + } + + if (_downloadIdentifier) { + if (storeResume && _downloaderFlags.downloaderImplementsCancelWithResume) { + as_log_verbose(ASImageLoadingLog(), "Canceling image download w resume for %@ id: %@", self, _downloadIdentifier); + [_downloader cancelImageDownloadWithResumePossibilityForIdentifier:_downloadIdentifier]; + } else { + as_log_verbose(ASImageLoadingLog(), "Canceling image download no resume for %@ id: %@", self, _downloadIdentifier); + [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; + } + } + _downloadIdentifier = nil; + _cacheSentinel++; +} + +- (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier, id userInfo))finished +{ + ASPerformBlockOnBackgroundThread(^{ + NSURL *url; + id downloadIdentifier; + BOOL cancelAndReattempt = NO; + ASInterfaceState interfaceState; + + // Below, to avoid performance issues, we're calling downloadImageWithURL without holding the lock. This is a bit ugly because + // We need to reobtain the lock after and ensure that the task we've kicked off still matches our URL. If not, we need to cancel + // it and try again. + { + ASLockScopeSelf(); + url = self->_URL; + interfaceState = self->_interfaceState; + } + + dispatch_queue_t callbackQueue = [self callbackQueue]; + ASImageDownloaderProgress downloadProgress = NULL; + ASImageDownloaderCompletion completion = ^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { + if (finished != NULL) { + finished(imageContainer, error, downloadIdentifier, userInfo); + } + }; + + if (self->_downloaderFlags.downloaderImplementsDownloadWithPriority + && ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + /* + Decide a priority based on the current interface state of this node. + It can happen that this method was called when the node entered preload state + but the interface state, at this point, tells us that the node is (going to be) visible. + If that's the case, we jump to a higher priority directly. + */ + ASImageDownloaderPriority priority = ASImageDownloaderPriorityWithInterfaceState(interfaceState); + + downloadIdentifier = [self->_downloader downloadImageWithURL:url + priority:priority + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; + } else { + /* + Kick off a download with default priority. + The actual "default" value is decided by the downloader. + ASBasicImageDownloader and ASPINRemoteImageDownloader both use ASImageDownloaderPriorityImminent + which is mapped to NSURLSessionTaskPriorityDefault. + + This means that preload and display nodes use the same priority + and their requests are put into the same pool. + */ + downloadIdentifier = [self->_downloader downloadImageWithURL:url + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; + } + as_log_verbose(ASImageLoadingLog(), "Downloading image for %@ url: %@", self, url); + + { + ASLockScopeSelf(); + if (ASObjectIsEqual(self->_URL, url)) { + // The download we kicked off is correct, no need to do any more work. + self->_downloadIdentifier = downloadIdentifier; + } else { + // The URL changed since we kicked off our download task. This shouldn't happen often so we'll pay the cost and + // cancel that request and kick off a new one. + cancelAndReattempt = YES; + } + } + + if (cancelAndReattempt) { + if (downloadIdentifier != nil) { + as_log_verbose(ASImageLoadingLog(), "Canceling image download no resume for %@ id: %@", self, downloadIdentifier); + [self->_downloader cancelImageDownloadForIdentifier:downloadIdentifier]; + } + [self _downloadImageWithCompletion:finished]; + return; + } + + [self _updateProgressImageBlockOnDownloaderIfNeeded]; + }); +} + +- (void)_lazilyLoadImageIfNecessary +{ + ASDisplayNodeAssertMainThread(); + + [self lock]; + __weak id delegate = _delegate; + BOOL delegateDidStartFetchingData = _delegateFlags.delegateDidStartFetchingData; + BOOL delegateWillLoadImageFromCache = _delegateFlags.delegateWillLoadImageFromCache; + BOOL delegateWillLoadImageFromNetwork = _delegateFlags.delegateWillLoadImageFromNetwork; + BOOL delegateDidLoadImageFromCache = _delegateFlags.delegateDidLoadImageFromCache; + BOOL isImageLoaded = _imageLoaded; + NSURL *URL = _URL; + id currentDownloadIdentifier = _downloadIdentifier; + [self unlock]; + + if (!isImageLoaded && URL != nil && currentDownloadIdentifier == nil) { + if (delegateDidStartFetchingData) { + [delegate imageNodeDidStartFetchingData:self]; + } + + if (URL.isFileURL) { + dispatch_async(dispatch_get_main_queue(), ^{ + ASLockScopeSelf(); + + // Bail out if not the same URL anymore + if (!ASObjectIsEqual(URL, self->_URL)) { + return; + } + + if (self->_shouldCacheImage) { + [self _locked__setImage:[UIImage imageNamed:URL.path.lastPathComponent]]; + } else { + // First try to load the path directly, for efficiency assuming a developer who + // doesn't want caching is trying to be as minimal as possible. + auto nonAnimatedImage = [[UIImage alloc] initWithContentsOfFile:URL.path]; + if (nonAnimatedImage == nil) { + // If we couldn't find it, execute an -imageNamed:-like search so we can find resources even if the + // extension is not provided in the path. This allows the same path to work regardless of shouldCacheImage. + NSString *filename = [[NSBundle mainBundle] pathForResource:URL.path.lastPathComponent ofType:nil]; + if (filename != nil) { + nonAnimatedImage = [[UIImage alloc] initWithContentsOfFile:filename]; + } + } + + // If the file may be an animated gif and then created an animated image. + id animatedImage = nil; + if (self->_downloaderFlags.downloaderImplementsAnimatedImage) { + const auto data = [[NSData alloc] initWithContentsOfURL:URL]; + if (data != nil) { + animatedImage = [self->_downloader animatedImageWithData:data]; + + if ([animatedImage respondsToSelector:@selector(isDataSupported:)] && [animatedImage isDataSupported:data] == NO) { + animatedImage = nil; + } + } + } + + if (animatedImage != nil) { + [self _locked_setAnimatedImage:animatedImage]; + } else { + [self _locked__setImage:nonAnimatedImage]; + } + } + + self->_imageLoaded = YES; + + [self _setCurrentImageQuality:1.0]; + + if (self->_delegateFlags.delegateDidLoadImageWithInfo) { + ASUnlockScope(self); + const auto info = [[ASNetworkImageLoadInfo alloc] initWithURL:URL sourceType:ASNetworkImageSourceFileURL downloadIdentifier:nil userInfo:nil]; + [delegate imageNode:self didLoadImage:self.image info:info]; + } else if (self->_delegateFlags.delegateDidLoadImage) { + ASUnlockScope(self); + [delegate imageNode:self didLoadImage:self.image]; + } + }); + } else { + __weak __typeof__(self) weakSelf = self; + const auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSourceType imageSource, id userInfo) { + ASPerformBlockOnBackgroundThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + + // Grab the lock for the rest of the block + ASLockScope(strongSelf); + + //Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; + } + + //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) + if (ASInterfaceStateIncludesPreload(strongSelf->_interfaceState) == NO) { + strongSelf->_downloadIdentifier = nil; + strongSelf->_cacheSentinel++; + return; + } + + UIImage *newImage; + if (imageContainer != nil) { + [strongSelf _setCurrentImageQuality:1.0]; + NSData *animatedImageData = [imageContainer asdk_animatedImageData]; + if (animatedImageData && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) { + id animatedImage = [strongSelf->_downloader animatedImageWithData:animatedImageData]; + [strongSelf _locked_setAnimatedImage:animatedImage]; + } else { + newImage = [imageContainer asdk_image]; + [strongSelf _locked__setImage:newImage]; + } + strongSelf->_imageLoaded = YES; + } + + strongSelf->_downloadIdentifier = nil; + strongSelf->_cacheSentinel++; + + void (^calloutBlock)(ASNetworkImageNode *inst); + + if (newImage) { + if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) { + calloutBlock = ^(ASNetworkImageNode *strongSelf) { + const auto info = [[ASNetworkImageLoadInfo alloc] initWithURL:URL sourceType:imageSource downloadIdentifier:downloadIdentifier userInfo:userInfo]; + [delegate imageNode:strongSelf didLoadImage:newImage info:info]; + }; + } else if (strongSelf->_delegateFlags.delegateDidLoadImage) { + calloutBlock = ^(ASNetworkImageNode *strongSelf) { + [delegate imageNode:strongSelf didLoadImage:newImage]; + }; + } + } else if (error && strongSelf->_delegateFlags.delegateDidFailWithError) { + calloutBlock = ^(ASNetworkImageNode *strongSelf) { + [delegate imageNode:strongSelf didFailWithError:error]; + }; + } + + if (calloutBlock) { + if (ASNetworkImageNode.useMainThreadDelegateCallbacks) { + ASPerformBlockOnMainThread(^{ + if (auto strongSelf = weakSelf) { + calloutBlock(strongSelf); + } + }); + } else { + calloutBlock(strongSelf); + } + } + }); + }; + + // As the _cache and _downloader is only set once in the intializer we don't have to use a + // lock in here + if (_cache != nil) { + NSInteger cacheSentinel = ASLockedSelf(++_cacheSentinel); + + as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ url: %@", self, URL); + + ASImageCacherCompletion completion = ^(id imageContainer) { + // If the cache sentinel changed, that means this request was cancelled. + if (ASLockedSelf(self->_cacheSentinel != cacheSentinel)) { + return; + } + + if ([imageContainer asdk_image] == nil && self->_downloader != nil) { + if (delegateWillLoadImageFromNetwork) { + [delegate imageNodeWillLoadImageFromNetwork:self]; + } + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload, userInfo); + }]; + } else { + if (delegateDidLoadImageFromCache) { + [delegate imageNodeDidLoadImageFromCache:self]; + } + as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache, nil); + } + }; + + if (delegateWillLoadImageFromCache) { + [delegate imageNodeWillLoadImageFromCache:self]; + } + [_cache cachedImageWithURL:URL + callbackQueue:[self callbackQueue] + completion:completion]; + } else { + if (delegateWillLoadImageFromNetwork) { + [delegate imageNodeWillLoadImageFromNetwork:self]; + } + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload, userInfo); + }]; + } + } + } +} + +#pragma mark - ASDisplayNode+Subclasses + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + id delegate = nil; + + { + ASLockScopeSelf(); + if (_delegateFlags.delegateDidFinishDecoding && self.layer.contents != nil) { + /* We store the image quality in _currentImageQuality whenever _image is set. On the following displayDidFinish, we'll know that + _currentImageQuality is the quality of the image that has just finished rendering. In order for this to be accurate, we + need to be sure we are on main thread when we set _currentImageQuality. Otherwise, it is possible for _currentImageQuality + to be modified at a point where it is too late to cancel the main thread's previous display (the final sentinel check has passed), + but before the displayDidFinish of the previous display pass is called. In this situation, displayDidFinish would be called and we + would set _renderedImageQuality to the new _currentImageQuality, but the actual quality of the rendered image should be the previous + value stored in _currentImageQuality. */ + + _renderedImageQuality = _currentImageQuality; + + // Assign the delegate to be used + delegate = _delegate; + } + } + + if (delegate != nil) { + [delegate imageNodeDidFinishDecoding:self]; + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASNodeController+Beta.h b/submodules/AsyncDisplayKit/Source/ASNodeController+Beta.h new file mode 100644 index 0000000000..e9028f1986 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNodeController+Beta.h @@ -0,0 +1,59 @@ +// +// ASNodeController+Beta.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import // for ASInterfaceState protocol + +/* ASNodeController is currently beta and open to change in the future */ +@interface ASNodeController<__covariant DisplayNodeType : ASDisplayNode *> + : NSObject + +@property (nonatomic, strong /* may be weak! */) DisplayNodeType node; + +// Until an ASNodeController can be provided in place of an ASCellNode, some apps may prefer to have +// nodes keep their controllers alive (and a weak reference from controller to node) + +@property (nonatomic) BOOL shouldInvertStrongReference; + +- (void)loadNode; + +// for descriptions see definition +- (void)nodeDidLoad ASDISPLAYNODE_REQUIRES_SUPER; +- (void)nodeDidLayout ASDISPLAYNODE_REQUIRES_SUPER; + +// This is only called during Yoga-driven layouts. +- (void)nodeWillCalculateLayout:(ASSizeRange)constrainedSize ASDISPLAYNODE_REQUIRES_SUPER; + +- (void)didEnterVisibleState ASDISPLAYNODE_REQUIRES_SUPER; +- (void)didExitVisibleState ASDISPLAYNODE_REQUIRES_SUPER; + +- (void)didEnterDisplayState ASDISPLAYNODE_REQUIRES_SUPER; +- (void)didExitDisplayState ASDISPLAYNODE_REQUIRES_SUPER; + +- (void)didEnterPreloadState ASDISPLAYNODE_REQUIRES_SUPER; +- (void)didExitPreloadState ASDISPLAYNODE_REQUIRES_SUPER; + +- (void)interfaceStateDidChange:(ASInterfaceState)newState + fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER; + +- (void)hierarchyDisplayDidFinish ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @discussion Attempts (via ASLockSequence, a backing-off spinlock similar to + * std::lock()) to lock both the node and its ASNodeController, if one exists. + */ +- (ASLockSet)lockPair; + +@end + +@interface ASDisplayNode (ASNodeController) + +@property(nonatomic, readonly) ASNodeController *nodeController; + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASNodeController+Beta.mm b/submodules/AsyncDisplayKit/Source/ASNodeController+Beta.mm new file mode 100644 index 0000000000..14aae33633 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASNodeController+Beta.mm @@ -0,0 +1,135 @@ +// +// ASNodeController+Beta.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#define _node (_shouldInvertStrongReference ? _weakNode : _strongNode) + +@implementation ASNodeController +{ + ASDisplayNode *_strongNode; + __weak ASDisplayNode *_weakNode; + AS::RecursiveMutex __instanceLock__; +} + +- (void)loadNode +{ + ASLockScopeSelf(); + self.node = [[ASDisplayNode alloc] init]; +} + +- (ASDisplayNode *)node +{ + ASLockScopeSelf(); + if (_node == nil) { + [self loadNode]; + } + return _node; +} + +- (void)setupReferencesWithNode:(ASDisplayNode *)node +{ + ASLockScopeSelf(); + if (_shouldInvertStrongReference) { + // The node should own the controller; weak reference from controller to node. + _weakNode = node; + _strongNode = nil; + } else { + // The controller should own the node; weak reference from node to controller. + _strongNode = node; + _weakNode = nil; + } + + [node __setNodeController:self]; +} + +- (void)setNode:(ASDisplayNode *)node +{ + ASLockScopeSelf(); + if (node == _node) { + return; + } + [self setupReferencesWithNode:node]; + [node addInterfaceStateDelegate:self]; +} + +- (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference +{ + ASLockScopeSelf(); + if (_shouldInvertStrongReference != shouldInvertStrongReference) { + // Because the BOOL controls which ivar we access, get the node before toggling. + ASDisplayNode *node = _node; + _shouldInvertStrongReference = shouldInvertStrongReference; + [self setupReferencesWithNode:node]; + } +} + +// subclass overrides +- (void)nodeDidLoad {} +- (void)nodeDidLayout {} +- (void)nodeWillCalculateLayout:(ASSizeRange)constrainedSize {} + +- (void)didEnterVisibleState {} +- (void)didExitVisibleState {} + +- (void)didEnterDisplayState {} +- (void)didExitDisplayState {} + +- (void)didEnterPreloadState {} +- (void)didExitPreloadState {} + +- (void)interfaceStateDidChange:(ASInterfaceState)newState + fromState:(ASInterfaceState)oldState {} + +- (void)hierarchyDisplayDidFinish {} + +- (ASLockSet)lockPair { + ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { + if (!addLock(_node)) { + return NO; + } + if (!addLock(self)) { + return NO; + } + return YES; + }); + + return lockSet; +} + +#pragma mark NSLocking + +- (void)lock +{ + __instanceLock__.lock(); +} + +- (void)unlock +{ + __instanceLock__.unlock(); +} + +- (BOOL)tryLock +{ + return __instanceLock__.try_lock(); +} + +@end + +@implementation ASDisplayNode (ASNodeController) + +- (ASNodeController *)nodeController +{ + return _weakNodeController ?: _strongNodeController; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASPagerFlowLayout.h b/submodules/AsyncDisplayKit/Source/ASPagerFlowLayout.h new file mode 100644 index 0000000000..8b2f7e2684 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPagerFlowLayout.h @@ -0,0 +1,22 @@ +// +// ASPagerFlowLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASPagerFlowLayout : UICollectionViewFlowLayout + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASPagerFlowLayout.mm b/submodules/AsyncDisplayKit/Source/ASPagerFlowLayout.mm new file mode 100644 index 0000000000..de20b08961 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPagerFlowLayout.mm @@ -0,0 +1,113 @@ +// +// ASPagerFlowLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@interface ASPagerFlowLayout () { + __weak ASCellNode *_currentCellNode; +} + +@end + +//TODO make this an ASCollectionViewLayout +@implementation ASPagerFlowLayout + +- (ASCollectionView *)asCollectionView +{ + // Dynamic cast is too slow and not worth it. + return (ASCollectionView *)self.collectionView; +} + +- (void)prepareLayout +{ + [super prepareLayout]; + if (_currentCellNode == nil) { + [self _updateCurrentNode]; + } +} + +- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset +{ + // Don't mess around if the user is interacting with the page node. Although if just a rotation happened we should + // try to use the current index path to not end up setting the target content offset to something in between pages + if (!self.collectionView.decelerating && !self.collectionView.tracking) { + NSIndexPath *indexPath = [self.asCollectionView indexPathForNode:_currentCellNode]; + if (indexPath) { + return [self _targetContentOffsetForItemAtIndexPath:indexPath proposedContentOffset:proposedContentOffset]; + } + } + + return [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; +} + +- (CGPoint)_targetContentOffsetForItemAtIndexPath:(NSIndexPath *)indexPath proposedContentOffset:(CGPoint)proposedContentOffset +{ + if ([self _dataSourceIsEmpty]) { + return proposedContentOffset; + } + + UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; + if (attributes == nil) { + return proposedContentOffset; + } + + CGFloat xOffset = (CGRectGetWidth(self.collectionView.bounds) - CGRectGetWidth(attributes.frame)) / 2.0; + return CGPointMake(attributes.frame.origin.x - xOffset, proposedContentOffset.y); +} + +- (BOOL)_dataSourceIsEmpty +{ + return ([self.collectionView numberOfSections] == 0 || + [self.collectionView numberOfItemsInSection:0] == 0); +} + +- (void)_updateCurrentNode +{ + // Never change node during an animated bounds change (rotation) + // NOTE! Listening for -prepareForAnimatedBoundsChange and -finalizeAnimatedBoundsChange + // isn't sufficient here! It's broken! + NSArray *animKeys = self.collectionView.layer.animationKeys; + for (NSString *key in animKeys) { + if ([key hasPrefix:@"bounds"]) { + return; + } + } + + CGRect bounds = self.collectionView.bounds; + CGRect rect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 1, 1); + + NSIndexPath *indexPath = [self layoutAttributesForElementsInRect:rect].firstObject.indexPath; + if (indexPath) { + ASCellNode *node = [self.asCollectionView nodeForItemAtIndexPath:indexPath]; + if (node) { + _currentCellNode = node; + } + } +} + +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds +{ + [self _updateCurrentNode]; + return [super shouldInvalidateLayoutForBoundsChange:newBounds]; +} + +- (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds +{ + UICollectionViewFlowLayoutInvalidationContext *ctx = (UICollectionViewFlowLayoutInvalidationContext *)[super invalidationContextForBoundsChange:newBounds]; + ctx.invalidateFlowLayoutDelegateMetrics = YES; + ctx.invalidateFlowLayoutAttributes = YES; + return ctx; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASPagerNode+Beta.h b/submodules/AsyncDisplayKit/Source/ASPagerNode+Beta.h new file mode 100644 index 0000000000..c4de4f45de --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPagerNode+Beta.h @@ -0,0 +1,17 @@ +// +// ASPagerNode+Beta.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +@interface ASPagerNode (Beta) + +- (instancetype)initUsingAsyncCollectionLayout; + +@end +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASPagerNode.h b/submodules/AsyncDisplayKit/Source/ASPagerNode.h new file mode 100644 index 0000000000..b20f187f67 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPagerNode.h @@ -0,0 +1,137 @@ +// +// ASPagerNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +@class ASPagerNode; +@class ASPagerFlowLayout; + +NS_ASSUME_NONNULL_BEGIN + +#define ASPagerNodeDataSource ASPagerDataSource +@protocol ASPagerDataSource + +/** + * This method replaces -collectionView:numberOfItemsInSection: + * + * @param pagerNode The sender. + * @return The total number of pages that can display in the pagerNode. + */ +- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode; + +@optional + +/** + * This method replaces -collectionView:nodeForItemAtIndexPath: + * + * @param pagerNode The sender. + * @param index The index of the requested node. + * @return a node for display at this index. This will be called on the main thread and should + * not implement reuse (it will be called once per row). Unlike UICollectionView's version, + * this method is not called when the row is about to display. + */ +- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index; + +/** + * This method replaces -collectionView:nodeBlockForItemAtIndexPath: + * This method takes precedence over pagerNode:nodeAtIndex: if implemented. + * + * @param pagerNode The sender. + * @param index The index of the requested node. + * @return a block that creates the node for display at this index. + * Must be thread-safe (can be called on the main thread or a background + * queue) and should not implement reuse (it will be called once per row). + */ +- (ASCellNodeBlock)pagerNode:(ASPagerNode *)pagerNode nodeBlockAtIndex:(NSInteger)index; + +@end + +@protocol ASPagerDelegate +@end + +/** + * A horizontal, paging collection node. + */ +@interface ASPagerNode : ASCollectionNode + +/** + * Configures a default horizontal, paging flow layout with 0 inter-item spacing. + */ +- (instancetype)init; + +/** + * Initializer with custom-configured flow layout properties. + * + * NOTE: The flow layout must have a horizontal scroll direction. + */ +- (instancetype)initWithCollectionViewLayout:(ASPagerFlowLayout *)flowLayout; + +/** + * Data Source is required, and uses a different protocol from ASCollectionNode. + */ +- (void)setDataSource:(nullable id )dataSource; +- (nullable id )dataSource AS_WARN_UNUSED_RESULT; + +/** + * Delegate is optional. + * This includes UIScrollViewDelegate as well as most methods from UICollectionViewDelegate, like willDisplay... + */ +- (void)setDelegate:(nullable id )delegate; +- (nullable id )delegate AS_WARN_UNUSED_RESULT; + +/** + * The underlying ASCollectionView object. + */ +@property (readonly) ASCollectionView *view; + +/** + * Returns the current page index. Main thread only. + */ +@property (nonatomic, readonly) NSInteger currentPageIndex; + +/** + * Scroll the contents of the receiver to ensure that the page is visible + */ +- (void)scrollToPageAtIndex:(NSInteger)index animated:(BOOL)animated; + +/** + * Returns the node for the passed page index + */ +- (ASCellNode *)nodeForPageAtIndex:(NSInteger)index AS_WARN_UNUSED_RESULT; + +/** + * Returns the index of the page for the cell passed or NSNotFound + */ +- (NSInteger)indexOfPageWithNode:(ASCellNode *)node; + +/** + * Tells the pager node to allow its view controller to automatically adjust its content insets. + * + * @see UIViewController.automaticallyAdjustsScrollViewInsets + * + * @discussion ASPagerNode should usually not have its content insets automatically adjusted + * because it scrolls horizontally, and flow layout will log errors because the pages + * do not fit between the top & bottom insets of the collection view. + * + * The default value is NO, which means that ASPagerNode expects that its view controller will + * have automaticallyAdjustsScrollViewInsets=NO. + * + * If this property is NO, but your view controller has automaticallyAdjustsScrollViewInsets=YES, + * the pager node will set the property on the view controller to NO and log a warning message. In the future, + * the pager node will just log the warning, and you'll need to configure your view controller on your own. + */ +@property (nonatomic) BOOL allowsAutomaticInsetsAdjustment; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASPagerNode.mm b/submodules/AsyncDisplayKit/Source/ASPagerNode.mm new file mode 100644 index 0000000000..73c9987c68 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPagerNode.mm @@ -0,0 +1,234 @@ +// +// ASPagerNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +@interface ASPagerNode () +{ + __weak id _pagerDataSource; + ASPagerNodeProxy *_proxyDataSource; + struct { + unsigned nodeBlockAtIndex:1; + unsigned nodeAtIndex:1; + } _pagerDataSourceFlags; + + __weak id _pagerDelegate; + ASPagerNodeProxy *_proxyDelegate; +} + +@end + +@implementation ASPagerNode + +@dynamic view, delegate, dataSource; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + ASPagerFlowLayout *flowLayout = [[ASPagerFlowLayout alloc] init]; + flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + flowLayout.minimumInteritemSpacing = 0; + flowLayout.minimumLineSpacing = 0; + + return [self initWithCollectionViewLayout:flowLayout]; +} + +- (instancetype)initWithCollectionViewLayout:(ASPagerFlowLayout *)flowLayout; +{ + ASDisplayNodeAssert([flowLayout isKindOfClass:[ASPagerFlowLayout class]], @"ASPagerNode requires a flow layout."); + ASDisplayNodeAssertTrue(flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal); + self = [super initWithCollectionViewLayout:flowLayout]; + return self; +} + +- (instancetype)initUsingAsyncCollectionLayout +{ + ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionHorizontalDirections]; + self = [super initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; + if (self) { + layoutDelegate.propertiesProvider = self; + } + return self; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + ASCollectionView *cv = self.view; + cv.asyncDataSource = (id)_proxyDataSource ?: self; + cv.asyncDelegate = (id)_proxyDelegate ?: self; +#if TARGET_OS_IOS + cv.pagingEnabled = YES; + cv.scrollsToTop = NO; +#endif + cv.allowsSelection = NO; + cv.showsVerticalScrollIndicator = NO; + cv.showsHorizontalScrollIndicator = NO; + + ASRangeTuningParameters minimumRenderParams = { .leadingBufferScreenfuls = 0.0, .trailingBufferScreenfuls = 0.0 }; + ASRangeTuningParameters minimumPreloadParams = { .leadingBufferScreenfuls = 1.0, .trailingBufferScreenfuls = 1.0 }; + [self setTuningParameters:minimumRenderParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay]; + [self setTuningParameters:minimumPreloadParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload]; + + ASRangeTuningParameters fullRenderParams = { .leadingBufferScreenfuls = 1.0, .trailingBufferScreenfuls = 1.0 }; + ASRangeTuningParameters fullPreloadParams = { .leadingBufferScreenfuls = 2.0, .trailingBufferScreenfuls = 2.0 }; + [self setTuningParameters:fullRenderParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay]; + [self setTuningParameters:fullPreloadParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload]; +} + +#pragma mark - Getters / Setters + +- (NSInteger)currentPageIndex +{ + return (self.view.contentOffset.x / [self pageSize].width); +} + +- (CGSize)pageSize +{ + UIEdgeInsets contentInset = self.contentInset; + CGSize pageSize = self.bounds.size; + pageSize.height -= (contentInset.top + contentInset.bottom); + return pageSize; +} + +#pragma mark - Helpers + +- (void)scrollToPageAtIndex:(NSInteger)index animated:(BOOL)animated +{ + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0]; + [self scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:animated]; +} + +- (ASCellNode *)nodeForPageAtIndex:(NSInteger)index +{ + return [self nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]]; +} + +- (NSInteger)indexOfPageWithNode:(ASCellNode *)node +{ + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (!indexPath) { + return NSNotFound; + } + return indexPath.row; +} + +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding + +- (CGSize)galleryLayoutDelegate:(nonnull ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(nonnull ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + return [self pageSize]; +} + +#pragma mark - ASCollectionDataSource + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_pagerDataSourceFlags.nodeBlockAtIndex) { + return [_pagerDataSource pagerNode:self nodeBlockAtIndex:indexPath.item]; + } else if (_pagerDataSourceFlags.nodeAtIndex) { + ASCellNode *node = [_pagerDataSource pagerNode:self nodeAtIndex:indexPath.item]; + return ^{ return node; }; + } else { + ASDisplayNodeFailAssert(@"Pager data source must implement either %@ or %@. Data source: %@", NSStringFromSelector(@selector(pagerNode:nodeBlockAtIndex:)), NSStringFromSelector(@selector(pagerNode:nodeAtIndex:)), _pagerDataSource); + return ^{ + return [[ASCellNode alloc] init]; + }; + } +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + ASDisplayNodeAssert(_pagerDataSource != nil, @"ASPagerNode must have a data source to load nodes to display"); + return [_pagerDataSource numberOfPagesInPagerNode:self]; +} + +#pragma mark - ASCollectionDelegate + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ASSizeRangeMake([self pageSize]); +} + +#pragma mark - Data Source Proxy + +- (id )dataSource +{ + return _pagerDataSource; +} + +- (void)setDataSource:(id )dataSource +{ + if (dataSource != _pagerDataSource) { + _pagerDataSource = dataSource; + + if (dataSource == nil) { + memset(&_pagerDataSourceFlags, 0, sizeof(_pagerDataSourceFlags)); + } else { + _pagerDataSourceFlags.nodeBlockAtIndex = [_pagerDataSource respondsToSelector:@selector(pagerNode:nodeBlockAtIndex:)]; + _pagerDataSourceFlags.nodeAtIndex = [_pagerDataSource respondsToSelector:@selector(pagerNode:nodeAtIndex:)]; + } + + _proxyDataSource = dataSource ? [[ASPagerNodeProxy alloc] initWithTarget:dataSource interceptor:self] : nil; + + super.dataSource = (id )_proxyDataSource; + } +} + +- (void)setDelegate:(id)delegate +{ + if (delegate != _pagerDelegate) { + _pagerDelegate = delegate; + _proxyDelegate = delegate ? [[ASPagerNodeProxy alloc] initWithTarget:delegate interceptor:self] : nil; + super.delegate = (id )_proxyDelegate; + } +} + +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy +{ + [self setDataSource:nil]; + [self setDelegate:nil]; +} + +- (void)didEnterHierarchy +{ + [super didEnterHierarchy]; + + // Check that our view controller does not automatically set our content insets + // In every use case I can imagine, the pager is not hosted inside a range-managed node. + if (_allowsAutomaticInsetsAdjustment == NO) { + UIViewController *vc = [self.view asdk_associatedViewController]; + if (vc.automaticallyAdjustsScrollViewInsets) { + NSLog(@"AsyncDisplayKit: ASPagerNode is setting automaticallyAdjustsScrollViewInsets=NO on its owning view controller %@. This automatic behavior will be disabled in the future. Set allowsAutomaticInsetsAdjustment=YES on the pager node to suppress this behavior.", vc); + vc.automaticallyAdjustsScrollViewInsets = NO; + } + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASRangeManagingNode.h b/submodules/AsyncDisplayKit/Source/ASRangeManagingNode.h new file mode 100644 index 0000000000..c331a77bee --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASRangeManagingNode.h @@ -0,0 +1,31 @@ +// +// ASRangeManagingNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@class ASCellNode; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Basically ASTableNode or ASCollectionNode. + */ +@protocol ASRangeManagingNode + +/** + * Retrieve the index path for the given node, if it's a member of this container. + * + * @param node The node. + * @return The index path, or nil if the node is not part of this container. + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)node; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.h b/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.h new file mode 100644 index 0000000000..07f3682bbf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.h @@ -0,0 +1,92 @@ +// +// ASRunLoopQueue.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASCATransactionQueueObserving +- (void)prepareForCATransactionCommit; +@end + +@interface ASAbstractRunLoopQueue : NSObject +@end + +AS_SUBCLASSING_RESTRICTED +@interface ASRunLoopQueue : ASAbstractRunLoopQueue + +/** + * Create a new queue with the given run loop and handler. + * + * @param runloop The run loop that will drive this queue. + * @param retainsObjects Whether the queue should retain its objects. + * @param handlerBlock An optional block to be run for each enqueued object. + * + * @discussion You may pass @c nil for the handler if you simply want the objects to + * be retained at enqueue time, and released during the run loop step. This is useful + * for creating a "main deallocation queue", as @c ASDeallocQueue creates its own + * worker thread with its own run loop. + */ +- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop + retainObjects:(BOOL)retainsObjects + handler:(nullable void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; + +- (void)enqueue:(ObjectType)object; + +@property (readonly) BOOL isEmpty; + +@property (nonatomic) NSUInteger batchSize; // Default == 1. +@property (nonatomic) BOOL ensureExclusiveMembership; // Default == YES. Set-like behavior. + +@end + + + +/** + * The queue to run on main run loop before CATransaction commit. + * + * @discussion this queue will run after ASRunLoopQueue and before CATransaction commit + * to get last chance of updating/coalesce info like interface state. + * Each node will only be called once per transaction commit to reflect interface change. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASCATransactionQueue : ASAbstractRunLoopQueue + +@property (readonly) BOOL isEmpty; + +@property (readonly, getter=isEnabled) BOOL enabled; + +- (void)enqueue:(id)object; + +@end + +extern ASCATransactionQueue *_ASSharedCATransactionQueue; +extern dispatch_once_t _ASSharedCATransactionQueueOnceToken; + +NS_INLINE ASCATransactionQueue *ASCATransactionQueueGet(void) { + dispatch_once(&_ASSharedCATransactionQueueOnceToken, ^{ + _ASSharedCATransactionQueue = [[ASCATransactionQueue alloc] init]; + }); + return _ASSharedCATransactionQueue; +} + +@interface ASDeallocQueue : NSObject + +@property (class, readonly) ASDeallocQueue *sharedDeallocationQueue; ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED; + +- (void)drain; + +- (void)releaseObjectInBackground:(id __strong _Nullable * _Nonnull)objectPtr; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm b/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm new file mode 100644 index 0000000000..4aefeeb89d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm @@ -0,0 +1,495 @@ +// +// ASRunLoopQueue.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#define ASRunLoopQueueLoggingEnabled 0 +#define ASRunLoopQueueVerboseLoggingEnabled 0 + +using AS::MutexLocker; + +static void runLoopSourceCallback(void *info) { + // No-op +#if ASRunLoopQueueVerboseLoggingEnabled + NSLog(@"<%@> - Called runLoopSourceCallback", info); +#endif +} + +#pragma mark - ASDeallocQueue + +@implementation ASDeallocQueue { + std::vector _queue; + AS::Mutex _lock; +} + ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED +{ + static ASDeallocQueue *deallocQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + deallocQueue = [[ASDeallocQueue alloc] init]; + }); + return deallocQueue; +} + +- (void)dealloc +{ + ASDisplayNodeFailAssert(@"Singleton should not dealloc."); +} + +- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr +{ + NSParameterAssert(objectPtr != NULL); + + // Cast to CFType so we can manipulate retain count manually. + const auto cfPtr = (CFTypeRef *)(void *)objectPtr; + if (!cfPtr || !*cfPtr) { + return; + } + + _lock.lock(); + const auto isFirstEntry = _queue.empty(); + // Push the pointer into our queue and clear their pointer. + // This "steals" the +1 from ARC and nils their pointer so they can't + // access or release the object. + _queue.push_back(*cfPtr); + *cfPtr = NULL; + _lock.unlock(); + + if (isFirstEntry) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.100 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [self drain]; + }); + } +} + +- (void)drain +{ + _lock.lock(); + const auto q = std::move(_queue); + _lock.unlock(); + for (CFTypeRef ref : q) { + // NOTE: Could check that retain count is 1 and retry later if not. + CFRelease(ref); + } +} + +@end + +@implementation ASAbstractRunLoopQueue + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { + return nil; + } + ASDisplayNodeAssert(self.class != [ASAbstractRunLoopQueue class], @"Should never create instances of abstract class ASAbstractRunLoopQueue."); + return self; +} + +@end + +#pragma mark - ASRunLoopQueue + +@interface ASRunLoopQueue () { + CFRunLoopRef _runLoop; + CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _runLoopObserver; + NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance. + AS::RecursiveMutex _internalQueueLock; + + // In order to not pollute the top-level activities, each queue has 1 root activity. + os_activity_t _rootActivity; + +#if ASRunLoopQueueLoggingEnabled + NSTimer *_runloopQueueLoggingTimer; +#endif +} + +@property (nonatomic) void (^queueConsumer)(id dequeuedItem, BOOL isQueueDrained); + +@end + +@implementation ASRunLoopQueue + +- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects handler:(void (^)(id _Nullable, BOOL))handlerBlock +{ + if (self = [super init]) { + _runLoop = runloop; + NSPointerFunctionsOptions options = retainsObjects ? NSPointerFunctionsStrongMemory : NSPointerFunctionsWeakMemory; + _internalQueue = [[NSPointerArray alloc] initWithOptions:options]; + _queueConsumer = handlerBlock; + _batchSize = 1; + _ensureExclusiveMembership = YES; + + // We don't want to pollute the top-level app activities with run loop batches, so we create one top-level + // activity per queue, and each batch activity joins that one instead. + _rootActivity = as_activity_create("Process run loop queue items", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DEFAULT); + { + // Log a message identifying this queue into the queue's root activity. + as_activity_scope_verbose(_rootActivity); + as_log_verbose(ASDisplayLog(), "Created run loop queue: %@", self); + } + + // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, + // __unsafe_unretained allows us to avoid flagging the memory cycle detector. + __unsafe_unretained __typeof__(self) weakSelf = self; + void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + [weakSelf processQueue]; + }; + _runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock); + CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes); + + // It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of + // the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done + CFRunLoopSourceContext sourceContext = {}; + sourceContext.perform = runLoopSourceCallback; +#if ASRunLoopQueueLoggingEnabled + sourceContext.info = (__bridge void *)self; +#endif + _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); + CFRunLoopAddSource(runloop, _runLoopSource, kCFRunLoopCommonModes); + +#if ASRunLoopQueueLoggingEnabled + _runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes]; +#endif + } + return self; +} + +- (void)dealloc +{ + if (CFRunLoopContainsSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes)) { + CFRunLoopRemoveSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes); + } + CFRelease(_runLoopSource); + _runLoopSource = nil; + + if (CFRunLoopObserverIsValid(_runLoopObserver)) { + CFRunLoopObserverInvalidate(_runLoopObserver); + } + CFRelease(_runLoopObserver); + _runLoopObserver = nil; +} + +#if ASRunLoopQueueLoggingEnabled +- (void)checkRunLoop +{ + NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.count); +} +#endif + +- (void)processQueue +{ + BOOL hasExecutionBlock = (_queueConsumer != nil); + + // If we have an execution block, this vector will be populated, otherwise remains empty. + // This is to avoid needlessly retaining/releasing the objects if we don't have a block. + std::vector itemsToProcess; + + BOOL isQueueDrained = NO; + { + MutexLocker l(_internalQueueLock); + + NSInteger internalQueueCount = _internalQueue.count; + // Early-exit if the queue is empty. + if (internalQueueCount == 0) { + return; + } + + ASSignpostStart(ASSignpostRunLoopQueueBatch); + + // Snatch the next batch of items. + NSInteger maxCountToProcess = MIN(internalQueueCount, self.batchSize); + + /** + * For each item in the next batch, if it's non-nil then NULL it out + * and if we have an execution block then add it in. + * This could be written a bunch of different ways but + * this particular one nicely balances readability, safety, and efficiency. + */ + NSInteger foundItemCount = 0; + for (NSInteger i = 0; i < internalQueueCount && foundItemCount < maxCountToProcess; i++) { + /** + * It is safe to use unsafe_unretained here. If the queue is weak, the + * object will be added to the autorelease pool. If the queue is strong, + * it will retain the object until we transfer it (retain it) in itemsToProcess. + */ + __unsafe_unretained id ptr = (__bridge id)[_internalQueue pointerAtIndex:i]; + if (ptr != nil) { + foundItemCount++; + if (hasExecutionBlock) { + itemsToProcess.push_back(ptr); + } + [_internalQueue replacePointerAtIndex:i withPointer:NULL]; + } + } + + if (foundItemCount == 0) { + // If _internalQueue holds weak references, and all of them just become NULL, then the array + // is never marked as needsCompletion, and compact will return early, not removing the NULL's. + // Inserting a NULL here ensures the compaction will take place. + // See http://www.openradar.me/15396578 and https://stackoverflow.com/a/40274426/1136669 + [_internalQueue addPointer:NULL]; + } + + [_internalQueue compact]; + if (_internalQueue.count == 0) { + isQueueDrained = YES; + } + } + + // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. + const auto count = itemsToProcess.size(); + if (count > 0) { + as_activity_scope_verbose(as_activity_create("Process run loop queue batch", _rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + const auto itemsEnd = itemsToProcess.cend(); + for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { + __unsafe_unretained id value = *iterator; + _queueConsumer(value, isQueueDrained && iterator == itemsEnd - 1); + as_log_verbose(ASDisplayLog(), "processed %@", value); + } + if (count > 1) { + as_log_verbose(ASDisplayLog(), "processed %lu items", (unsigned long)count); + } + } + + // If the queue is not fully drained yet force another run loop to process next batch of items + if (!isQueueDrained) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); + } + + ASSignpostEnd(ASSignpostRunLoopQueueBatch); +} + +- (void)enqueue:(id)object +{ + if (!object) { + return; + } + + MutexLocker l(_internalQueueLock); + + // Check if the object exists. + BOOL foundObject = NO; + + if (_ensureExclusiveMembership) { + for (id currentObject in _internalQueue) { + if (currentObject == object) { + foundObject = YES; + break; + } + } + } + + if (!foundObject) { + [_internalQueue addPointer:(__bridge void *)object]; + if (_internalQueue.count == 1) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); + } + } +} + +- (BOOL)isEmpty +{ + MutexLocker l(_internalQueueLock); + return _internalQueue.count == 0; +} + +ASSynthesizeLockingMethodsWithMutex(_internalQueueLock) + +@end + +#pragma mark - ASCATransactionQueue + +@interface ASCATransactionQueue () { + CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _preTransactionObserver; + + // Current buffer for new entries, only accessed from within its mutex. + std::vector> _internalQueue; + + // No retain, no release, pointer hash, pointer equality. + // Enforce uniqueness in our queue. std::unordered_set does a heap allocation for each entry – not good. + CFMutableSetRef _internalQueueHashSet; + + // Temporary buffer, only accessed from the main thread in -process. + std::vector> _batchBuffer; + + AS::Mutex _internalQueueLock; + + // In order to not pollute the top-level activities, each queue has 1 root activity. + os_activity_t _rootActivity; + +#if ASRunLoopQueueLoggingEnabled + NSTimer *_runloopQueueLoggingTimer; +#endif +} + +@end + +@implementation ASCATransactionQueue + +// CoreAnimation commit order is 2000000, the goal of this is to process shortly beforehand +// but after most other scheduled work on the runloop has processed. +static int const kASASCATransactionQueueOrder = 1000000; + +ASCATransactionQueue *_ASSharedCATransactionQueue; +dispatch_once_t _ASSharedCATransactionQueueOnceToken; + +- (instancetype)init +{ + if (self = [super init]) { + _internalQueueHashSet = CFSetCreateMutable(NULL, 0, NULL); + + // This is going to be a very busy queue – every node in the preload range will enter this queue. + // Save some time on first render by reserving space up front. + static constexpr int kInternalQueueInitialCapacity = 64; + _internalQueue.reserve(kInternalQueueInitialCapacity); + _batchBuffer.reserve(kInternalQueueInitialCapacity); + + // We don't want to pollute the top-level app activities with run loop batches, so we create one top-level + // activity per queue, and each batch activity joins that one instead. + _rootActivity = as_activity_create("Process run loop queue items", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DEFAULT); + { + // Log a message identifying this queue into the queue's root activity. + as_activity_scope_verbose(_rootActivity); + as_log_verbose(ASDisplayLog(), "Created run loop queue: %@", self); + } + + // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, + // __unsafe_unretained allows us to avoid flagging the memory cycle detector. + __unsafe_unretained __typeof__(self) weakSelf = self; + _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + while (!weakSelf->_internalQueue.empty()) { + [weakSelf processQueue]; + } + }); + + CFRunLoopAddObserver(CFRunLoopGetMain(), _preTransactionObserver, kCFRunLoopCommonModes); + + // It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of + // the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done + CFRunLoopSourceContext sourceContext = {}; + sourceContext.perform = runLoopSourceCallback; +#if ASRunLoopQueueLoggingEnabled + sourceContext.info = (__bridge void *)self; +#endif + _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); + CFRunLoopAddSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); + +#if ASRunLoopQueueLoggingEnabled + _runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes]; +#endif + } + return self; +} + +- (void)dealloc +{ + ASDisplayNodeAssertMainThread(); + + CFRelease(_internalQueueHashSet); + CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); + CFRelease(_runLoopSource); + _runLoopSource = nil; + + if (CFRunLoopObserverIsValid(_preTransactionObserver)) { + CFRunLoopObserverInvalidate(_preTransactionObserver); + } + CFRelease(_preTransactionObserver); + _preTransactionObserver = nil; +} + +#if ASRunLoopQueueLoggingEnabled +- (void)checkRunLoop +{ + NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.count); +} +#endif + +- (void)processQueue +{ + ASDisplayNodeAssertMainThread(); + + AS::UniqueLock l(_internalQueueLock); + NSInteger count = _internalQueue.size(); + // Early-exit if the queue is empty. + if (count == 0) { + return; + } + as_activity_scope_verbose(as_activity_create("Process run loop queue batch", _rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + ASSignpostStart(ASSignpostRunLoopQueueBatch); + + // Swap buffers, clear our hash table. + _internalQueue.swap(_batchBuffer); + CFSetRemoveAllValues(_internalQueueHashSet); + + // Unlock early. We are done with internal queue, and batch buffer is main-thread-only so no lock. + l.unlock(); + + for (const id &value : _batchBuffer) { + [value prepareForCATransactionCommit]; + as_log_verbose(ASDisplayLog(), "processed %@", value); + } + _batchBuffer.clear(); + as_log_verbose(ASDisplayLog(), "processed %lu items", (unsigned long)count); + ASSignpostEnd(ASSignpostRunLoopQueueBatch); +} + +- (void)enqueue:(id)object +{ + if (!object) { + return; + } + + if (!self.enabled) { + [object prepareForCATransactionCommit]; + return; + } + + MutexLocker l(_internalQueueLock); + if (CFSetContainsValue(_internalQueueHashSet, (__bridge void *)object)) { + return; + } + CFSetAddValue(_internalQueueHashSet, (__bridge void *)object); + _internalQueue.emplace_back(object); + if (_internalQueue.size() == 1) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(CFRunLoopGetMain()); + } +} + +- (BOOL)isEmpty +{ + MutexLocker l(_internalQueueLock); + return _internalQueue.empty(); +} + +- (BOOL)isEnabled +{ + return ASActivateExperimentalFeature(ASExperimentalInterfaceStateCoalescing); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASScrollNode.h b/submodules/AsyncDisplayKit/Source/ASScrollNode.h new file mode 100644 index 0000000000..1137e89ede --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASScrollNode.h @@ -0,0 +1,50 @@ +// +// ASScrollNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UIScrollView; + +/** + * Simple node that wraps UIScrollView. + */ +@interface ASScrollNode : ASDisplayNode + +/** + * @abstract The node's UIScrollView. + */ +@property (readonly) UIScrollView *view; + +/** + * @abstract When enabled, the size calculated by the node's layout spec is used as + * the .contentSize of the scroll view, instead of the bounds size. The bounds is instead + * allowed to match the parent's size (whenever it is finite - otherwise, the bounds size + * also grows to the full contentSize). It also works with .layoutSpecBlock(). + * NOTE: Most users of ASScrollNode will want to use this, and may be enabled by default later. + */ +@property BOOL automaticallyManagesContentSize; + +/** + * @abstract This property controls how the constrainedSize is interpreted when sizing the content. + * if you are using automaticallyManagesContentSize, it can be crucial to ensure that the sizing is + * done how you expect. + * Vertical: The constrainedSize is interpreted as having unbounded .height (CGFLOAT_MAX), allowing + * stacks and other content in the layout spec to expand and result in scrollable content. + * Horizontal: The constrainedSize is interpreted as having unbounded .width (CGFLOAT_MAX), ... + * Vertical & Horizontal: the constrainedSize is interpreted as unbounded in both directions. + * @default ASScrollDirectionVerticalDirections + */ +@property ASScrollDirection scrollableDirections; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASScrollNode.mm b/submodules/AsyncDisplayKit/Source/ASScrollNode.mm new file mode 100644 index 0000000000..c6e60e619c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASScrollNode.mm @@ -0,0 +1,172 @@ +// +// ASScrollNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +@interface ASScrollView : UIScrollView +@end + +@implementation ASScrollView + +// This special +layerClass allows ASScrollNode to get -layout calls from -layoutSublayers. ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +- (ASScrollNode *)scrollNode +{ + return (ASScrollNode *)ASViewToDisplayNode(self); +} + +#pragma mark - _ASDisplayView behavior substitutions +// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element. +// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView. +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar. + BOOL visible = (newWindow != nil); + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar. + BOOL visible = (self.window != nil); + if (!visible && node.inHierarchy) { + [node __exitHierarchy]; + } +} + +- (NSArray *)accessibilityElements +{ + return [self.asyncdisplaykit_node accessibilityElements]; +} + +@end + +@implementation ASScrollNode +{ + ASScrollDirection _scrollableDirections; + BOOL _automaticallyManagesContentSize; + CGSize _contentCalculatedSizeFromLayout; +} +@dynamic view; + +- (instancetype)init +{ + if (self = [super init]) { + [self setViewBlock:^UIView *{ return [[ASScrollView alloc] init]; }]; + } + return self; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASScopedLockSelfOrToRoot(); + + ASSizeRange contentConstrainedSize = constrainedSize; + if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) { + contentConstrainedSize.max.height = CGFLOAT_MAX; + } + if (ASScrollDirectionContainsHorizontalDirection(_scrollableDirections)) { + contentConstrainedSize.max.width = CGFLOAT_MAX; + } + + ASLayout *layout = [super calculateLayoutThatFits:contentConstrainedSize + restrictedToSize:size + relativeToParentSize:parentSize]; + + if (_automaticallyManagesContentSize) { + // To understand this code, imagine we're containing a horizontal stack set within a vertical table node. + // Our parentSize is fixed ~375pt width, but 0 - INF height. Our stack measures 1000pt width, 50pt height. + // In this case, we want our scrollNode.bounds to be 375pt wide, and 50pt high. ContentSize 1000pt, 50pt. + // We can achieve this behavior by: + // 1. Always set contentSize to layout.size. + // 2. Set bounds to a size that is calculated by clamping parentSize against constrained size, + // unless one dimension is not defined, in which case adopt the contentSize for that dimension. + _contentCalculatedSizeFromLayout = layout.size; + CGSize selfSize = ASSizeRangeClamp(constrainedSize, parentSize); + if (ASPointsValidForLayout(selfSize.width) == NO) { + selfSize.width = _contentCalculatedSizeFromLayout.width; + } + if (ASPointsValidForLayout(selfSize.height) == NO) { + selfSize.height = _contentCalculatedSizeFromLayout.height; + } + // Don't provide a position, as that should be set by the parent. + layout = [ASLayout layoutWithLayoutElement:self + size:selfSize + sublayouts:layout.sublayouts]; + } + return layout; +} + +- (void)layout +{ + [super layout]; + + ASLockScopeSelf(); // Lock for using our two instance variables. + + if (_automaticallyManagesContentSize) { + CGSize contentSize = _contentCalculatedSizeFromLayout; + if (ASIsCGSizeValidForLayout(contentSize) == NO) { + NSLog(@"%@ calculated a size in its layout spec that can't be applied to .contentSize: %@. Applying parentSize (scrollNode's bounds) instead: %@.", self, NSStringFromCGSize(contentSize), NSStringFromCGSize(self.calculatedSize)); + contentSize = self.calculatedSize; + } + self.view.contentSize = contentSize; + } +} + +- (BOOL)automaticallyManagesContentSize +{ + ASLockScopeSelf(); + return _automaticallyManagesContentSize; +} + +- (void)setAutomaticallyManagesContentSize:(BOOL)automaticallyManagesContentSize +{ + ASLockScopeSelf(); + _automaticallyManagesContentSize = automaticallyManagesContentSize; + if (_automaticallyManagesContentSize == YES + && ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO + && ASScrollDirectionContainsHorizontalDirection(_scrollableDirections) == NO) { + // Set the @default value, for more user-friendly behavior of the most + // common use cases of .automaticallyManagesContentSize. + _scrollableDirections = ASScrollDirectionVerticalDirections; + } +} + +- (ASScrollDirection)scrollableDirections +{ + ASLockScopeSelf(); + return _scrollableDirections; +} + +- (void)setScrollableDirections:(ASScrollDirection)scrollableDirections +{ + ASLockScopeSelf(); + if (_scrollableDirections != scrollableDirections) { + _scrollableDirections = scrollableDirections; + [self setNeedsLayout]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASSectionController.h b/submodules/AsyncDisplayKit/Source/ASSectionController.h new file mode 100644 index 0000000000..7df4dc84b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASSectionController.h @@ -0,0 +1,88 @@ +// +// ASSectionController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASBatchContext; + +/** + * A protocol that your section controllers should conform to, in order to be used with Texture. + * + * @note Your supplementary view source should conform to @c ASSupplementaryNodeSource. + */ +@protocol ASSectionController + +@optional + +/** + * A method to provide the node block for the item at the given index. + * The node block you return will be run asynchronously off the main thread, + * so it's important to retrieve any objects from your section _outside_ the block + * because by the time the block is run, the array may have changed. + * + * @param index The index of the item. + * @return A block to be run concurrently to build the node for this item. + * @see collectionNode:nodeBlockForItemAtIndexPath: + */ +- (ASCellNodeBlock)nodeBlockForItemAtIndex:(NSInteger)index; + +/** + * Similar to -collectionView:cellForItemAtIndexPath:. + * + * Note: only called if nodeBlockForItemAtIndex: returns nil. + * + * @param index The index of the item. + * + * @return A node to display for the given item. This will be called on the main thread and should + * not implement reuse (it will be called once per item). Unlike UICollectionView's version, + * this method is not called when the item is about to display. + */ +- (ASCellNode *)nodeForItemAtIndex:(NSInteger)index; + +/** + * Asks the section controller whether it should batch fetch because the user is + * near the end of the current data set. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the assumed return value is @c YES. + */ +- (BOOL)shouldBatchFetch; + +/** + * Asks the section controller to begin fetching more content (tail loading) because + * the user is near the end of the current data set. + * + * @param context A context object that must be notified when the batch fetch is completed. + * + * @discussion You must eventually call -completeBatchFetching: with an argument of YES in order to receive future + * notifications to do batch fetches. This method is called on a background queue. + */ +- (void)beginBatchFetchWithContext:(ASBatchContext *)context; + +/** + * A method to provide the size range used for measuring the item + * at the given index. + * + * @param index The index of the item. + * @return A size range used for asynchronously measuring the node at this index. + * @see collectionNode:constrainedSizeForItemAtIndexPath: + */ +- (ASSizeRange)sizeRangeForItemAtIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASSupplementaryNodeSource.h b/submodules/AsyncDisplayKit/Source/ASSupplementaryNodeSource.h new file mode 100644 index 0000000000..bc361dc736 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASSupplementaryNodeSource.h @@ -0,0 +1,55 @@ +// +// ASSupplementaryNodeSource.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASSupplementaryNodeSource + +@optional + +/** + * A method to provide the node-block for the supplementary element. + * + * @param elementKind The kind of supplementary element. + * @param index The index of the item. + * @return A node block for the supplementary element. + * @see collectionNode:nodeForSupplementaryElementOfKind:atIndexPath: + */ +- (ASCellNodeBlock)nodeBlockForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index; + +/** + * Asks the controller to provide a node to display for the given supplementary element. + * + * @param kind The kind of supplementary element. + * @param index The index of the item. + */ +- (ASCellNode *)nodeForSupplementaryElementOfKind:(NSString *)kind atIndex:(NSInteger)index; + +/** + * A method to provide the size range used for measuring the supplementary + * element of the given kind at the given index. + * + * @param elementKind The kind of supplementary element. + * @param index The index of the item. + * @return A size range used for asynchronously measuring the node. + * @see collectionNode:constrainedSizeForSupplementaryElementOfKind:atIndexPath: + */ +- (ASSizeRange)sizeRangeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTabBarController.h b/submodules/AsyncDisplayKit/Source/ASTabBarController.h new file mode 100644 index 0000000000..7479f7ac85 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTabBarController.h @@ -0,0 +1,32 @@ +// +// ASTabBarController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * ASTabBarController + * + * @discussion ASTabBarController is a drop in replacement for UITabBarController + * which implements the memory efficiency improving @c ASManagesChildVisibilityDepth protocol. + * + * @see ASManagesChildVisibilityDepth + */ +@interface ASTabBarController : UITabBarController + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTabBarController.mm b/submodules/AsyncDisplayKit/Source/ASTabBarController.mm new file mode 100644 index 0000000000..c0f9e39dd3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTabBarController.mm @@ -0,0 +1,87 @@ +// +// ASTabBarController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +@implementation ASTabBarController +{ + BOOL _parentManagesVisibilityDepth; + NSInteger _visibilityDepth; +} + +ASVisibilityDidMoveToParentViewController; + +ASVisibilityViewWillAppear; + +ASVisibilityViewDidDisappearImplementation; + +ASVisibilitySetVisibilityDepth; + +ASVisibilityDepthImplementation; + +- (void)visibilityDepthDidChange +{ + for (UIViewController *viewController in self.viewControllers) { + if ([viewController conformsToProtocol:@protocol(ASVisibilityDepth)]) { + [(id )viewController visibilityDepthDidChange]; + } + } +} + +- (NSInteger)visibilityDepthOfChildViewController:(UIViewController *)childViewController +{ + NSUInteger viewControllerIndex = [self.viewControllers indexOfObjectIdenticalTo:childViewController]; + if (viewControllerIndex == NSNotFound) { + //If childViewController is not actually a child, return NSNotFound which is also a really large number. + return NSNotFound; + } + + if (self.selectedViewController == childViewController) { + return [self visibilityDepth]; + } + return [self visibilityDepth] + 1; +} + +#pragma mark - UIKit overrides + +- (void)setViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers +{ + [super setViewControllers:viewControllers]; + [self visibilityDepthDidChange]; +} + +- (void)setViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers animated:(BOOL)animated +{ + [super setViewControllers:viewControllers animated:animated]; + [self visibilityDepthDidChange]; +} + +- (void)setSelectedIndex:(NSUInteger)selectedIndex +{ + as_activity_create_for_scope("Set selected index of ASTabBarController"); + as_log_info(ASNodeLog(), "Selected tab %tu of %@", selectedIndex, self); + + [super setSelectedIndex:selectedIndex]; + [self visibilityDepthDidChange]; +} + +- (void)setSelectedViewController:(__kindof UIViewController *)selectedViewController +{ + as_activity_create_for_scope("Set selected view controller of ASTabBarController"); + as_log_info(ASNodeLog(), "Selected view controller %@ of %@", selectedViewController, self); + + [super setSelectedViewController:selectedViewController]; + [self visibilityDepthDidChange]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTableNode+Beta.h b/submodules/AsyncDisplayKit/Source/ASTableNode+Beta.h new file mode 100644 index 0000000000..6d580bdc43 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableNode+Beta.h @@ -0,0 +1,21 @@ +// +// ASTableNode+Beta.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@protocol ASBatchFetchingDelegate; + +NS_ASSUME_NONNULL_BEGIN + +@interface ASTableNode (Beta) + +@property (nonatomic, weak) id batchFetchingDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASTableNode.h b/submodules/AsyncDisplayKit/Source/ASTableNode.h new file mode 100644 index 0000000000..f05c9f1dc8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableNode.h @@ -0,0 +1,759 @@ +// +// ASTableNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASTableDataSource; +@protocol ASTableDelegate; +@class ASTableView, ASBatchContext; + +/** + * ASTableNode is a node based class that wraps an ASTableView. It can be used + * as a subnode of another node, and provide room for many (great) features and improvements later on. + */ +@interface ASTableNode : ASDisplayNode + +- (instancetype)init; // UITableViewStylePlain +- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER; + +@property (readonly) ASTableView *view; + +// These properties can be set without triggering the view to be created, so it's fine to set them in -init. +@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) id dataSource; + +/** + * The number of screens left to scroll before the delegate -tableNode:beginBatchFetchingWithContext: is called. + * + * Defaults to two screenfuls. + */ +@property (nonatomic) CGFloat leadingScreensForBatching; + +/* + * A Boolean value that determines whether the table will be flipped. + * If the value of this property is YES, the first cell node will be at the bottom of the table (as opposed to the top by default). This is useful for chat/messaging apps. The default value is NO. + */ +@property (nonatomic) BOOL inverted; + +/** + * The distance that the content view is inset from the table node edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets contentInset; + +/** + * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. + */ +@property (nonatomic) CGPoint contentOffset; + +/** + * Sets the offset from the content node’s origin to the table node’s origin. + * + * @param contentOffset The offset + * + * @param animated YES to animate to this new offset at a constant velocity, NO to not aniamte and immediately make the transition. + */ +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + +/** + * YES to automatically adjust the contentOffset when cells are inserted or deleted above + * visible cells, maintaining the users' visible scroll position. + * + * @note This is only applied to non-animated updates. For animated updates, there is no way to + * synchronize or "cancel out" the appearance of a scroll due to UITableView API limitations. + * + * default is NO. + */ +@property (nonatomic) BOOL automaticallyAdjustsContentOffset; + +/* + * A Boolean value that determines whether users can select a row. + * If the value of this property is YES (the default), users can select rows. If you set it to NO, they cannot select rows. Setting this property affects cell selection only when the table view is not in editing mode. If you want to restrict selection of cells in editing mode, use `allowsSelectionDuringEditing`. + */ +@property (nonatomic) BOOL allowsSelection; +/* + * A Boolean value that determines whether users can select cells while the table view is in editing mode. + * If the value of this property is YES, users can select rows during editing. The default value is NO. If you want to restrict selection of cells regardless of mode, use allowsSelection. + */ +@property (nonatomic) BOOL allowsSelectionDuringEditing; +/* + * A Boolean value that determines whether users can select more than one row outside of editing mode. + * This property controls whether multiple rows can be selected simultaneously outside of editing mode. When the value of this property is YES, each row that is tapped acquires a selected appearance. Tapping the row again removes the selected appearance. If you access indexPathsForSelectedRows, you can get the index paths that identify the selected rows. + */ +@property (nonatomic) BOOL allowsMultipleSelection; +/* + * A Boolean value that controls whether users can select more than one cell simultaneously in editing mode. + * The default value of this property is NO. If you set it to YES, check marks appear next to selected rows in editing mode. In addition, UITableView does not query for editing styles when it goes into editing mode. If you access indexPathsForSelectedRows, you can get the index paths that identify the selected rows. + */ +@property (nonatomic) BOOL allowsMultipleSelectionDuringEditing; + +/** + * Tuning parameters for a range type in full mode. + * + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in full mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in full mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType; + +/** + * Tuning parameters for a range type in the specified mode. + * + * @param rangeMode The range mode to get the running parameters for. + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in the given mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in the specified mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeMode The range mode to set the running parameters for. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +/** + * Scrolls the table to the given row. + * + * @param indexPath The index path of the row. + * @param scrollPosition Where the row should end up after the scroll. + * @param animated Whether the scroll should be animated or not. + * + * This method must be called on the main thread. + */ +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated; + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @param completion block to run on completion of asynchronous loading or nil. If supplied, the block is run on + * the main thread. + * @warning This method is substantially more expensive than UITableView's version. + */ +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion; + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UITableView's version. + */ +- (void)reloadData; + +/** + * Triggers a relayout of all nodes. + * + * @discussion This method invalidates and lays out every cell node in the table view. + */ +- (void)relayoutItems; + +/** + * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. + * The data source must be updated to reflect the changes before the update block completes. + * + * @param animated NO to disable animations for this batch + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; + +/** + * Perform a batch of updates asynchronously with animations in the batch. This method must be called from the main thread. + * The data source must be updated to reflect the changes before the update block completes. + * + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; + +/** + * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. + * This is typically the concurrent allocation (calling nodeBlocks) and layout of newly inserted + * ASCellNodes. If YES is returned, then calling -waitUntilAllUpdatesAreProcessed may take tens of + * milliseconds to return as it blocks on these concurrent operations. + * + * Returns NO if ASCollectionNode is fully synchronized with the underlying UICollectionView. This + * means that until the next performBatchUpdates: is called, it is safe to compare UIKit values + * (such as from UICollectionViewLayout) with your app's data source. + * + * This method will always return NO if called immediately after -waitUntilAllUpdatesAreProcessed. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; + +/** + * Schedules a block to be performed (on the main thread) after processing of performBatchUpdates: + * is finished (completely synchronized to UIKit). The blocks will be run at the moment that + * -isProcessingUpdates changes from YES to NO; + * + * When isProcessingUpdates == NO, the block is run block immediately (before the method returns). + * + * Blocks scheduled by this mechanism are NOT guaranteed to run in the order they are scheduled. + * They may also be delayed if performBatchUpdates continues to be called; the blocks will wait until + * all running updates are finished. + * + * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. + */ +- (void)onDidFinishProcessingUpdates:(void (^)(void))didFinishProcessingUpdates; + +/** + * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. + */ +- (void)waitUntilAllUpdatesAreProcessed; + +/** + * Inserts one or more sections, with an option to animate the insertion. + * + * @param sections An index set that specifies the sections to insert. + * + * @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Deletes one or more sections, with an option to animate the deletion. + * + * @param sections An index set that specifies the sections to delete. + * + * @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Reloads the specified sections using a given animation effect. + * + * @param sections An index set that specifies the sections to reload. + * + * @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; + +/** + * Inserts rows at the locations identified by an array of index paths, with an option to animate the insertion. + * + * @param indexPaths An array of NSIndexPath objects, each representing a row index and section index that together identify a row. + * + * @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Deletes the rows specified by an array of index paths, with an option to animate the deletion. + * + * @param indexPaths An array of NSIndexPath objects identifying the rows to delete. + * + * @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Reloads the specified rows using a given animation effect. + * + * @param indexPaths An array of NSIndexPath objects identifying the rows to reload. + * + * @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Moves the row at a specified location to a destination location. + * + * @param indexPath The index path identifying the row to move. + * + * @param newIndexPath The index path that is the destination of the move for the row. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; + +#pragma mark - Selection + +/** + * Selects a row in the table view identified by index path, optionally scrolling the row to a location in the table view. + * This method does not cause any selection-related delegate methods to be called. + * + * @param indexPath An index path identifying a row in the table view. + * + * @param animated Specify YES to animate the change in the selection or NO to make the change without animating it. + * + * @param scrollPosition A constant that identifies a relative position in the table view (top, middle, bottom) for the row when scrolling concludes. See `UITableViewScrollPosition` for descriptions of valid constants. + * + * @discussion This method must be called from the main thread. + */ +- (void)selectRowAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition; + +/* + * Deselects a given row identified by index path, with an option to animate the deselection. + * This method does not cause any selection-related delegate methods to be called. + * Calling this method does not cause any scrolling to the deselected row. + * + * @param indexPath An index path identifying a row in the table view. + * + * @param animated Specify YES to animate the change in the selection or NO to make the change without animating it. + * + * @discussion This method must be called from the main thread. + */ +- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated; + +#pragma mark - Querying Data + +/** + * Retrieves the number of rows in the given section. + * + * @param section The section. + * + * @return The number of rows. + */ +- (NSInteger)numberOfRowsInSection:(NSInteger)section AS_WARN_UNUSED_RESULT; + +/** + * The number of sections in the table node. + */ +@property (nonatomic, readonly) NSInteger numberOfSections; + +/** + * Similar to -visibleCells. + * + * @return an array containing the nodes being displayed on screen. This must be called on the main thread. + */ +@property (nonatomic, readonly) NSArray<__kindof ASCellNode *> *visibleNodes; + +/** + * Retrieves the node for the row at the given index path. + */ +- (nullable __kindof ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Similar to -indexPathForCell:. + * + * @param cellNode a node for a row. + * + * @return The index path to this row, if it exists. + * + * @discussion This method will return @c nil for a node that is still being + * displayed in the table view, if the data source has deleted the row. + * That is, the node is visible but it no longer corresponds + * to any item in the data source and will be removed soon. + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; + +/** + * Similar to -[UITableView rectForRowAtIndexPath:] + * + * @param indexPath An index path identifying a row in the table view. + * + * @return A rectangle defining the area in which the table view draws the row or CGRectZero if indexPath is invalid. + * + * @discussion This method must be called from the main thread. + */ +- (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Similar to -[UITableView cellForRowAtIndexPath:] + * + * @param indexPath An index path identifying a row in the table view. + * + * @return An object representing a cell of the table, or nil if the cell is not visible or indexPath is out of range. + * + * @discussion This method must be called from the main thread. + */ +- (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +/** + * Similar to UITableView.indexPathForSelectedRow + * + * @return The value of this property is an index path identifying the row and section + * indexes of the selected row, or nil if the index path is invalid. If there are multiple selections, + * this property contains the first index-path object in the array of row selections; + * this object has the lowest index values for section and row. + * + * @discussion This method must be called from the main thread. + */ +@property (nullable, nonatomic, copy, readonly) NSIndexPath *indexPathForSelectedRow; + +@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedRows; + +/** + * Similar to -[UITableView indexPathForRowAtPoint:] + * + * @param point A point in the local coordinate system of the table view (the table view’€™s bounds). + * + * @return An index path representing the row and section associated with point, + * or nil if the point is out of the bounds of any row. + * + * @discussion This method must be called from the main thread. + */ +- (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point AS_WARN_UNUSED_RESULT; + +/** + * Similar to -[UITableView indexPathsForRowsInRect:] + * + * @param rect A rectangle defining an area of the table view in local coordinates. + * + * @return An array of NSIndexPath objects each representing a row and section index identifying a row within rect. + * Returns an empty array if there aren’t any rows to return. + * + * @discussion This method must be called from the main thread. + */ +- (nullable NSArray *)indexPathsForRowsInRect:(CGRect)rect AS_WARN_UNUSED_RESULT; + +/** + * Similar to -[UITableView indexPathsForVisibleRows] + * + * @return The value of this property is an array of NSIndexPath objects each representing a row index and section index + * that together identify a visible row in the table view. If no rows are visible, the value is nil. + * + * @discussion This method must be called from the main thread. + */ +- (NSArray *)indexPathsForVisibleRows AS_WARN_UNUSED_RESULT; + +@end + +/** + * This is a node-based UITableViewDataSource. + */ +@protocol ASTableDataSource + +@optional + +/** + * Asks the data source for the number of sections in the table node. + * + * @see @c numberOfSectionsInTableView: + */ +- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode; + +/** + * Asks the data source for the number of rows in the given section of the table node. + * + * @see @c numberOfSectionsInTableView: + */ +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section; + +/** + * Asks the data source for a block to create a node to represent the row at the given index path. + * The block will be run by the table node concurrently in the background before the row is inserted + * into the table view. + * + * @param tableNode The sender. + * @param indexPath The index path of the row. + * + * @return a block that creates the node for display at this indexpath. + * Must be thread-safe (can be called on the main thread or a background + * queue) and should not implement reuse (it will be called once per row). + * + * @note This method takes precedence over tableNode:nodeForRowAtIndexPath: if implemented. + */ +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the data source for a node to represent the row at the given index path. + * + * @param tableNode The sender. + * @param indexPath The index path of the row. + * + * @return a node to display for this row. This will be called on the main thread and should not implement reuse (it will be called once per row). Unlike UITableView's version, this method + * is not called when the row is about to display. + */ +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Similar to -tableView:cellForRowAtIndexPath:. + * + * @param tableView The sender. + * + * @param indexPath The index path of the requested node. + * + * @return a node for display at this indexpath. This will be called on the main thread and should not implement reuse (it will be called once per row). Unlike UITableView's version, this method + * is not called when the row is about to display. + */ +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Similar to -tableView:nodeForRowAtIndexPath: + * This method takes precedence over tableView:nodeForRowAtIndexPath: if implemented. + * @param tableView The sender. + * + * @param indexPath The index path of the requested node. + * + * @return a block that creates the node for display at this indexpath. + * Must be thread-safe (can be called on the main thread or a background + * queue) and should not implement reuse (it will be called once per row). + */ +- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Indicator to lock the data source for data fetching in async mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception + * due to the data access in async mode. + * + * @param tableView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. + */ +- (void)tableViewLockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED_MSG("Data source accesses are on the main thread. Method will not be called."); + +/** + * Indicator to unlock the data source for data fetching in asyn mode. + * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception + * due to the data access in async mode. + * + * @param tableView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. + */ +- (void)tableViewUnlockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED_MSG("Data source accesses are on the main thread. Method will not be called."); + +/** + * Generate a unique identifier for an element in a table. This helps state restoration persist the scroll position + * of a table view even when the data in that table changes. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param indexPath The index path of the requested node. + * + * @param tableNode The sender. + * + * @return a unique identifier for the element at the given path. Return nil if the index path does not exist in the table. + */ +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inNode:(ASTableNode *)tableNode; + +/** + * Similar to -tableView:cellForRowAtIndexPath:. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param identifier The model identifier of the element, previously generated by a call to modelIdentifierForElementAtIndexPath. + * + * @param tableNode The sender. + * + * @return the index path to the current position of the matching element in the table. Return nil if the element is not found. + */ +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inNode:(ASTableNode *)tableNode; + +@end + +/** + * This is a node-based UITableViewDelegate. + * + * Note that -tableView:heightForRowAtIndexPath: has been removed; instead, your custom ASCellNode subclasses are + * responsible for deciding their preferred onscreen height in -calculateSizeThatFits:. + */ +@protocol ASTableDelegate + +@optional + +- (void)tableNode:(ASTableNode *)tableNode willDisplayRowWithNode:(ASCellNode *)node; + +- (void)tableNode:(ASTableNode *)tableNode didEndDisplayingRowWithNode:(ASCellNode *)node; + +- (nullable NSIndexPath *)tableNode:(ASTableNode *)tableNode willSelectRowAtIndexPath:(NSIndexPath *)indexPath; + +- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath; + +- (nullable NSIndexPath *)tableNode:(ASTableNode *)tableNode willDeselectRowAtIndexPath:(NSIndexPath *)indexPath; + +- (void)tableNode:(ASTableNode *)tableNode didDeselectRowAtIndexPath:(NSIndexPath *)indexPath; + +- (BOOL)tableNode:(ASTableNode *)tableNode shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath; +- (void)tableNode:(ASTableNode *)tableNode didHighlightRowAtIndexPath:(NSIndexPath *)indexPath; +- (void)tableNode:(ASTableNode *)tableNode didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath; + +- (BOOL)tableNode:(ASTableNode *)tableNode shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath; +- (BOOL)tableNode:(ASTableNode *)tableNode canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender; +- (void)tableNode:(ASTableNode *)tableNode performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender; + +/** + * Provides the constrained size range for measuring the row at the index path. + * Note: the widths in the returned size range are ignored! + * + * @param tableNode The sender. + * + * @param indexPath The index path of the node. + * + * @return A constrained size range for layout the node at this index path. + */ +- (ASSizeRange)tableNode:(ASTableNode *)tableNode constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. + * + * @param tableNode The sender. + * @param context A context object that must be notified when the batch fetch is completed. + * + * @discussion You must eventually call -completeBatchFetching: with an argument of YES in order to receive future + * notifications to do batch fetches. This method is called on a background queue. + * + * ASTableView currently only supports batch events for tail loads. If you require a head load, consider implementing a + * UIRefreshControl. + */ +- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context; + +/** + * Tell the tableView if batch fetching should begin. + * + * @param tableNode The sender. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the tableView assumes that it should notify its asyncDelegate when batch fetching + * should occur. + */ +- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode; + +/** + * Informs the delegate that the table view will add the given node + * at the given index path to the view hierarchy. + * + * @param tableView The sender. + * @param node The node that will be displayed. + * @param indexPath The index path of the row that will be displayed. + * + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)tableView:(ASTableView *)tableView willDisplayNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Informs the delegate that the table view did remove the provided node from the view hierarchy. + * This may be caused by the node scrolling out of view, or by deleting the row + * or its containing section with @c deleteRowsAtIndexPaths:withRowAnimation: or @c deleteSections:withRowAnimation: . + * + * @param tableView The sender. + * @param node The node which was removed from the view hierarchy. + * @param indexPath The index path at which the node was located before the removal. + * + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)tableView:(ASTableView *)tableView didEndDisplayingNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. + * + * @param tableView The sender. + * @param context A context object that must be notified when the batch fetch is completed. + * + * @discussion You must eventually call -completeBatchFetching: with an argument of YES in order to receive future + * notifications to do batch fetches. This method is called on a background queue. + * + * ASTableView currently only supports batch events for tail loads. If you require a head load, consider implementing a + * UIRefreshControl. + */ +- (void)tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Tell the tableView if batch fetching should begin. + * + * @param tableView The sender. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the tableView assumes that it should notify its asyncDelegate when batch fetching + * should occur. + */ +- (BOOL)shouldBatchFetchForTableView:(ASTableView *)tableView AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Provides the constrained size range for measuring the row at the index path. + * Note: the widths in the returned size range are ignored! + * + * @param tableView The sender. + * + * @param indexPath The index path of the node. + * + * @return A constrained size range for layout the node at this index path. + */ +- (ASSizeRange)tableView:(ASTableView *)tableView constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +/** + * Informs the delegate that the table view will add the node + * at the given index path to the view hierarchy. + * + * @param tableView The sender. + * @param indexPath The index path of the row that will be displayed. + * + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c tableView:willDisplayNode:forRowAtIndexPath: instead. + */ +- (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's method instead."); + +@end + +@interface ASTableNode (Deprecated) + +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -waitUntilAllUpdatesAreProcessed."); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTableNode.mm b/submodules/AsyncDisplayKit/Source/ASTableNode.mm new file mode 100644 index 0000000000..b1ce6a6274 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableNode.mm @@ -0,0 +1,871 @@ +// +// ASTableNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - _ASTablePendingState + +@interface _ASTablePendingState : NSObject { +@public + std::vector> _tuningParameters; +} +@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) id dataSource; +@property (nonatomic) ASLayoutRangeMode rangeMode; +@property (nonatomic) BOOL allowsSelection; +@property (nonatomic) BOOL allowsSelectionDuringEditing; +@property (nonatomic) BOOL allowsMultipleSelection; +@property (nonatomic) BOOL allowsMultipleSelectionDuringEditing; +@property (nonatomic) BOOL inverted; +@property (nonatomic) CGFloat leadingScreensForBatching; +@property (nonatomic) UIEdgeInsets contentInset; +@property (nonatomic) CGPoint contentOffset; +@property (nonatomic) BOOL animatesContentOffset; +@property (nonatomic) BOOL automaticallyAdjustsContentOffset; + +@end + +@implementation _ASTablePendingState + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self) { + _rangeMode = ASLayoutRangeModeUnspecified; + _tuningParameters = [ASAbstractLayoutController defaultTuningParameters]; + _allowsSelection = YES; + _allowsSelectionDuringEditing = NO; + _allowsMultipleSelection = NO; + _allowsMultipleSelectionDuringEditing = NO; + _inverted = NO; + _leadingScreensForBatching = 2; + _contentInset = UIEdgeInsetsZero; + _contentOffset = CGPointZero; + _animatesContentOffset = NO; + _automaticallyAdjustsContentOffset = NO; + } + return self; +} + +#pragma mark Tuning Parameters + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [self tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + return [self setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + ASDisplayNodeAssert(rangeMode < _tuningParameters.size() && rangeType < _tuningParameters[rangeMode].size(), @"Requesting a range that is OOB for the configured tuning parameters"); + return _tuningParameters[rangeMode][rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + ASDisplayNodeAssert(rangeMode < _tuningParameters.size() && rangeType < _tuningParameters[rangeMode].size(), @"Setting a range that is OOB for the configured tuning parameters"); + _tuningParameters[rangeMode][rangeType] = tuningParameters; +} + +@end + +#pragma mark - ASTableView + +@interface ASTableNode () +{ + AS::RecursiveMutex _environmentStateLock; + id _batchFetchingDelegate; +} + +@property (nonatomic) _ASTablePendingState *pendingState; +@property (nonatomic, weak) ASRangeController *rangeController; +@end + +@implementation ASTableNode + +#pragma mark Lifecycle + +- (instancetype)initWithStyle:(UITableViewStyle)style +{ + if (self = [super init]) { + __weak __typeof__(self) weakSelf = self; + [self setViewBlock:^{ + // Variable will be unused if event logging is off. + __unused __typeof__(self) strongSelf = weakSelf; + return [[ASTableView alloc] _initWithFrame:CGRectZero style:style dataControllerClass:nil owningNode:strongSelf eventLog:ASDisplayNodeGetEventLog(strongSelf)]; + }]; + } + return self; +} + +- (instancetype)init +{ + return [self initWithStyle:UITableViewStylePlain]; +} + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED +- (void)dealloc +{ + if (self.nodeLoaded) { + __weak UIView *view = self.view; + ASPerformBlockOnMainThread(^{ + ASDisplayNodeCAssertNil(view.superview, @"Node's view should be removed from hierarchy."); + }); + } +} +#endif + +#pragma mark ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + ASTableView *view = self.view; + view.tableNode = self; + + _rangeController = view.rangeController; + + if (_pendingState) { + _ASTablePendingState *pendingState = _pendingState; + self.pendingState = nil; + view.asyncDelegate = pendingState.delegate; + view.asyncDataSource = pendingState.dataSource; + view.inverted = pendingState.inverted; + view.allowsSelection = pendingState.allowsSelection; + view.allowsSelectionDuringEditing = pendingState.allowsSelectionDuringEditing; + view.allowsMultipleSelection = pendingState.allowsMultipleSelection; + view.allowsMultipleSelectionDuringEditing = pendingState.allowsMultipleSelectionDuringEditing; + view.automaticallyAdjustsContentOffset = pendingState.automaticallyAdjustsContentOffset; + + UIEdgeInsets contentInset = pendingState.contentInset; + if (!UIEdgeInsetsEqualToEdgeInsets(contentInset, UIEdgeInsetsZero)) { + view.contentInset = contentInset; + } + + CGPoint contentOffset = pendingState.contentOffset; + if (!CGPointEqualToPoint(contentOffset, CGPointZero)) { + [view setContentOffset:contentOffset animated:pendingState.animatesContentOffset]; + } + + const auto tuningParametersVector = pendingState->_tuningParameters; + const auto tuningParametersVectorSize = tuningParametersVector.size(); + for (NSInteger rangeMode = 0; rangeMode < tuningParametersVectorSize; rangeMode++) { + const auto tuningparametersRangeModeVector = tuningParametersVector[rangeMode]; + const auto tuningParametersVectorRangeModeSize = tuningparametersRangeModeVector.size(); + for (NSInteger rangeType = 0; rangeType < tuningParametersVectorRangeModeSize; rangeType++) { + ASRangeTuningParameters tuningParameters = tuningparametersRangeModeVector[rangeType]; + [_rangeController setTuningParameters:tuningParameters + forRangeMode:(ASLayoutRangeMode)rangeMode + rangeType:(ASLayoutRangeType)rangeType]; + } + } + + if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { + [_rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; + } + } +} + +- (ASTableView *)view +{ + return (ASTableView *)[super view]; +} + +- (void)clearContents +{ + [super clearContents]; + [self.rangeController clearContents]; +} + +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + [ASRangeController layoutDebugOverlayIfNeeded]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + // Intentionally allocate the view here and trigger a layout pass on it, which in turn will trigger the intial data load. + // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. + [self.view layoutIfNeeded]; +} + +#if ASRangeControllerLoggingEnabled +- (void)didEnterVisibleState +{ + [super didEnterVisibleState]; + NSLog(@"%@ - visible: YES", self); +} + +- (void)didExitVisibleState +{ + [super didExitVisibleState]; + NSLog(@"%@ - visible: NO", self); +} +#endif + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + [self.rangeController clearPreloadedData]; +} + +#pragma mark Setter / Getter + +// TODO: Implement this without the view. Then revisit ASLayoutElementCollectionTableSetTraitCollection +- (ASDataController *)dataController +{ + return self.view.dataController; +} + +- (_ASTablePendingState *)pendingState +{ + if (!_pendingState && ![self isNodeLoaded]) { + _pendingState = [[_ASTablePendingState alloc] init]; + } + ASDisplayNodeAssert(![self isNodeLoaded] || !_pendingState, @"ASTableNode should not have a pendingState once it is loaded"); + return _pendingState; +} + +- (void)setInverted:(BOOL)inverted +{ + self.transform = inverted ? CATransform3DMakeScale(1, -1, 1) : CATransform3DIdentity; + if ([self pendingState]) { + _pendingState.inverted = inverted; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.inverted = inverted; + } +} + +- (BOOL)inverted +{ + if ([self pendingState]) { + return _pendingState.inverted; + } else { + return self.view.inverted; + } +} + +- (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.leadingScreensForBatching = leadingScreensForBatching; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.leadingScreensForBatching = leadingScreensForBatching; + } +} + +- (CGFloat)leadingScreensForBatching +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.leadingScreensForBatching; + } else { + return self.view.leadingScreensForBatching; + } +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.contentInset = contentInset; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.contentInset = contentInset; + } +} + +- (UIEdgeInsets)contentInset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.contentInset; + } else { + return self.view.contentInset; + } +} + +- (void)setContentOffset:(CGPoint)contentOffset +{ + [self setContentOffset:contentOffset animated:NO]; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.contentOffset = contentOffset; + pendingState.animatesContentOffset = animated; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + [self.view setContentOffset:contentOffset animated:animated]; + } +} + +- (CGPoint)contentOffset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.contentOffset; + } else { + return self.view.contentOffset; + } +} + +- (void)setAutomaticallyAdjustsContentOffset:(BOOL)automaticallyAdjustsContentOffset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.automaticallyAdjustsContentOffset = automaticallyAdjustsContentOffset; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.automaticallyAdjustsContentOffset = automaticallyAdjustsContentOffset; + } +} + +- (BOOL)automaticallyAdjustsContentOffset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.automaticallyAdjustsContentOffset; + } else { + return self.view.automaticallyAdjustsContentOffset; + } +} + +- (void)setDelegate:(id )delegate +{ + if ([self pendingState]) { + _pendingState.delegate = delegate; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASTableView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDelegate = delegate; + }); + } +} + +- (id )delegate +{ + if ([self pendingState]) { + return _pendingState.delegate; + } else { + return self.view.asyncDelegate; + } +} + +- (void)setDataSource:(id )dataSource +{ + if ([self pendingState]) { + _pendingState.dataSource = dataSource; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + + // Manually trampoline to the main thread. The view requires this be called on main + // and asserting here isn't an option – it is a common pattern for users to clear + // the delegate/dataSource in dealloc, which may be running on a background thread. + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASTableView *view = self.view; + ASPerformBlockOnMainThread(^{ + view.asyncDataSource = dataSource; + }); + } +} + +- (id )dataSource +{ + if ([self pendingState]) { + return _pendingState.dataSource; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + return self.view.asyncDataSource; + } +} + +- (void)setAllowsSelection:(BOOL)allowsSelection +{ + if ([self pendingState]) { + _pendingState.allowsSelection = allowsSelection; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.allowsSelection = allowsSelection; + } +} + +- (BOOL)allowsSelection +{ + if ([self pendingState]) { + return _pendingState.allowsSelection; + } else { + return self.view.allowsSelection; + } +} + +- (void)setAllowsSelectionDuringEditing:(BOOL)allowsSelectionDuringEditing +{ + if ([self pendingState]) { + _pendingState.allowsSelectionDuringEditing = allowsSelectionDuringEditing; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.allowsSelectionDuringEditing = allowsSelectionDuringEditing; + } +} + +- (BOOL)allowsSelectionDuringEditing +{ + if ([self pendingState]) { + return _pendingState.allowsSelectionDuringEditing; + } else { + return self.view.allowsSelectionDuringEditing; + } +} + +- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection +{ + if ([self pendingState]) { + _pendingState.allowsMultipleSelection = allowsMultipleSelection; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.allowsMultipleSelection = allowsMultipleSelection; + } +} + +- (BOOL)allowsMultipleSelection +{ + if ([self pendingState]) { + return _pendingState.allowsMultipleSelection; + } else { + return self.view.allowsMultipleSelection; + } +} + +- (void)setAllowsMultipleSelectionDuringEditing:(BOOL)allowsMultipleSelectionDuringEditing +{ + if ([self pendingState]) { + _pendingState.allowsMultipleSelectionDuringEditing = allowsMultipleSelectionDuringEditing; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.allowsMultipleSelectionDuringEditing = allowsMultipleSelectionDuringEditing; + } +} + +- (BOOL)allowsMultipleSelectionDuringEditing +{ + if ([self pendingState]) { + return _pendingState.allowsMultipleSelectionDuringEditing; + } else { + return self.view.allowsMultipleSelectionDuringEditing; + } +} + +- (void)setBatchFetchingDelegate:(id)batchFetchingDelegate +{ + _batchFetchingDelegate = batchFetchingDelegate; +} + +- (id)batchFetchingDelegate +{ + return _batchFetchingDelegate; +} + +#pragma mark ASRangeControllerUpdateRangeProtocol + +- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode +{ + if ([self pendingState]) { + _pendingState.rangeMode = rangeMode; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASTableNode should be loaded if pendingState doesn't exist"); + [self.rangeController updateCurrentRangeWithMode:rangeMode]; + } +} + +#pragma mark ASEnvironment + +ASLayoutElementCollectionTableSetTraitCollection(_environmentStateLock) + +#pragma mark - Range Tuning + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [self tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + [self setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + if ([self pendingState]) { + return [_pendingState tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + } else { + return [self.rangeController tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + } +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + if ([self pendingState]) { + [_pendingState setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; + } else { + return [self.rangeController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; + } +} + +#pragma mark - Selection + +- (void)selectRowAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + indexPath = [tableView convertIndexPathFromTableNode:indexPath waitingIfNeeded:YES]; + if (indexPath != nil) { + [tableView selectRowAtIndexPath:indexPath animated:animated scrollPosition:scrollPosition]; + } else { + NSLog(@"Failed to select row at index path %@ because the row never reached the view.", indexPath); + } + +} + +- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + indexPath = [tableView convertIndexPathFromTableNode:indexPath waitingIfNeeded:YES]; + if (indexPath != nil) { + [tableView deselectRowAtIndexPath:indexPath animated:animated]; + } else { + NSLog(@"Failed to deselect row at index path %@ because the row never reached the view.", indexPath); + } +} + +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + indexPath = [tableView convertIndexPathFromTableNode:indexPath waitingIfNeeded:YES]; + + if (indexPath != nil) { + [tableView scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; + } else { + NSLog(@"Failed to scroll to row at index path %@ because the row never reached the view.", indexPath); + } +} + +#pragma mark - Querying Data + +- (void)reloadDataInitiallyIfNeeded +{ + ASDisplayNodeAssertMainThread(); + if (!self.dataController.initialReloadDataHasBeenCalled) { + // Note: Just calling reloadData isn't enough here – we need to + // ensure that _nodesConstrainedWidth is updated first. + [self.view layoutIfNeeded]; + } +} + +- (NSInteger)numberOfRowsInSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + [self reloadDataInitiallyIfNeeded]; + return [self.dataController.pendingMap numberOfItemsInSection:section]; +} + +- (NSInteger)numberOfSections +{ + ASDisplayNodeAssertMainThread(); + [self reloadDataInitiallyIfNeeded]; + return [self.dataController.pendingMap numberOfSections]; +} + +- (NSArray<__kindof ASCellNode *> *)visibleNodes +{ + ASDisplayNodeAssertMainThread(); + return self.isNodeLoaded ? [self.view visibleNodes] : @[]; +} + +- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode +{ + return [self.dataController.pendingMap indexPathForElement:cellNode.collectionElement]; +} + +- (ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + [self reloadDataInitiallyIfNeeded]; + return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].node; +} + +- (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + indexPath = [tableView convertIndexPathFromTableNode:indexPath waitingIfNeeded:YES]; + return [tableView rectForRowAtIndexPath:indexPath]; +} + +- (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + indexPath = [tableView convertIndexPathFromTableNode:indexPath waitingIfNeeded:YES]; + if (indexPath == nil) { + return nil; + } + return [tableView cellForRowAtIndexPath:indexPath]; +} + +- (nullable NSIndexPath *)indexPathForSelectedRow +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + NSIndexPath *indexPath = tableView.indexPathForSelectedRow; + if (indexPath != nil) { + return [tableView convertIndexPathToTableNode:indexPath]; + } + return indexPath; +} + +- (NSArray *)indexPathsForSelectedRows +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + return [tableView convertIndexPathsToTableNode:tableView.indexPathsForSelectedRows]; +} + +- (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + + NSIndexPath *indexPath = [tableView indexPathForRowAtPoint:point]; + if (indexPath != nil) { + return [tableView convertIndexPathToTableNode:indexPath]; + } + return indexPath; +} + +- (nullable NSArray *)indexPathsForRowsInRect:(CGRect)rect +{ + ASDisplayNodeAssertMainThread(); + ASTableView *tableView = self.view; + return [tableView convertIndexPathsToTableNode:[tableView indexPathsForRowsInRect:rect]]; +} + +- (NSArray *)indexPathsForVisibleRows +{ + ASDisplayNodeAssertMainThread(); + NSMutableArray *indexPathsArray = [NSMutableArray new]; + for (ASCellNode *cell in [self visibleNodes]) { + NSIndexPath *indexPath = [self indexPathForNode:cell]; + if (indexPath) { + [indexPathsArray addObject:indexPath]; + } + } + return indexPathsArray; +} + +#pragma mark - Editing + +- (void)reloadDataWithCompletion:(void (^)())completion +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view reloadDataWithCompletion:completion]; + } else { + if (completion) { + completion(); + } + } +} + +- (void)reloadData +{ + [self reloadDataWithCompletion:nil]; +} + +- (void)relayoutItems +{ + [self.view relayoutItems]; +} + +- (void)performBatchAnimated:(BOOL)animated updates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + ASTableView *tableView = self.view; + [tableView beginUpdates]; + if (updates) { + updates(); + } + [tableView endUpdatesAnimated:animated completion:completion]; + } else { + if (updates) { + updates(); + } + } +} + +- (void)performBatchUpdates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion +{ + [self performBatchAnimated:YES updates:updates completion:completion]; +} + +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view insertSections:sections withRowAnimation:animation]; + } +} + +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view deleteSections:sections withRowAnimation:animation]; + } +} + +- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view reloadSections:sections withRowAnimation:animation]; + } +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view moveSection:section toSection:newSection]; + } +} + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; + } +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; + } +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; + } +} + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; + } +} + +- (BOOL)isProcessingUpdates +{ + return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO); +} + +- (void)onDidFinishProcessingUpdates:(void (^)())completion +{ + if (!completion) { + return; + } + if (!self.nodeLoaded) { + completion(); + } else { + [self.view onDidFinishProcessingUpdates:completion]; + } +} + +- (void)waitUntilAllUpdatesAreProcessed +{ + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view waitUntilAllUpdatesAreCommitted]; + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)waitUntilAllUpdatesAreCommitted +{ + [self waitUntilAllUpdatesAreProcessed]; +} +#pragma clang diagnostic pop + +#pragma mark - Debugging (Private) + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [super propertiesForDebugDescription]; + [result addObject:@{ @"dataSource" : ASObjectDescriptionMakeTiny(self.dataSource) }]; + [result addObject:@{ @"delegate" : ASObjectDescriptionMakeTiny(self.delegate) }]; + return result; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTableView.h b/submodules/AsyncDisplayKit/Source/ASTableView.h new file mode 100644 index 0000000000..3f627beb11 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableView.h @@ -0,0 +1,249 @@ +// +// ASTableView.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCellNode; +@protocol ASTableDataSource; +@protocol ASTableDelegate; +@class ASTableNode; + +/** + * Asynchronous UITableView with Intelligent Preloading capabilities. + * + * @note ASTableNode is strongly recommended over ASTableView. This class is provided for adoption convenience. + */ +@interface ASTableView : UITableView + +/// The corresponding table node, or nil if one does not exist. +@property (nonatomic, weak, readonly) ASTableNode *tableNode; + +/** + * Retrieves the node for the row at the given index path. + */ +- (nullable ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; + +@end + +@interface ASTableView (Deprecated) + +@property (nonatomic, weak) id asyncDelegate ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's .delegate property instead."); +@property (nonatomic, weak) id asyncDataSource ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode .dataSource property instead."); + +/** + * Initializer. + * + * @param frame A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. + * The frame of the table view changes as table cells are added and deleted. + * + * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. + */ +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style ASDISPLAYNODE_DEPRECATED_MSG("Please use ASTableNode instead of ASTableView."); + +/** + * The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called. + * + * Defaults to two screenfuls. + */ +@property (nonatomic) CGFloat leadingScreensForBatching ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +/** + * The distance that the content view is inset from the table view edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets contentInset ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead"); + +/** + * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. + */ +@property (nonatomic) CGPoint contentOffset ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +/** + * YES to automatically adjust the contentOffset when cells are inserted or deleted above + * visible cells, maintaining the users' visible scroll position. + * + * @note This is only applied to non-animated updates. For animated updates, there is no way to + * synchronize or "cancel out" the appearance of a scroll due to UITableView API limitations. + * + * default is NO. + */ +@property (nonatomic) BOOL automaticallyAdjustsContentOffset ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +/* + * A Boolean value that determines whether the nodes that the data source renders will be flipped. + */ +@property (nonatomic) BOOL inverted ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedRows ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +@property (nonatomic, readonly, nullable) NSArray *indexPathsForVisibleRows ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +/** + * Tuning parameters for a range type in full mode. + * + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in full mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Set the tuning parameters for a range type in full mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Tuning parameters for a range type in the specified mode. + * + * @param rangeMode The range mode to get the running parameters for. + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in the given mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Set the tuning parameters for a range type in the specified mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeMode The range mode to set the running parameters for. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (nullable NSArray *)indexPathsForRowsInRect:(CGRect)rect ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Similar to -visibleCells. + * + * @return an array containing the cell nodes being displayed on screen. + */ +- (NSArray *)visibleNodes AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Similar to -indexPathForCell:. + * + * @param cellNode a cellNode part of the table view + * + * @return an indexPath for this cellNode + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @param completion block to run on completion of asynchronous loading or nil. If supplied, the block is run on + * the main thread. + * @warning This method is substantially more expensive than UITableView's version. + */ +-(void)reloadDataWithCompletion:(void (^ _Nullable)(void))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UITableView's version. + */ +- (void)reloadData ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +/** + * Triggers a relayout of all nodes. + * + * @discussion This method invalidates and lays out every cell node in the table view. + */ +- (void)relayoutItems ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)beginUpdates ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's -performBatchUpdates:completion: instead."); + +- (void)endUpdates ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's -performBatchUpdates:completion: instead."); + +/** + * Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view. + * You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations + * to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating + * the operations simultaneously. This method is must be called from the main thread. It's important to remember that the ASTableView will + * be processing the updates asynchronously after this call and are not guaranteed to be reflected in the ASTableView until + * the completion block is executed. + * + * @param animated NO to disable all animations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL completed))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's -performBatchUpdates:completion: instead."); + +/** + * See ASTableNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(void (^)(void))completion; +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASTableNode waitUntilAllUpdatesAreProcessed] instead."); + +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + +@end + +ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASTableDataSource.") +@protocol ASTableViewDataSource +@end + +ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASTableDelegate.") +@protocol ASTableViewDelegate +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTableView.mm b/submodules/AsyncDisplayKit/Source/ASTableView.mm new file mode 100644 index 0000000000..170abb50b5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableView.mm @@ -0,0 +1,2044 @@ +// +// ASTableView.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +/** + * See note at the top of ASCollectionView.mm near declaration of macro GET_COLLECTIONNODE_OR_RETURN + */ +#define GET_TABLENODE_OR_RETURN(__var, __val) \ + ASTableNode *__var = self.tableNode; \ + if (__var == nil) { \ + return __val; \ + } + +#define UITABLEVIEW_RESPONDS_TO_SELECTOR() \ + ({ \ + static BOOL superResponds; \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + superResponds = [UITableView instancesRespondToSelector:_cmd]; \ + }); \ + superResponds; \ + }) + +@interface UITableView (ScrollViewDelegate) + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView; +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView; +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset; +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; + +@end + +#pragma mark - +#pragma mark ASCellNode<->UITableViewCell bridging. + +@class _ASTableViewCell; + +@protocol _ASTableViewCellDelegate +- (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell; +@end + +@interface _ASTableViewCell : UITableViewCell +@property (nonatomic, weak) id<_ASTableViewCellDelegate> delegate; +@property (nonatomic, readonly) ASCellNode *node; +@property (nonatomic) ASCollectionElement *element; +@end + +@implementation _ASTableViewCell +// TODO add assertions to prevent use of view-backed UITableViewCell properties (eg .textLabel) + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [_delegate didLayoutSubviewsOfTableViewCell:self]; +} + +- (void)didTransitionToState:(UITableViewCellStateMask)state +{ + [self setNeedsLayout]; + [self layoutIfNeeded]; + [super didTransitionToState:state]; +} + +- (ASCellNode *)node +{ + return self.element.node; +} + +- (void)setElement:(ASCollectionElement *)element +{ + _element = element; + ASCellNode *node = element.node; + + if (node) { + self.backgroundColor = node.backgroundColor; + self.selectedBackgroundView = node.selectedBackgroundView; + self.backgroundView = node.backgroundView; +#if TARGET_OS_IOS + self.separatorInset = node.separatorInset; +#endif + self.selectionStyle = node.selectionStyle; + self.focusStyle = node.focusStyle; + self.accessoryType = node.accessoryType; + self.tintColor = node.tintColor; + + // the following ensures that we clip the entire cell to it's bounds if node.clipsToBounds is set (the default) + // This is actually a workaround for a bug we are seeing in some rare cases (selected background view + // overlaps other cells if size of ASCellNode has changed.) + self.clipsToBounds = node.clipsToBounds; + } + + [node __setSelectedFromUIKit:self.selected]; + [node __setHighlightedFromUIKit:self.highlighted]; +} + +- (BOOL)consumesCellNodeVisibilityEvents +{ + ASCellNode *node = self.node; + if (node == nil) { + return NO; + } + return ASSubclassOverridesSelector([ASCellNode class], [node class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:)); +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated +{ + [super setSelected:selected animated:animated]; + [self.node __setSelectedFromUIKit:selected]; +} + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + [super setHighlighted:highlighted animated:animated]; + [self.node __setHighlightedFromUIKit:highlighted]; +} + +- (void)prepareForReuse +{ + // Need to clear element before UIKit calls setSelected:NO / setHighlighted:NO on its cells + self.element = nil; + [super prepareForReuse]; +} + +@end + +#pragma mark - +#pragma mark ASTableView + +@interface ASTableView () +{ + ASTableViewProxy *_proxyDataSource; + ASTableViewProxy *_proxyDelegate; + + ASTableLayoutController *_layoutController; + + ASRangeController *_rangeController; + + ASBatchContext *_batchContext; + + // When we update our data controller in response to an interactive move, + // we don't want to tell the table view about the change (it knows!) + BOOL _updatingInResponseToInteractiveMove; + BOOL _inverted; + + // The top cell node that was visible before the update. + __weak ASCellNode *_contentOffsetAdjustmentTopVisibleNode; + // The y-offset of the top visible row's origin before the update. + CGFloat _contentOffsetAdjustmentTopVisibleNodeOffset; + CGFloat _leadingScreensForBatching; + BOOL _automaticallyAdjustsContentOffset; + + CGPoint _deceleratingVelocity; + + CGFloat _nodesConstrainedWidth; + BOOL _queuedNodeHeightUpdate; + BOOL _isDeallocating; + NSHashTable<_ASTableViewCell *> *_cellsForVisibilityUpdates; + + // CountedSet because UIKit may display the same element in multiple cells e.g. during animations. + NSCountedSet *_visibleElements; + + NSHashTable *_cellsForLayoutUpdates; + + // See documentation on same property in ASCollectionView + BOOL _hasEverCheckedForBatchFetchingDueToUpdate; + + // The section index overlay view, if there is one present. + // This is useful because we need to measure our row nodes against (width - indexView.width). + __weak UIView *_sectionIndexView; + + /** + * The change set that we're currently building, if any. + */ + _ASHierarchyChangeSet *_changeSet; + + /** + * Counter used to keep track of nested batch updates. + */ + NSInteger _batchUpdateCount; + + /** + * Keep a strong reference to node till view is ready to release. + */ + ASTableNode *_keepalive_node; + + struct { + unsigned int scrollViewDidScroll:1; + unsigned int scrollViewWillBeginDragging:1; + unsigned int scrollViewDidEndDragging:1; + unsigned int scrollViewWillEndDragging:1; + unsigned int scrollViewDidEndDecelerating:1; + unsigned int tableNodeWillDisplayNodeForRow:1; + unsigned int tableViewWillDisplayNodeForRow:1; + unsigned int tableViewWillDisplayNodeForRowDeprecated:1; + unsigned int tableNodeDidEndDisplayingNodeForRow:1; + unsigned int tableViewDidEndDisplayingNodeForRow:1; + unsigned int tableNodeWillBeginBatchFetch:1; + unsigned int tableViewWillBeginBatchFetch:1; + unsigned int shouldBatchFetchForTableView:1; + unsigned int shouldBatchFetchForTableNode:1; + unsigned int tableViewConstrainedSizeForRow:1; + unsigned int tableNodeConstrainedSizeForRow:1; + unsigned int tableViewWillSelectRow:1; + unsigned int tableNodeWillSelectRow:1; + unsigned int tableViewDidSelectRow:1; + unsigned int tableNodeDidSelectRow:1; + unsigned int tableViewWillDeselectRow:1; + unsigned int tableNodeWillDeselectRow:1; + unsigned int tableViewDidDeselectRow:1; + unsigned int tableNodeDidDeselectRow:1; + unsigned int tableViewShouldHighlightRow:1; + unsigned int tableNodeShouldHighlightRow:1; + unsigned int tableViewDidHighlightRow:1; + unsigned int tableNodeDidHighlightRow:1; + unsigned int tableViewDidUnhighlightRow:1; + unsigned int tableNodeDidUnhighlightRow:1; + unsigned int tableViewShouldShowMenuForRow:1; + unsigned int tableNodeShouldShowMenuForRow:1; + unsigned int tableViewCanPerformActionForRow:1; + unsigned int tableNodeCanPerformActionForRow:1; + unsigned int tableViewPerformActionForRow:1; + unsigned int tableNodePerformActionForRow:1; + } _asyncDelegateFlags; + + struct { + unsigned int numberOfSectionsInTableView:1; + unsigned int numberOfSectionsInTableNode:1; + unsigned int tableNodeNumberOfRowsInSection:1; + unsigned int tableViewNumberOfRowsInSection:1; + unsigned int tableViewNodeBlockForRow:1; + unsigned int tableNodeNodeBlockForRow:1; + unsigned int tableViewNodeForRow:1; + unsigned int tableNodeNodeForRow:1; + unsigned int tableViewCanMoveRow:1; + unsigned int tableNodeCanMoveRow:1; + unsigned int tableViewMoveRow:1; + unsigned int tableNodeMoveRow:1; + unsigned int sectionIndexMethods:1; // if both section index methods are implemented + unsigned int modelIdentifierMethods:1; // if both modelIdentifierForElementAtIndexPath and indexPathForElementWithModelIdentifier are implemented + } _asyncDataSourceFlags; +} + +@property (nonatomic) ASDataController *dataController; + +@property (nonatomic, weak) ASTableNode *tableNode; + +@property (nonatomic) BOOL test_enableSuperUpdateCallLogging; +@end + +@implementation ASTableView +{ + __weak id _asyncDelegate; + __weak id _asyncDataSource; +} + +// Using _ASDisplayLayer ensures things like -layout are properly forwarded to ASTableNode. ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + ++ (Class)dataControllerClass +{ + return [ASDataController class]; +} + +#pragma mark - +#pragma mark Lifecycle + +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style +{ + return [self _initWithFrame:frame style:style dataControllerClass:nil owningNode:nil eventLog:nil]; +} + +- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass owningNode:(ASTableNode *)tableNode eventLog:(ASEventLog *)eventLog +{ + if (!(self = [super initWithFrame:frame style:style])) { + return nil; + } + _cellsForVisibilityUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + _cellsForLayoutUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + if (!dataControllerClass) { + dataControllerClass = [[self class] dataControllerClass]; + } + + _layoutController = [[ASTableLayoutController alloc] initWithTableView:self]; + + _rangeController = [[ASRangeController alloc] init]; + _rangeController.layoutController = _layoutController; + _rangeController.dataSource = self; + _rangeController.delegate = self; + + _dataController = [[dataControllerClass alloc] initWithDataSource:self node:tableNode eventLog:eventLog]; + _dataController.delegate = _rangeController; + + _leadingScreensForBatching = 2.0; + _batchContext = [[ASBatchContext alloc] init]; + _visibleElements = [[NSCountedSet alloc] init]; + + _automaticallyAdjustsContentOffset = NO; + + _nodesConstrainedWidth = self.bounds.size.width; + + _proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; + super.delegate = (id)_proxyDelegate; + + _proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; + super.dataSource = (id)_proxyDataSource; + + [self registerClass:_ASTableViewCell.class forCellReuseIdentifier:kCellReuseIdentifier]; + + // iOS 11 automatically uses estimated heights, so disable those (see PR #485) + if (AS_AT_LEAST_IOS11) { + super.estimatedRowHeight = 0.0; + super.estimatedSectionHeaderHeight = 0.0; + super.estimatedSectionFooterHeight = 0.0; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + NSLog(@"Warning: AsyncDisplayKit is not designed to be used with Interface Builder. Table properties set in IB will be lost."); + return [self initWithFrame:CGRectZero style:UITableViewStylePlain]; +} + +- (void)dealloc +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeCAssert(_batchUpdateCount == 0, @"ASTableView deallocated in the middle of a batch update."); + + // Sometimes the UIKit classes can call back to their delegate even during deallocation. + _isDeallocating = YES; + if (!ASActivateExperimentalFeature(ASExperimentalCollectionTeardown)) { + [self setAsyncDelegate:nil]; + [self setAsyncDataSource:nil]; + } + + // Data controller & range controller may own a ton of nodes, let's deallocate those off-main + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); +} + +#pragma mark - +#pragma mark Overrides + +- (void)setDataSource:(id)dataSource +{ + // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. + ASDisplayNodeAssert(dataSource == nil, @"ASTableView uses asyncDataSource, not UITableView's dataSource property."); +} + +- (void)setDelegate:(id)delegate +{ + // Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here. + ASDisplayNodeAssert(delegate == nil, @"ASTableView uses asyncDelegate, not UITableView's delegate property."); +} + +- (id)asyncDataSource +{ + return _asyncDataSource; +} + +- (void)setAsyncDataSource:(id)asyncDataSource +{ + // Changing super.dataSource will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle + // the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource + // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong + // reference to the old dataSource in this case because calls to ASTableViewProxy will start failing and cause crashes. + NS_VALID_UNTIL_END_OF_SCOPE id oldDataSource = self.dataSource; + + if (asyncDataSource == nil) { + _asyncDataSource = nil; + _proxyDataSource = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; + + memset(&_asyncDataSourceFlags, 0, sizeof(_asyncDataSourceFlags)); + } else { + _asyncDataSource = asyncDataSource; + _proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; + + _asyncDataSourceFlags.numberOfSectionsInTableView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]; + _asyncDataSourceFlags.numberOfSectionsInTableNode = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableNode:)]; + _asyncDataSourceFlags.tableViewNumberOfRowsInSection = [_asyncDataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]; + _asyncDataSourceFlags.tableNodeNumberOfRowsInSection = [_asyncDataSource respondsToSelector:@selector(tableNode:numberOfRowsInSection:)]; + _asyncDataSourceFlags.tableViewNodeForRow = [_asyncDataSource respondsToSelector:@selector(tableView:nodeForRowAtIndexPath:)]; + _asyncDataSourceFlags.tableNodeNodeForRow = [_asyncDataSource respondsToSelector:@selector(tableNode:nodeForRowAtIndexPath:)]; + _asyncDataSourceFlags.tableViewNodeBlockForRow = [_asyncDataSource respondsToSelector:@selector(tableView:nodeBlockForRowAtIndexPath:)]; + _asyncDataSourceFlags.tableNodeNodeBlockForRow = [_asyncDataSource respondsToSelector:@selector(tableNode:nodeBlockForRowAtIndexPath:)]; + _asyncDataSourceFlags.tableViewCanMoveRow = [_asyncDataSource respondsToSelector:@selector(tableView:canMoveRowAtIndexPath:)]; + _asyncDataSourceFlags.tableViewMoveRow = [_asyncDataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]; + _asyncDataSourceFlags.sectionIndexMethods = [_asyncDataSource respondsToSelector:@selector(sectionIndexTitlesForTableView:)] && [_asyncDataSource respondsToSelector:@selector(tableView:sectionForSectionIndexTitle:atIndex:)]; + _asyncDataSourceFlags.modelIdentifierMethods = [_asyncDataSource respondsToSelector:@selector(modelIdentifierForElementAtIndexPath:inNode:)] && [_asyncDataSource respondsToSelector:@selector(indexPathForElementWithModelIdentifier:inNode:)]; + + ASDisplayNodeAssert(_asyncDataSourceFlags.tableViewNodeBlockForRow + || _asyncDataSourceFlags.tableViewNodeForRow + || _asyncDataSourceFlags.tableNodeNodeBlockForRow + || _asyncDataSourceFlags.tableNodeNodeForRow, @"Data source must implement tableNode:nodeBlockForRowAtIndexPath: or tableNode:nodeForRowAtIndexPath:"); + ASDisplayNodeAssert(_asyncDataSourceFlags.tableNodeNumberOfRowsInSection || _asyncDataSourceFlags.tableViewNumberOfRowsInSection, @"Data source must implement tableNode:numberOfRowsInSection:"); + } + + _dataController.validationErrorSource = asyncDataSource; + super.dataSource = (id)_proxyDataSource; + [self _asyncDelegateOrDataSourceDidChange]; +} + +- (id)asyncDelegate +{ + return _asyncDelegate; +} + +- (void)setAsyncDelegate:(id)asyncDelegate +{ + // Changing super.delegate will trigger a setNeedsLayout, so this must happen on the main thread. + ASDisplayNodeAssertMainThread(); + + // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle + // the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate + // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong + // reference to the old delegate in this case because calls to ASTableViewProxy will start failing and cause crashes. + NS_VALID_UNTIL_END_OF_SCOPE id oldDelegate = super.delegate; + + if (asyncDelegate == nil) { + _asyncDelegate = nil; + _proxyDelegate = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; + + memset(&_asyncDelegateFlags, 0, sizeof(_asyncDelegateFlags)); + } else { + _asyncDelegate = asyncDelegate; + _proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; + + _asyncDelegateFlags.scrollViewDidScroll = [_asyncDelegate respondsToSelector:@selector(scrollViewDidScroll:)]; + + _asyncDelegateFlags.tableViewWillDisplayNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNode:forRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeWillDisplayNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:willDisplayRowWithNode:)]; + if (_asyncDelegateFlags.tableViewWillDisplayNodeForRow == NO) { + _asyncDelegateFlags.tableViewWillDisplayNodeForRowDeprecated = [_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]; + } + _asyncDelegateFlags.tableViewDidEndDisplayingNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeDidEndDisplayingNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didEndDisplayingRowWithNode:)]; + _asyncDelegateFlags.scrollViewWillEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]; + _asyncDelegateFlags.scrollViewDidEndDecelerating = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]; + _asyncDelegateFlags.tableViewWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; + _asyncDelegateFlags.tableNodeWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(tableNode:willBeginBatchFetchWithContext:)]; + _asyncDelegateFlags.shouldBatchFetchForTableView = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]; + _asyncDelegateFlags.shouldBatchFetchForTableNode = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableNode:)]; + _asyncDelegateFlags.scrollViewWillBeginDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]; + _asyncDelegateFlags.scrollViewDidEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]; + _asyncDelegateFlags.tableViewConstrainedSizeForRow = [_asyncDelegate respondsToSelector:@selector(tableView:constrainedSizeForRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeConstrainedSizeForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:constrainedSizeForRowAtIndexPath:)]; + + _asyncDelegateFlags.tableViewWillSelectRow = [_asyncDelegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeWillSelectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:willSelectRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewDidSelectRow = [_asyncDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeDidSelectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didSelectRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewWillDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeWillDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:willDeselectRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewDidDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeDidDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didDeselectRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewShouldHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableView:shouldHighlightRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeShouldHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableNode:shouldHighlightRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewDidHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableView:didHighlightRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeDidHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didHighlightRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewDidUnhighlightRow = [_asyncDelegate respondsToSelector:@selector(tableView:didUnhighlightRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeDidUnhighlightRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didUnhighlightRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewShouldShowMenuForRow = [_asyncDelegate respondsToSelector:@selector(tableView:shouldShowMenuForRowAtIndexPath:)]; + _asyncDelegateFlags.tableNodeShouldShowMenuForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:shouldShowMenuForRowAtIndexPath:)]; + _asyncDelegateFlags.tableViewCanPerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableView:canPerformAction:forRowAtIndexPath:withSender:)]; + _asyncDelegateFlags.tableNodeCanPerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:canPerformAction:forRowAtIndexPath:withSender:)]; + _asyncDelegateFlags.tableViewPerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableView:performAction:forRowAtIndexPath:withSender:)]; + _asyncDelegateFlags.tableNodePerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:performAction:forRowAtIndexPath:withSender:)]; + } + + super.delegate = (id)_proxyDelegate; + [self _asyncDelegateOrDataSourceDidChange]; +} + +- (void)_asyncDelegateOrDataSourceDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (_asyncDataSource == nil && _asyncDelegate == nil && !ASActivateExperimentalFeature(ASExperimentalSkipClearData)) { + [_dataController clearData]; + } +} + +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy +{ + if (proxy == _proxyDelegate) { + [self setAsyncDelegate:nil]; + } else if (proxy == _proxyDataSource) { + [self setAsyncDataSource:nil]; + } +} + +- (void)reloadDataWithCompletion:(void (^)())completion +{ + ASDisplayNodeAssertMainThread(); + + if (! _dataController.initialReloadDataHasBeenCalled) { + // If this is the first reload, forward to super immediately to prevent it from triggering more "initial" loads while our data controller is working. + [super reloadData]; + } + + void (^batchUpdatesCompletion)(BOOL); + if (completion) { + batchUpdatesCompletion = ^(BOOL) { + completion(); + }; + } + + [self beginUpdates]; + [_changeSet reloadData]; + [self endUpdatesWithCompletion:batchUpdatesCompletion]; +} + +- (void)reloadData +{ + [self reloadDataWithCompletion:nil]; +} + +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated +{ + if ([self validateIndexPath:indexPath]) { + [super scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; + } +} + +- (void)relayoutItems +{ + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + [_rangeController setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [_rangeController tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + [_rangeController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + return [_rangeController tuningParametersForRangeMode:rangeMode rangeType:rangeType]; +} + +- (ASElementMap *)elementMapForRangeController:(ASRangeController *)rangeController +{ + return _dataController.visibleMap; +} + +- (ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return [_dataController.visibleMap elementForItemAtIndexPath:indexPath].node; +} + +- (NSIndexPath *)convertIndexPathFromTableNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait +{ + NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; + if (viewIndexPath == nil && wait) { + [self waitUntilAllUpdatesAreCommitted]; + return [self convertIndexPathFromTableNode:indexPath waitingIfNeeded:NO]; + } + return viewIndexPath; +} + +- (NSIndexPath *)convertIndexPathToTableNode:(NSIndexPath *)indexPath +{ + if ([self validateIndexPath:indexPath] == nil) { + return nil; + } + + return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; +} + +- (NSArray *)convertIndexPathsToTableNode:(NSArray *)indexPaths +{ + if (indexPaths == nil) { + return nil; + } + + NSMutableArray *indexPathsArray = [NSMutableArray new]; + + for (NSIndexPath *indexPathInView in indexPaths) { + NSIndexPath *indexPath = [self convertIndexPathToTableNode:indexPathInView]; + if (indexPath != nil) { + [indexPathsArray addObject:indexPath]; + } + } + return indexPathsArray; +} + +- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode +{ + return [self indexPathForNode:cellNode waitingIfNeeded:NO]; +} + +/** + * Asserts that the index path is a valid view-index-path, and returns it if so, nil otherwise. + */ +- (nullable NSIndexPath *)validateIndexPath:(nullable NSIndexPath *)indexPath +{ + if (indexPath == nil) { + return nil; + } + + NSInteger section = indexPath.section; + if (section >= self.numberOfSections) { + ASDisplayNodeFailAssert(@"Table view index path has invalid section %lu, section count = %lu", (unsigned long)section, (unsigned long)self.numberOfSections); + return nil; + } + + NSInteger item = indexPath.item; + // item == NSNotFound means e.g. "scroll to this section" and is acceptable + if (item != NSNotFound && item >= [self numberOfRowsInSection:section]) { + ASDisplayNodeFailAssert(@"Table view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfRowsInSection:section]); + return nil; + } + + return indexPath; +} + +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode waitingIfNeeded:(BOOL)wait +{ + if (cellNode == nil) { + return nil; + } + + NSIndexPath *indexPath = [_dataController.visibleMap indexPathForElement:cellNode.collectionElement]; + indexPath = [self validateIndexPath:indexPath]; + if (indexPath == nil && wait) { + [self waitUntilAllUpdatesAreCommitted]; + return [self indexPathForNode:cellNode waitingIfNeeded:NO]; + } + return indexPath; +} + +- (NSArray *)visibleNodes +{ + const auto elements = [self visibleElementsForRangeController:_rangeController]; + return ASArrayByFlatMapping(elements, ASCollectionElement *e, e.node); +} + +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + // _changeSet must be available during batch update + ASDisplayNodeAssertTrue((_batchUpdateCount > 0) == (_changeSet != nil)); + + if (_batchUpdateCount == 0) { + _changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[_dataController itemCountsFromDataSource]]; + } + _batchUpdateCount++; +} + +- (void)endUpdates +{ + [self endUpdatesWithCompletion:nil]; +} + +- (void)endUpdatesWithCompletion:(void (^)(BOOL completed))completion +{ + // We capture the current state of whether animations are enabled if they don't provide us with one. + [self endUpdatesAnimated:[UIView areAnimationsEnabled] completion:completion]; +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssertNotNil(_changeSet, @"_changeSet must be available when batch update ends"); + + _batchUpdateCount--; + // Prevent calling endUpdatesAnimated:completion: in an unbalanced way + NSAssert(_batchUpdateCount >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call"); + + [_changeSet addCompletionHandler:completion]; + + if (_batchUpdateCount == 0) { + _ASHierarchyChangeSet *changeSet = _changeSet; + // Nil out _changeSet before forwarding to _dataController to allow the change set to cause subsequent batch updates on the same run loop + _changeSet = nil; + changeSet.animated = animated; + [_dataController updateWithChangeSet:changeSet]; + } +} + +- (BOOL)isProcessingUpdates +{ + return [_dataController isProcessingUpdates]; +} + +- (void)onDidFinishProcessingUpdates:(void (^)())completion +{ + [_dataController onDidFinishProcessingUpdates:completion]; +} + +- (void)waitUntilAllUpdatesAreCommitted +{ + ASDisplayNodeAssertMainThread(); + if (_batchUpdateCount > 0) { + // This assertion will be enabled soon. + // ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); + return; + } + + [_dataController waitUntilAllUpdatesAreProcessed]; +} + +- (void)layoutSubviews +{ + // Remeasure all rows if our row width has changed. + UIEdgeInsets contentInset = self.contentInset; + CGFloat constrainedWidth = self.bounds.size.width - [self sectionIndexWidth] - contentInset.left - contentInset.right; + if (constrainedWidth > 0 && _nodesConstrainedWidth != constrainedWidth) { + _nodesConstrainedWidth = constrainedWidth; + [_cellsForLayoutUpdates removeAllObjects]; + + [self beginUpdates]; + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; + [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; + } else { + if (_cellsForLayoutUpdates.count > 0) { + NSArray *nodes = [_cellsForLayoutUpdates allObjects]; + [_cellsForLayoutUpdates removeAllObjects]; + + const auto nodesSizeChanged = [[NSMutableArray alloc] init]; + [_dataController relayoutNodes:nodes nodesSizeChanged:nodesSizeChanged]; + if (nodesSizeChanged.count > 0) { + [self requeryNodeHeights]; + } + } + } + + // To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last + [super layoutSubviews]; + [_rangeController updateIfNeeded]; +} + +#pragma mark - +#pragma mark Editing + +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } + [self beginUpdates]; + [_changeSet insertSections:sections animationOptions:animation]; + [self endUpdates]; +} + +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } + [self beginUpdates]; + [_changeSet deleteSections:sections animationOptions:animation]; + [self endUpdates]; +} + +- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } + [self beginUpdates]; + [_changeSet reloadSections:sections animationOptions:animation]; + [self endUpdates]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet moveSection:section toSection:newSection animationOptions:UITableViewRowAnimationNone]; + [self endUpdates]; +} + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } + [self beginUpdates]; + [_changeSet insertItems:indexPaths animationOptions:animation]; + [self endUpdates]; +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } + [self beginUpdates]; + [_changeSet deleteItems:indexPaths animationOptions:animation]; + [self endUpdates]; +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } + [self beginUpdates]; + [_changeSet reloadItems:indexPaths animationOptions:animation]; + [self endUpdates]; +} + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +{ + ASDisplayNodeAssertMainThread(); + [self beginUpdates]; + [_changeSet moveItemAtIndexPath:indexPath toIndexPath:newIndexPath animationOptions:UITableViewRowAnimationNone]; + [self endUpdates]; +} + +#pragma mark - +#pragma mark adjust content offset + +- (void)beginAdjustingContentOffset +{ + NSIndexPath *firstVisibleIndexPath = [self.indexPathsForVisibleRows sortedArrayUsingSelector:@selector(compare:)].firstObject; + if (firstVisibleIndexPath) { + ASCellNode *node = [self nodeForRowAtIndexPath:firstVisibleIndexPath]; + if (node) { + _contentOffsetAdjustmentTopVisibleNode = node; + _contentOffsetAdjustmentTopVisibleNodeOffset = [self rectForRowAtIndexPath:firstVisibleIndexPath].origin.y - self.bounds.origin.y; + } + } +} + +- (void)endAdjustingContentOffsetAnimated:(BOOL)animated +{ + // We can't do this for animated updates. + if (animated) { + return; + } + + // We can't do this if we didn't have a top visible row before. + if (_contentOffsetAdjustmentTopVisibleNode == nil) { + return; + } + + NSIndexPath *newIndexPathForTopVisibleRow = [self indexPathForNode:_contentOffsetAdjustmentTopVisibleNode]; + // We can't do this if our top visible row was deleted + if (newIndexPathForTopVisibleRow == nil) { + return; + } + + CGFloat newRowOriginYInSelf = [self rectForRowAtIndexPath:newIndexPathForTopVisibleRow].origin.y - self.bounds.origin.y; + CGPoint newContentOffset = self.contentOffset; + newContentOffset.y += (newRowOriginYInSelf - _contentOffsetAdjustmentTopVisibleNodeOffset); + self.contentOffset = newContentOffset; + _contentOffsetAdjustmentTopVisibleNode = nil; +} + +#pragma mark - Intercepted selectors + +- (void)setTableHeaderView:(UIView *)tableHeaderView +{ + // Typically the view will be nil before setting it, but reset state if it is being re-hosted. + [self.tableHeaderView.asyncdisplaykit_node exitHierarchyState:ASHierarchyStateRangeManaged]; + [super setTableHeaderView:tableHeaderView]; + [self.tableHeaderView.asyncdisplaykit_node enterHierarchyState:ASHierarchyStateRangeManaged]; +} + +- (void)setTableFooterView:(UIView *)tableFooterView +{ + // Typically the view will be nil before setting it, but reset state if it is being re-hosted. + [self.tableFooterView.asyncdisplaykit_node exitHierarchyState:ASHierarchyStateRangeManaged]; + [super setTableFooterView:tableFooterView]; + [self.tableFooterView.asyncdisplaykit_node enterHierarchyState:ASHierarchyStateRangeManaged]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + _ASTableViewCell *cell = [self dequeueReusableCellWithIdentifier:kCellReuseIdentifier forIndexPath:indexPath]; + cell.delegate = self; + + ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; + cell.element = element; + ASCellNode *node = element.node; + if (node) { + [_rangeController configureContentView:cell.contentView forCellNode:node]; + } + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat height = 0.0; + + ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; + if (element != nil) { + ASCellNode *node = element.node; + ASDisplayNodeAssertNotNil(node, @"Node must not be nil!"); + height = [node layoutThatFits:element.constrainedSize].size.height; + } + +#if TARGET_OS_IOS + /** + * Weirdly enough, Apple expects the return value here to _include_ the height + * of the separator, if there is one! So if our node wants to be 43.5, we need + * to return 44. UITableView will make a cell of height 44 with a content view + * of height 43.5. + */ + if (tableView.separatorStyle != UITableViewCellSeparatorStyleNone) { + height += 1.0 / ASScreenScale(); + } +#endif + + return height; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return _dataController.visibleMap.numberOfSections; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return [_dataController.visibleMap numberOfItemsInSection:section]; +} + +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_TABLENODE_OR_RETURN(tableNode, nil); + NSIndexPath *convertedPath = [self convertIndexPathToTableNode:indexPath]; + if (convertedPath == nil) { + return nil; + } else { + return [_asyncDataSource modelIdentifierForElementAtIndexPath:convertedPath inNode:tableNode]; + } + } else { + return nil; + } +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_TABLENODE_OR_RETURN(tableNode, nil); + return [_asyncDataSource indexPathForElementWithModelIdentifier:identifier inNode:tableNode]; + } else { + return nil; + } +} + +- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (_asyncDataSourceFlags.tableViewCanMoveRow) { + return [_asyncDataSource tableView:self canMoveRowAtIndexPath:indexPath]; + } else { + return NO; + } +} + +- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath +{ + if (_asyncDataSourceFlags.tableViewMoveRow) { + [_asyncDataSource tableView:self moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + } + + // Move node after informing data source in case they call nodeAtIndexPath: + // Get up to date + [self waitUntilAllUpdatesAreCommitted]; + // Set our flag to suppress informing super about the change. + _updatingInResponseToInteractiveMove = YES; + // Submit the move + [self moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + // Wait for it to finish – should be fast! + [self waitUntilAllUpdatesAreCommitted]; + // Clear the flag + _updatingInResponseToInteractiveMove = NO; +} + +- (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = cell.element; + if (element) { + ASDisplayNodeAssertTrue([_dataController.visibleMap elementForItemAtIndexPath:indexPath] == element); + [_visibleElements addObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplayCell: %@, %@, %@", cell, self, indexPath); + return; + } + + ASCellNode *cellNode = element.node; + cellNode.scrollView = tableView; + + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); + + if (_asyncDelegateFlags.tableNodeWillDisplayNodeForRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + [_asyncDelegate tableNode:tableNode willDisplayRowWithNode:cellNode]; + } else if (_asyncDelegateFlags.tableViewWillDisplayNodeForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self willDisplayNode:cellNode forRowAtIndexPath:indexPath]; + } else if (_asyncDelegateFlags.tableViewWillDisplayNodeForRowDeprecated) { + [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; + } +#pragma clang diagnostic pop + + [_rangeController setNeedsUpdate]; + + if ([cell consumesCellNodeVisibilityEvents]) { + [_cellsForVisibilityUpdates addObject:cell]; + } +} + +- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. + ASCollectionElement *element = cell.element; + if (element) { + [_visibleElements removeObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingCell: %@, %@, %@", cell, self, indexPath); + return; + } + + ASCellNode *cellNode = element.node; + + [_rangeController setNeedsUpdate]; + + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); + if (_asyncDelegateFlags.tableNodeDidEndDisplayingNodeForRow) { + if (ASTableNode *tableNode = self.tableNode) { + [_asyncDelegate tableNode:tableNode didEndDisplayingRowWithNode:cellNode]; + } + } else if (_asyncDelegateFlags.tableViewDidEndDisplayingNodeForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + + [_cellsForVisibilityUpdates removeObject:cell]; + + cellNode.scrollView = nil; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeWillSelectRow) { + GET_TABLENODE_OR_RETURN(tableNode, indexPath); + NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; + // If this item was is gone, just let the table view do its default behavior and select. + if (result == nil) { + return indexPath; + } else { + result = [_asyncDelegate tableNode:tableNode willSelectRowAtIndexPath:result]; + result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; + return result; + } + } else if (_asyncDelegateFlags.tableViewWillSelectRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate tableView:self willSelectRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } else { + return indexPath; + } +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeDidSelectRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate tableNode:tableNode didSelectRowAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.tableViewDidSelectRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self didSelectRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeWillDeselectRow) { + GET_TABLENODE_OR_RETURN(tableNode, indexPath); + NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; + // If this item was is gone, just let the table view do its default behavior and deselect. + if (result == nil) { + return indexPath; + } else { + result = [_asyncDelegate tableNode:tableNode willDeselectRowAtIndexPath:result]; + result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; + return result; + } + } else if (_asyncDelegateFlags.tableViewWillDeselectRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate tableView:self willDeselectRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return indexPath; +} + +- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeDidDeselectRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate tableNode:tableNode didDeselectRowAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.tableViewDidDeselectRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self didDeselectRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeShouldHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate tableNode:tableNode shouldHighlightRowAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.tableViewShouldHighlightRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate tableView:self shouldHighlightRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return YES; +} + +- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeDidHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate tableNode:tableNode didHighlightRowAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.tableViewDidHighlightRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self didHighlightRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeDidHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate tableNode:tableNode didUnhighlightRowAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.tableViewDidUnhighlightRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self didUnhighlightRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } +} + +- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (_asyncDelegateFlags.tableNodeShouldShowMenuForRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate tableNode:tableNode shouldShowMenuForRowAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.tableViewShouldShowMenuForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate tableView:self shouldShowMenuForRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } + return NO; +} + +- (BOOL)tableView:(UITableView *)tableView canPerformAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender +{ + if (_asyncDelegateFlags.tableNodeCanPerformActionForRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + return [_asyncDelegate tableNode:tableNode canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; + } + } else if (_asyncDelegateFlags.tableViewCanPerformActionForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate tableView:self canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; +#pragma clang diagnostic pop + } + return NO; +} + +- (void)tableView:(UITableView *)tableView performAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender +{ + if (_asyncDelegateFlags.tableNodePerformActionForRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + indexPath = [self convertIndexPathToTableNode:indexPath]; + if (indexPath != nil) { + [_asyncDelegate tableNode:tableNode performAction:action forRowAtIndexPath:indexPath withSender:sender]; + } + } else if (_asyncDelegateFlags.tableViewPerformActionForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self performAction:action forRowAtIndexPath:indexPath withSender:sender]; +#pragma clang diagnostic pop + } +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { + [super scrollViewDidScroll:scrollView]; + return; + } + ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; + if (ASInterfaceStateIncludesVisible(interfaceState)) { + [self _checkForBatchFetching]; + } + for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) { + [[tableCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged + inScrollView:scrollView + withCellFrame:tableCell.frame]; + } + if (_asyncDelegateFlags.scrollViewDidScroll) { + [_asyncDelegate scrollViewDidScroll:scrollView]; + } +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { + [super scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + return; + } + CGPoint contentOffset = scrollView.contentOffset; + _deceleratingVelocity = CGPointMake( + contentOffset.x - ((targetContentOffset != NULL) ? targetContentOffset->x : 0), + contentOffset.y - ((targetContentOffset != NULL) ? targetContentOffset->y : 0) + ); + + if (targetContentOffset != NULL) { + ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); + [self _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset velocity:velocity]; + } + + if (_asyncDelegateFlags.scrollViewWillEndDragging) { + [_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:(targetContentOffset ? : &contentOffset)]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView +{ + if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { + [super scrollViewDidEndDecelerating:scrollView]; + return; + } + _deceleratingVelocity = CGPointZero; + + if (_asyncDelegateFlags.scrollViewDidEndDecelerating) { + [_asyncDelegate scrollViewDidEndDecelerating:scrollView]; + } +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { + [super scrollViewWillBeginDragging:scrollView]; + return; + } + // If a scroll happens the current range mode needs to go to full + _rangeController.contentHasBeenScrolled = YES; + [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; + + for (_ASTableViewCell *tableViewCell in _cellsForVisibilityUpdates) { + [[tableViewCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging + inScrollView:scrollView + withCellFrame:tableViewCell.frame]; + } + if (_asyncDelegateFlags.scrollViewWillBeginDragging) { + [_asyncDelegate scrollViewWillBeginDragging:scrollView]; + } +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate +{ + if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { + [super scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; + return; + } + for (_ASTableViewCell *tableViewCell in _cellsForVisibilityUpdates) { + [[tableViewCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging + inScrollView:scrollView + withCellFrame:tableViewCell.frame]; + } + if (_asyncDelegateFlags.scrollViewDidEndDragging) { + [_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; + } +} + +#pragma mark - Misc + +- (BOOL)inverted +{ + return _inverted; +} + +- (void)setInverted:(BOOL)inverted +{ + _inverted = inverted; +} + +- (CGFloat)leadingScreensForBatching +{ + return _leadingScreensForBatching; +} + +- (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching +{ + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); + } +} + +- (BOOL)automaticallyAdjustsContentOffset +{ + return _automaticallyAdjustsContentOffset; +} + +- (void)setAutomaticallyAdjustsContentOffset:(BOOL)automaticallyAdjustsContentOffset +{ + _automaticallyAdjustsContentOffset = automaticallyAdjustsContentOffset; +} + +#pragma mark - Scroll Direction + +- (ASScrollDirection)scrollDirection +{ + CGPoint scrollVelocity; + if (self.isTracking) { + scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; + } else { + scrollVelocity = _deceleratingVelocity; + } + + ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity]; + return ASScrollDirectionApplyTransform(scrollDirection, self.transform); +} + +- (ASScrollDirection)_scrollDirectionForVelocity:(CGPoint)scrollVelocity +{ + ASScrollDirection direction = ASScrollDirectionNone; + ASScrollDirection scrollableDirections = [self scrollableDirections]; + + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { // Can scroll vertically. + if (scrollVelocity.y < 0.0) { + direction |= ASScrollDirectionDown; + } else if (scrollVelocity.y > 0.0) { + direction |= ASScrollDirectionUp; + } + } + + return direction; +} + +- (ASScrollDirection)scrollableDirections +{ + ASScrollDirection scrollableDirection = ASScrollDirectionNone; + CGFloat totalContentWidth = self.contentSize.width + self.contentInset.left + self.contentInset.right; + CGFloat totalContentHeight = self.contentSize.height + self.contentInset.top + self.contentInset.bottom; + + if (self.alwaysBounceHorizontal || totalContentWidth > self.bounds.size.width) { // Can scroll horizontally. + scrollableDirection |= ASScrollDirectionHorizontalDirections; + } + if (self.alwaysBounceVertical || totalContentHeight > self.bounds.size.height) { // Can scroll vertically. + scrollableDirection |= ASScrollDirectionVerticalDirections; + } + return scrollableDirection; +} + + +#pragma mark - Batch Fetching + +- (ASBatchContext *)batchContext +{ + return _batchContext; +} + +- (BOOL)canBatchFetch +{ + // if the delegate does not respond to this method, there is no point in starting to fetch + BOOL canFetch = _asyncDelegateFlags.tableNodeWillBeginBatchFetch || _asyncDelegateFlags.tableViewWillBeginBatchFetch; + if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableNode) { + GET_TABLENODE_OR_RETURN(tableNode, NO); + return [_asyncDelegate shouldBatchFetchForTableNode:tableNode]; + } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableView) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDelegate shouldBatchFetchForTableView:self]; +#pragma clang diagnostic pop + } else { + return canFetch; + } +} + +- (id)batchFetchingDelegate +{ + return self.tableNode.batchFetchingDelegate; +} + +- (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes +{ + // Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided + if (changes == 0 && _hasEverCheckedForBatchFetchingDueToUpdate) { + return; + } + _hasEverCheckedForBatchFetchingDueToUpdate = YES; + + // Push this to the next runloop to be sure the scroll view has the right content size + dispatch_async(dispatch_get_main_queue(), ^{ + [self _checkForBatchFetching]; + }); +} + +- (void)_checkForBatchFetching +{ + // Dragging will be handled in scrollViewWillEndDragging:withVelocity:targetContentOffset: + if (self.isDragging || self.isTracking) { + return; + } + + [self _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset velocity:CGPointZero]; +} + +- (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset velocity:(CGPoint)velocity +{ + if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, ASScrollDirectionVerticalDirections, contentOffset, velocity)) { + [self _beginBatchFetching]; + } +} + +- (void)_beginBatchFetching +{ + [_batchContext beginBatchFetching]; + if (_asyncDelegateFlags.tableNodeWillBeginBatchFetch) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + [_asyncDelegate tableNode:tableNode willBeginBatchFetchWithContext:_batchContext]; + }); + } else if (_asyncDelegateFlags.tableViewWillBeginBatchFetch) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext]; +#pragma clang diagnostic pop + }); + } +} + +#pragma mark - ASRangeControllerDataSource + +- (ASRangeController *)rangeController +{ + return _rangeController; +} + +- (NSHashTable *)visibleElementsForRangeController:(ASRangeController *)rangeController +{ + return ASPointerTableByFlatMapping(_visibleElements, id element, element); +} + +- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController +{ + return self.scrollDirection; +} + +- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController +{ + return ASInterfaceStateForDisplayNode(self.tableNode, self.window); +} + +- (NSString *)nameForRangeControllerDataSource +{ + return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); +} + +#pragma mark - ASRangeControllerDelegate + +- (BOOL)rangeControllerShouldUpdateRanges:(ASRangeController *)rangeController +{ + return YES; +} + +- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _updatingInResponseToInteractiveMove) { + updates(); + [changeSet executeCompletionHandlerWithFinished:NO]; + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (changeSet.includesReloadData) { + LOG(@"UITableView reloadData"); + ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super reloadData]"); + } + updates(); + [super reloadData]; + // Flush any range changes that happened as part of submitting the reload. + [_rangeController updateIfNeeded]; + [self _scheduleCheckForBatchFetchingForNumberOfChanges:1]; + [changeSet executeCompletionHandlerWithFinished:YES]; + }); + return; + } + + BOOL shouldAdjustContentOffset = (_automaticallyAdjustsContentOffset && !changeSet.includesReloadData); + if (shouldAdjustContentOffset) { + [self beginAdjustingContentOffset]; + } + + NSUInteger numberOfUpdates = 0; + + LOG(@"--- UITableView beginUpdates"); + [super beginUpdates]; + + updates(); + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { + NSArray *indexPaths = change.indexPaths; + UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; + + LOG(@"UITableView reloadRows:%ld rows", indexPaths.count); + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super reloadRowsAtIndexPaths]: %@", indexPaths); + } + [super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animationOptions]; + }); + + numberOfUpdates++; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { + NSIndexSet *sectionIndexes = change.indexSet; + UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; + + LOG(@"UITableView reloadSections:%@", sectionIndexes); + BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super reloadSections]: %@", sectionIndexes); + } + [super reloadSections:sectionIndexes withRowAnimation:animationOptions]; + }); + + numberOfUpdates++; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { + NSArray *indexPaths = change.indexPaths; + UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; + + LOG(@"UITableView deleteRows:%ld rows", indexPaths.count); + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super deleteRowsAtIndexPaths]: %@", indexPaths); + } + [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animationOptions]; + }); + + numberOfUpdates++; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { + NSIndexSet *sectionIndexes = change.indexSet; + UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; + + LOG(@"UITableView deleteSections:%@", sectionIndexes); + BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super deleteSections]: %@", sectionIndexes); + } + [super deleteSections:sectionIndexes withRowAnimation:animationOptions]; + }); + + numberOfUpdates++; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { + NSIndexSet *sectionIndexes = change.indexSet; + UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; + + LOG(@"UITableView insertSections:%@", sectionIndexes); + BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super insertSections]: %@", sectionIndexes); + } + [super insertSections:sectionIndexes withRowAnimation:animationOptions]; + }); + + numberOfUpdates++; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { + NSArray *indexPaths = change.indexPaths; + UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; + + LOG(@"UITableView insertRows:%ld rows", indexPaths.count); + BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super insertRowsAtIndexPaths]: %@", indexPaths); + } + [super insertRowsAtIndexPaths:indexPaths withRowAnimation:animationOptions]; + }); + + numberOfUpdates++; + } + + LOG(@"--- UITableView endUpdates"); + ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ + [super endUpdates]; + [_rangeController updateIfNeeded]; + [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; + }); + if (shouldAdjustContentOffset) { + [self endAdjustingContentOffsetAnimated:changeSet.animated]; + } + [changeSet executeCompletionHandlerWithFinished:YES]; +} + +#pragma mark - ASDataControllerSource + +- (BOOL)dataController:(ASDataController *)dataController shouldEagerlyLayoutNode:(ASCellNode *)node +{ + return YES; +} + +- (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataController +{ + return NO; +} + +- (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet +{ + // Reload data is expensive, don't block main while doing so. + if (changeSet.includesReloadData) { + return NO; + } + // For more details on this method, see the comment in the ASCollectionView implementation. + if (changeSet.countForAsyncLayout < 2) { + return YES; + } + CGSize contentSize = self.contentSize; + CGSize boundsSize = self.bounds.size; + if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { + return YES; + } + return NO; +} + +- (void)dataControllerDidFinishWaiting:(ASDataController *)dataController +{ + // ASCellLayoutMode is not currently supported on ASTableView (see ASCollectionView for details). +} + +- (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath +{ + // Not currently supported for tables. Will be added when the collection API stabilizes. + return nil; +} + +- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout +{ + ASCellNodeBlock block = nil; + + if (_asyncDataSourceFlags.tableNodeNodeBlockForRow) { + if (ASTableNode *tableNode = self.tableNode) { + block = [_asyncDataSource tableNode:tableNode nodeBlockForRowAtIndexPath:indexPath]; + } + } else if (_asyncDataSourceFlags.tableNodeNodeForRow) { + ASCellNode *node = nil; + if (ASTableNode *tableNode = self.tableNode) { + node = [_asyncDataSource tableNode:tableNode nodeForRowAtIndexPath:indexPath]; + } + if ([node isKindOfClass:[ASCellNode class]]) { + block = ^{ + return node; + }; + } else { + ASDisplayNodeFailAssert(@"Data source returned invalid node from tableNode:nodeForRowAtIndexPath:. Node: %@", node); + } + } else if (_asyncDataSourceFlags.tableViewNodeBlockForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + block = [_asyncDataSource tableView:self nodeBlockForRowAtIndexPath:indexPath]; + } else if (_asyncDataSourceFlags.tableViewNodeForRow) { + ASCellNode *node = [_asyncDataSource tableView:self nodeForRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + if ([node isKindOfClass:[ASCellNode class]]) { + block = ^{ + return node; + }; + } else { + ASDisplayNodeFailAssert(@"Data source returned invalid node from tableView:nodeForRowAtIndexPath:. Node: %@", node); + } + } + + // Handle nil node block + if (block == nil) { + ASDisplayNodeFailAssert(@"ASTableNode could not get a node block for row at index path %@", indexPath); + block = ^{ + return [[ASCellNode alloc] init]; + }; + } + + // Wrap the node block + __weak __typeof__(self) weakSelf = self; + return ^{ + __typeof__(self) strongSelf = weakSelf; + ASCellNode *node = (block != nil ? block() : [[ASCellNode alloc] init]); + ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASTableNode provided a non-ASCellNode! %@, %@", node, strongSelf); + + [node enterHierarchyState:ASHierarchyStateRangeManaged]; + if (node.interactionDelegate == nil) { + node.interactionDelegate = strongSelf; + } + if (_inverted) { + node.transform = CATransform3DMakeScale(1, -1, 1) ; + } + return node; + }; + return block; +} + +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + ASSizeRange constrainedSize = ASSizeRangeZero; + if (_asyncDelegateFlags.tableNodeConstrainedSizeForRow) { + GET_TABLENODE_OR_RETURN(tableNode, constrainedSize); + ASSizeRange delegateConstrainedSize = [_asyncDelegate tableNode:tableNode constrainedSizeForRowAtIndexPath:indexPath]; + // ignore widths in the returned size range (for TableView) + constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.min.height), + CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.max.height)); + } else if (_asyncDelegateFlags.tableViewConstrainedSizeForRow) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + ASSizeRange delegateConstrainedSize = [_asyncDelegate tableView:self constrainedSizeForRowAtIndexPath:indexPath]; +#pragma clang diagnostic pop + // ignore widths in the returned size range (for TableView) + constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.min.height), + CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.max.height)); + } else { + constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, 0), + CGSizeMake(_nodesConstrainedWidth, CGFLOAT_MAX)); + } + return constrainedSize; +} + +- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section +{ + if (_asyncDataSourceFlags.tableNodeNumberOfRowsInSection) { + GET_TABLENODE_OR_RETURN(tableNode, 0); + return [_asyncDataSource tableNode:tableNode numberOfRowsInSection:section]; + } else if (_asyncDataSourceFlags.tableViewNumberOfRowsInSection) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDataSource tableView:self numberOfRowsInSection:section]; +#pragma clang diagnostic pop + } else { + return 0; + } +} + +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController +{ + if (_asyncDataSourceFlags.numberOfSectionsInTableNode) { + GET_TABLENODE_OR_RETURN(tableNode, 0); + return [_asyncDataSource numberOfSectionsInTableNode:tableNode]; + } else if (_asyncDataSourceFlags.numberOfSectionsInTableView) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [_asyncDataSource numberOfSectionsInTableView:self]; +#pragma clang diagnostic pop + } else { + return 1; // default section number + } +} + +- (BOOL)dataController:(ASDataController *)dataController presentedSizeForElement:(ASCollectionElement *)element matchesSize:(CGSize)size +{ + NSIndexPath *indexPath = [self indexPathForNode:element.node]; + if (indexPath == nil) { + ASDisplayNodeFailAssert(@"Data controller should not ask for presented size for element that is not presented."); + return YES; + } + CGRect rect = [self rectForRowAtIndexPath:indexPath]; + +#if TARGET_OS_IOS + /** + * Weirdly enough, Apple expects the return value in tableView:heightForRowAtIndexPath: to _include_ the height + * of the separator, if there is one! So if rectForRow would return 44.0 we need to use 43.5. + */ + if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { + rect.size.height -= 1.0 / ASScreenScale(); + } +#endif + + return (fabs(rect.size.height - size.height) < FLT_EPSILON); +} + +#pragma mark - _ASTableViewCellDelegate + +- (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell +{ + ASCellNode *node = tableViewCell.node; + if (node == nil || _asyncDataSource == nil) { + return; + } + + CGFloat contentViewWidth = tableViewCell.contentView.bounds.size.width; + ASSizeRange constrainedSize = node.constrainedSizeForCalculatedLayout; + + // Table view cells should always fill its content view width. + // Normally the content view width equals to the constrained size width (which equals to the table view width). + // If there is a mismatch between these values, for example after the table view entered or left editing mode, + // content view width is preferred and used to re-measure the cell node. + if (CGSizeEqualToSize(node.calculatedSize, CGSizeZero) == NO && contentViewWidth != constrainedSize.max.width) { + constrainedSize.min.width = contentViewWidth; + constrainedSize.max.width = contentViewWidth; + + // Re-measurement is done on main to ensure thread affinity. In the worst case, this is as fast as UIKit's implementation. + // + // Unloaded nodes *could* be re-measured off the main thread, but only with the assumption that content view width + // is the same for all cells (because there is no easy way to get that individual value before the node being assigned to a _ASTableViewCell). + // Also, in many cases, some nodes may not need to be re-measured at all, such as when user enters and then immediately leaves editing mode. + // To avoid premature optimization and making such assumption, as well as to keep ASTableView simple, re-measurement is strictly done on main. + CGSize oldSize = node.bounds.size; + const CGSize calculatedSize = [node layoutThatFits:constrainedSize].size; + node.frame = { .size = calculatedSize }; + + // After the re-measurement, set the new constrained size to the node's backing colleciton element. + node.collectionElement.constrainedSize = constrainedSize; + + // If the node height changed, trigger a height requery. + if (oldSize.height != calculatedSize.height) { + [self beginUpdates]; + [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; + } + } +} + +#pragma mark - ASCellNodeDelegate + +- (void)nodeSelectedStateDidChange:(ASCellNode *)node +{ + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath) { + if (node.isSelected) { + [self selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; + } else { + [self deselectRowAtIndexPath:indexPath animated:NO]; + } + } +} + +- (void)nodeHighlightedStateDidChange:(ASCellNode *)node +{ + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath) { + [self cellForRowAtIndexPath:indexPath].highlighted = node.isHighlighted; + } +} + +- (void)nodeDidInvalidateSize:(ASCellNode *)node +{ + [_cellsForLayoutUpdates addObject:node]; + [self setNeedsLayout]; +} + +// Cause UITableView to requery for the new height of this node +- (void)requeryNodeHeights +{ + _queuedNodeHeightUpdate = NO; + + [super beginUpdates]; + [super endUpdates]; +} + +#pragma mark - Helper Methods + +// Note: This is called every layout, and so it is very performance sensitive. +- (CGFloat)sectionIndexWidth +{ + // If they don't implement the methods, then there's no section index. + if (_asyncDataSourceFlags.sectionIndexMethods == NO) { + return 0; + } + + UIView *indexView = _sectionIndexView; + if (indexView.superview == self) { + return indexView.frame.size.width; + } + + CGRect bounds = self.bounds; + for (UIView *view in self.subviews) { + CGRect frame = view.frame; + // Section index is right-aligned and less than half-width. + if (CGRectGetMaxX(frame) == CGRectGetMaxX(bounds) && frame.size.width * 2 < bounds.size.width) { + _sectionIndexView = view; + return frame.size.width; + } + } + return 0; +} + +#pragma mark - _ASDisplayView behavior substitutions +// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element. +// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView. +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + BOOL visible = (newWindow != nil); + ASDisplayNode *node = self.tableNode; + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + BOOL visible = (self.window != nil); + ASDisplayNode *node = self.tableNode; + BOOL rangeControllerNeedsUpdate = ![node supportsRangeManagedInterfaceState];; + + if (!visible && node.inHierarchy) { + if (rangeControllerNeedsUpdate) { + rangeControllerNeedsUpdate = NO; + // Exit CellNodes first before Table to match UIKit behaviors (tear down bottom up). + // Although we have not yet cleared the interfaceState's Visible bit (this happens in __exitHierarchy), + // the ASRangeController will get the correct value from -interfaceStateForRangeController:. + [_rangeController updateRanges]; + } + [node __exitHierarchy]; + } + + // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their + // their update in the layout pass + if (rangeControllerNeedsUpdate) { + [_rangeController updateRanges]; + } + + // When we aren't visible, we will only fetch up to the visible area. Now that we are visible, + // we will fetch visible area + leading screens, so we need to check. + if (visible) { + [self _checkForBatchFetching]; + } +} + +- (void)willMoveToSuperview:(UIView *)newSuperview +{ + if (self.superview == nil && newSuperview != nil) { + _keepalive_node = self.tableNode; + } +} + +- (void)didMoveToSuperview +{ + if (self.superview == nil) { + _keepalive_node = nil; + } +} + +#pragma mark - Accessibility overrides + +- (NSArray *)accessibilityElements +{ + [self waitUntilAllUpdatesAreCommitted]; + return [super accessibilityElements]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTableViewInternal.h b/submodules/AsyncDisplayKit/Source/ASTableViewInternal.h new file mode 100644 index 0000000000..b887e9323b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableViewInternal.h @@ -0,0 +1,69 @@ +// +// ASTableViewInternal.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +@class ASDataController; +@class ASTableNode; +@class ASRangeController; +@class ASEventLog; + +@interface ASTableView (Internal) + +@property (nonatomic, readonly) ASDataController *dataController; +@property (nonatomic, weak) ASTableNode *tableNode; +@property (nonatomic, readonly) ASRangeController *rangeController; + +/** + * Initializer. + * + * @param frame A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. + * The frame of the table view changes as table cells are added and deleted. + * + * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. + * + * @param dataControllerClass A controller class injected to and used to create a data controller for the table view. + * + * @param eventLog An event log passed through to the data controller. + */ +- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass owningNode:(ASTableNode *)tableNode eventLog:(ASEventLog *)eventLog; + +/// Set YES and we'll log every time we call [super insertRows…] etc +@property (nonatomic) BOOL test_enableSuperUpdateCallLogging; + +/** + * Attempt to get the view-layer index path for the row with the given index path. + * + * @param indexPath The index path of the row. + * @param wait If the item hasn't reached the view yet, this attempts to wait for updates to commit. + */ +- (NSIndexPath *)convertIndexPathFromTableNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait; + +/** + * Attempt to get the node index path given the view-layer index path. + * + * @param indexPath The index path of the row. + */ +- (NSIndexPath *)convertIndexPathToTableNode:(NSIndexPath *)indexPath; + +/** + * Attempt to get the node index paths given the view-layer index paths. + * + * @param indexPaths An array of index paths in the view space + */ +- (NSArray *)convertIndexPathsToTableNode:(NSArray *)indexPaths; + +/// Returns the width of the section index view on the right-hand side of the table, if one is present. +- (CGFloat)sectionIndexWidth; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTableViewProtocols.h b/submodules/AsyncDisplayKit/Source/ASTableViewProtocols.h new file mode 100644 index 0000000000..56de956724 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTableViewProtocols.h @@ -0,0 +1,98 @@ +// +// ASTableViewProtocols.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This is a subset of UITableViewDataSource. + * + * @see ASTableDataSource + */ +@protocol ASCommonTableDataSource + +@optional + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:numberOfRowsInSection: instead."); + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView ASDISPLAYNODE_DEPRECATED_MSG("Implement numberOfSectionsInTableNode: instead."); + +- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section; +- (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section; + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath; + +- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath; + +- (nullable NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView; +- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index; + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath; + +- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; + +@end + + +/** + * This is a subset of UITableViewDelegate. + * + * @see ASTableDelegate + */ +@protocol ASCommonTableViewDelegate + +@optional + +- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section; +- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section; +- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section; +- (void)tableView:(UITableView *)tableView didEndDisplayingFooterView:(UIView *)view forSection:(NSInteger)section; + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section; +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section; + +- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section; +- (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section; + +- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath; + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:shouldHighlightRowAtIndexPath: instead."); +- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:didHighlightRowAtIndexPath: instead."); +- (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:didUnhighlightRowAtIndexPath: instead."); + +- (nullable NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:willSelectRowAtIndexPath: instead."); +- (nullable NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:willDeselectRowAtIndexPath: instead."); +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:didSelectRowAtIndexPath: instead."); +- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:didDeselectRowAtIndexPath: instead."); + +- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath; +- (nullable NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath; +#if TARGET_OS_IOS +- (nullable NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath; +#endif +- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath; + +- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath; +- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath; + +- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath; + +- (NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath; + +- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:shouldShowMenuForRowAtIndexPath: instead."); +- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:canPerformAction:forRowAtIndexPath:withSender: instead."); +- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender ASDISPLAYNODE_DEPRECATED_MSG("Implement -tableNode:performAction:forRowAtIndexPath:withSender: instead."); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextNode+Beta.h b/submodules/AsyncDisplayKit/Source/ASTextNode+Beta.h new file mode 100644 index 0000000000..ad897c5f0e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNode+Beta.h @@ -0,0 +1,45 @@ +// +// ASTextNode+Beta.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASTextNode () + +/** + @abstract An array of descending scale factors that will be applied to this text node to try to make it fit within its constrained size + @discussion This array should be in descending order and NOT contain the scale factor 1.0. For example, it could return @[@(.9), @(.85), @(.8)]; + @default nil (no scaling) + */ +@property (nullable, nonatomic, copy) NSArray *pointSizeScaleFactors; + +/** + @abstract Text margins for text laid out in the text node. + @discussion defaults to UIEdgeInsetsZero. + This property can be useful for handling text which does not fit within the view by default. An example: like UILabel, + ASTextNode will clip the left and right of the string "judar" if it's rendered in an italicised font. + */ +@property (nonatomic) UIEdgeInsets textContainerInset; + +/** + * Returns YES if this node is using the experimental implementation. NO otherwise. Will not change. + */ +@property (readonly) BOOL usingExperiment; + +/** + * Returns a Boolean indicating if the text node will truncate for the given constrained size + */ +- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASTextNode.h b/submodules/AsyncDisplayKit/Source/ASTextNode.h new file mode 100644 index 0000000000..783eafc53e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNode.h @@ -0,0 +1,264 @@ +// +// ASTextNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +#if (!AS_ENABLE_TEXTNODE) + +// Pull in ASTextNode2 to replace ASTextNode with ASTextNode2 +#import + +#else + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract Draws interactive rich text. + @discussion Backed by TextKit. + */ +@interface ASTextNode : ASControlNode + +/** + @abstract The styled text displayed by the node. + @discussion Defaults to nil, no text is shown. + For inline image attachments, add an attribute of key NSAttachmentAttributeName, with a value of an NSTextAttachment. + */ +@property (nullable, copy) NSAttributedString *attributedText; + +#pragma mark - Truncation + +/** + @abstract The attributedText to use when the text must be truncated. + @discussion Defaults to a localized ellipsis character. + */ +@property (nullable, copy) NSAttributedString *truncationAttributedText; + +/** + @summary The second attributed string appended for truncation. + @discussion This string will be highlighted on touches. + @default nil + */ +@property (nullable, copy) NSAttributedString *additionalTruncationMessage; + +/** + @abstract Determines how the text is truncated to fit within the receiver's maximum size. + @discussion Defaults to NSLineBreakByWordWrapping. + @note Setting a truncationMode in attributedString will override the truncation mode set here. + */ +@property NSLineBreakMode truncationMode; + +/** + @abstract If the text node is truncated. Text must have been sized first. + */ +@property (readonly, getter=isTruncated) BOOL truncated; + +/** + @abstract The maximum number of lines to render of the text before truncation. + @default 0 (No limit) + */ +@property NSUInteger maximumNumberOfLines; + +/** + @abstract The number of lines in the text. Text must have been sized first. + */ +@property (readonly) NSUInteger lineCount; + +/** + * An array of path objects representing the regions where text should not be displayed. + * + * @discussion The default value of this property is an empty array. You can + * assign an array of UIBezierPath objects to exclude text from one or more regions in + * the text node's bounds. You can use this property to have text wrap around images, + * shapes or other text like a fancy magazine. + */ +@property (nullable, copy) NSArray *exclusionPaths; + +#pragma mark - Placeholders + +/** + * @abstract ASTextNode has a special placeholder behavior when placeholderEnabled is YES. + * + * @discussion Defaults to NO. When YES, it draws rectangles for each line of text, + * following the true shape of the text's wrapping. This visually mirrors the overall + * shape and weight of paragraphs, making the appearance of the finished text less jarring. + */ +@property BOOL placeholderEnabled; + +/** + @abstract The placeholder color. + */ +@property (nullable, copy) UIColor *placeholderColor; + +/** + @abstract Inset each line of the placeholder. + */ +@property UIEdgeInsets placeholderInsets; + +#pragma mark - Shadow + +/** + @abstract When you set these ASDisplayNode properties, they are composited into the bitmap instead of being applied by CA. + + @property (nonatomic) CGColorRef shadowColor; + @property (nonatomic) CGFloat shadowOpacity; + @property (nonatomic) CGSize shadowOffset; + @property (nonatomic) CGFloat shadowRadius; + */ + +/** + @abstract The number of pixels used for shadow padding on each side of the receiver. + @discussion Each inset will be less than or equal to zero, so that applying + UIEdgeInsetsRect(boundingRectForText, shadowPadding) + will return a CGRect large enough to fit both the text and the appropriate shadow padding. + */ +@property (readonly) UIEdgeInsets shadowPadding; + +#pragma mark - Positioning + +/** + @abstract Returns an array of rects bounding the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns an array of rects used for highlighting the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles the highlights of a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. This method is useful for visual coordination with a + highlighted range of text. + */ +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns a bounding rect for the given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion The height of the frame returned is that of the receiver's line-height; adjustment for + cap-height and descenders is not performed. This method raises an exception if textRange is not + a valid substring range of the receiver's string. + */ +- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns the trailing rectangle of space in the receiver, after the final character. + @discussion Use this method to detect which portion of the receiver is not occupied by characters. + The rect returned is in the coordinate system of the receiver. + */ +- (CGRect)trailingRect AS_WARN_UNUSED_RESULT; + + +#pragma mark - Actions + +/** + @abstract The set of attribute names to consider links. Defaults to NSLinkAttributeName. + */ +@property (copy) NSArray *linkAttributeNames; + +/** + @abstract Indicates whether the receiver has an entity at a given point. + @param point The point, in the receiver's coordinate system. + @param attributeNameOut The name of the attribute at the point. Can be NULL. + @param rangeOut The ultimate range of the found text. Can be NULL. + @result YES if an entity exists at `point`; NO otherwise. + */ +- (nullable id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString * _Nullable * _Nullable)attributeNameOut range:(out NSRange * _Nullable)rangeOut AS_WARN_UNUSED_RESULT; + +/** + @abstract The style to use when highlighting text. + */ +@property ASTextNodeHighlightStyle highlightStyle; + +/** + @abstract The range of text highlighted by the receiver. Changes to this property are not animated by default. + */ +@property NSRange highlightRange; + +/** + @abstract Set the range of text to highlight, with optional animation. + + @param highlightRange The range of text to highlight. + + @param animated Whether the text should be highlighted with an animation. + */ +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated; + +/** + @abstract Responds to actions from links in the text node. + @discussion The delegate must be set before the node is loaded, and implement + textNode:longPressedLinkAttribute:value:atPoint:textRange: in order for + the long press gesture recognizer to be installed. + */ +@property (nullable, weak) id delegate; + +/** + @abstract If YES and a long press is recognized, touches are cancelled. Default is NO + */ +@property (nonatomic) BOOL longPressCancelsTouches; + +/** + @abstract if YES will not intercept touches for non-link areas of the text. Default is NO. + */ +@property (nonatomic) BOOL passthroughNonlinkTouches; + +@end + +@interface ASTextNode (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +@end + +/** + * @abstract Text node unsupported properties + */ +@interface ASTextNode (Unsupported) + +@property (nullable, nonatomic) id textContainerLinePositionModifier; + +@end + +/** + * @abstract Text node deprecated properties + */ +@interface ASTextNode (Deprecated) + +/** + The attributedString and attributedText properties are equivalent, but attributedText is now the standard API + name in order to match UILabel and ASEditableTextNode. + + @see attributedText + */ +@property (nullable, copy) NSAttributedString *attributedString ASDISPLAYNODE_DEPRECATED_MSG("Use .attributedText instead."); + + +/** + The truncationAttributedString and truncationAttributedText properties are equivalent, but truncationAttributedText is now the + standard API name in order to match UILabel and ASEditableTextNode. + + @see truncationAttributedText + */ +@property (nullable, copy) NSAttributedString *truncationAttributedString ASDISPLAYNODE_DEPRECATED_MSG("Use .truncationAttributedText instead."); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextNode.mm b/submodules/AsyncDisplayKit/Source/ASTextNode.mm new file mode 100644 index 0000000000..e872a09297 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNode.mm @@ -0,0 +1,1494 @@ +// +// ASTextNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +#import + +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import + +#import +#import + +#import +#import +#import +#import + +/** + * If set, we will record all values set to attributedText into an array + * and once we get 2000, we'll write them all out into a plist file. + * + * This is useful for gathering realistic text data sets from apps for performance + * testing. + */ +#define AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS 0 + +static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; +static const NSTimeInterval ASTextNodeHighlightFadeInDuration = 0.1; +static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; +static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; +static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; + +#pragma mark - ASTextKitRenderer + +@interface ASTextNodeRendererKey : NSObject +@property (nonatomic) ASTextKitAttributes attributes; +@property (nonatomic) CGSize constrainedSize; +@end + +@implementation ASTextNodeRendererKey { + std::mutex _m; +} + +- (NSUInteger)hash +{ + std::lock_guard _l(_m); +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + size_t attributesHash; + CGSize constrainedSize; +#pragma clang diagnostic pop + } data = { + _attributes.hash(), + _constrainedSize + }; + return ASHashBytes(&data, sizeof(data)); +} + +- (BOOL)isEqual:(ASTextNodeRendererKey *)object +{ + if (self == object) { + return YES; + } + + // NOTE: Skip the class check for this specialized, internal Key object. + + // Lock both objects, avoiding deadlock. + std::lock(_m, object->_m); + std::lock_guard lk1(_m, std::adopt_lock); + std::lock_guard lk2(object->_m, std::adopt_lock); + + return _attributes == object->_attributes && CGSizeEqualToSize(_constrainedSize, object->_constrainedSize); +} + +@end + +static NSCache *sharedRendererCache() +{ + static dispatch_once_t onceToken; + static NSCache *__rendererCache = nil; + dispatch_once(&onceToken, ^{ + __rendererCache = [[NSCache alloc] init]; + __rendererCache.countLimit = 500; // 500 renders cache + }); + return __rendererCache; +} + +/** + The concept here is that neither the node nor layout should ever have a strong reference to the renderer object. + This is to reduce memory load when loading thousands and thousands of text nodes into memory at once. Instead + we maintain a LRU renderer cache that is queried via a unique key based on text kit attributes and constrained size. + */ + +static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize) +{ + NSCache *cache = sharedRendererCache(); + + ASTextNodeRendererKey *key = [[ASTextNodeRendererKey alloc] init]; + key.attributes = attributes; + key.constrainedSize = constrainedSize; + + ASTextKitRenderer *renderer = [cache objectForKey:key]; + if (renderer == nil) { + renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize]; + [cache setObject:renderer forKey:key]; + } + + return renderer; +} + +#pragma mark - ASTextNodeDrawParameter + +@interface ASTextNodeDrawParameter : NSObject { +@package + ASTextKitAttributes _rendererAttributes; + UIColor *_backgroundColor; + UIEdgeInsets _textContainerInsets; + CGFloat _contentScale; + BOOL _opaque; + CGRect _bounds; +} +@end + +@implementation ASTextNodeDrawParameter + +- (instancetype)initWithRendererAttributes:(ASTextKitAttributes)rendererAttributes + backgroundColor:(/*nullable*/ UIColor *)backgroundColor + textContainerInsets:(UIEdgeInsets)textContainerInsets + contentScale:(CGFloat)contentScale + opaque:(BOOL)opaque + bounds:(CGRect)bounds +{ + self = [super init]; + if (self != nil) { + _rendererAttributes = rendererAttributes; + _backgroundColor = backgroundColor; + _textContainerInsets = textContainerInsets; + _contentScale = contentScale; + _opaque = opaque; + _bounds = bounds; + } + return self; +} + +- (ASTextKitRenderer *)rendererForBounds:(CGRect)bounds +{ + CGRect rect = UIEdgeInsetsInsetRect(bounds, _textContainerInsets); + return rendererForAttributes(_rendererAttributes, rect.size); +} + +@end + + +#pragma mark - ASTextNode + +@interface ASTextNode () + +@end + +@implementation ASTextNode { + CGSize _shadowOffset; + CGColorRef _shadowColor; + UIColor *_cachedShadowUIColor; + UIColor *_placeholderColor; + CGFloat _shadowOpacity; + CGFloat _shadowRadius; + + UIEdgeInsets _textContainerInset; + + NSArray *_exclusionPaths; + + NSAttributedString *_attributedText; + NSAttributedString *_truncationAttributedText; + NSAttributedString *_additionalTruncationMessage; + NSAttributedString *_composedTruncationText; + NSArray *_pointSizeScaleFactors; + NSLineBreakMode _truncationMode; + + NSUInteger _maximumNumberOfLines; + + NSString *_highlightedLinkAttributeName; + id _highlightedLinkAttributeValue; + ASTextNodeHighlightStyle _highlightStyle; + NSRange _highlightRange; + ASHighlightOverlayLayer *_activeHighlightLayer; + + UILongPressGestureRecognizer *_longPressGestureRecognizer; +} +@dynamic placeholderEnabled; + +static NSArray *DefaultLinkAttributeNames() { + static NSArray *names; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + names = @[ NSLinkAttributeName ]; + }); + return names; +} + +- (instancetype)init +{ + if (self = [super init]) { + // Load default values from superclass. + _shadowOffset = [super shadowOffset]; + _shadowColor = CGColorRetain([super shadowColor]); + _shadowOpacity = [super shadowOpacity]; + _shadowRadius = [super shadowRadius]; + + // Disable user interaction for text node by default. + self.userInteractionEnabled = NO; + self.needsDisplayOnBoundsChange = YES; + + _truncationMode = NSLineBreakByWordWrapping; + + // The common case is for a text node to be non-opaque and blended over some background. + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + + self.linkAttributeNames = DefaultLinkAttributeNames(); + + // Accessibility + self.isAccessibilityElement = YES; + self.accessibilityTraits = self.defaultAccessibilityTraits; + + // Placeholders + // Disabled by default in ASDisplayNode, but add a few options for those who toggle + // on the special placeholder behavior of ASTextNode. + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); + _placeholderInsets = UIEdgeInsetsMake(1.0, 0.0, 1.0, 0.0); + } + + return self; +} + +- (void)dealloc +{ + CGColorRelease(_shadowColor); + + // TODO: This may not be necessary post-iOS-9 when most UIKit assign APIs + // were changed to weak. + if (_longPressGestureRecognizer) { + _longPressGestureRecognizer.delegate = nil; + [_longPressGestureRecognizer removeTarget:nil action:NULL]; + [self.view removeGestureRecognizer:_longPressGestureRecognizer]; + } +} + +#pragma mark - Description + +- (NSString *)_plainStringForDescription +{ + NSString *plainString = [[self.attributedText string] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + if (plainString.length > 50) { + plainString = [[plainString substringToIndex:50] stringByAppendingString:@"\u2026"]; + } + return plainString; +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [super propertiesForDescription]; + NSString *plainString = [self _plainStringForDescription]; + if (plainString.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(plainString) }]; + } + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [super propertiesForDebugDescription]; + NSString *plainString = [self _plainStringForDescription]; + if (plainString.length > 0) { + [result insertObject:@{ @"text" : ASStringWithQuotesIfMultiword(plainString) } atIndex:0]; + } + return result; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + // If we are view-backed and the delegate cares, support the long-press callback. + SEL longPressCallback = @selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:); + if (!self.isLayerBacked && [_delegate respondsToSelector:longPressCallback]) { + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_handleLongPress:)]; + _longPressGestureRecognizer.cancelsTouchesInView = self.longPressCancelsTouches; + _longPressGestureRecognizer.delegate = self; + [self.view addGestureRecognizer:_longPressGestureRecognizer]; + } +} + +- (BOOL)supportsLayerBacking +{ + if (!super.supportsLayerBacking) { + return NO; + } + + // If the text contains any links, return NO. + NSAttributedString *attributedText = self.attributedText; + NSRange range = NSMakeRange(0, attributedText.length); + for (NSString *linkAttributeName in _linkAttributeNames) { + __block BOOL hasLink = NO; + [attributedText enumerateAttribute:linkAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + hasLink = (value != nil); + *stop = YES; + }]; + if (hasLink) { + return NO; + } + } + return YES; +} + +#pragma mark - Renderer Management + +- (ASTextKitRenderer *)_renderer +{ + ASLockScopeSelf(); + return [self _locked_renderer]; +} + +- (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds +{ + ASLockScopeSelf(); + return [self _locked_rendererWithBounds:bounds]; +} + +- (ASTextKitRenderer *)_locked_renderer +{ + ASAssertLocked(__instanceLock__); + return [self _locked_rendererWithBounds:[self _locked_threadSafeBounds]]; +} + +- (ASTextKitRenderer *)_locked_rendererWithBounds:(CGRect)bounds +{ + ASAssertLocked(__instanceLock__); + bounds = UIEdgeInsetsInsetRect(bounds, _textContainerInset); + return rendererForAttributes([self _locked_rendererAttributes], bounds.size); +} + +- (ASTextKitAttributes)_locked_rendererAttributes +{ + ASAssertLocked(__instanceLock__); + return { + .attributedString = _attributedText, + .truncationAttributedString = [self _locked_composedTruncationText], + .lineBreakMode = _truncationMode, + .maximumNumberOfLines = _maximumNumberOfLines, + .exclusionPaths = _exclusionPaths, + // use the property getter so a subclass can provide these scale factors on demand if desired + .pointSizeScaleFactors = self.pointSizeScaleFactors, + .shadowOffset = _shadowOffset, + .shadowColor = _cachedShadowUIColor, + .shadowOpacity = _shadowOpacity, + .shadowRadius = _shadowRadius + }; +} + +- (NSString *)defaultAccessibilityLabel +{ + ASLockScopeSelf(); + return _attributedText.string; +} + +- (UIAccessibilityTraits)defaultAccessibilityTraits +{ + return UIAccessibilityTraitStaticText; +} + +#pragma mark - Layout and Sizing + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + if (ASLockedSelfCompareAssignCustom(_textContainerInset, textContainerInset, UIEdgeInsetsEqualToEdgeInsets)) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)textContainerInset +{ + return ASLockedSelf(_textContainerInset); +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASLockScopeSelf(); + + ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); + ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); + + // Cache the original constrained size for final size calculateion + CGSize originalConstrainedSize = constrainedSize; + + [self setNeedsDisplay]; + + ASTextKitRenderer *renderer = [self _locked_rendererWithBounds:{.size = constrainedSize}]; + CGSize size = renderer.size; + if (_attributedText.length > 0) { + self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; + self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender]; + if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) { + // while not perfect, this is a good estimate of what the ascender of the scaled font will be. + self.style.ascender *= renderer.currentScaleFactor; + self.style.descender *= renderer.currentScaleFactor; + } + } + + // Add the constrained size back textContainerInset + size.width += (_textContainerInset.left + _textContainerInset.right); + size.height += (_textContainerInset.top + _textContainerInset.bottom); + + return CGSizeMake(std::fmin(size.width, originalConstrainedSize.width), + std::fmin(size.height, originalConstrainedSize.height)); +} + +#pragma mark - Modifying User Text + +// Returns the ascender of the first character in attributedString by also including the line height if specified in paragraph style. ++ (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString +{ + UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL]; + NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL]; + if (!paragraphStyle) { + return font.ascender; + } + CGFloat lineHeight = MAX(font.lineHeight, paragraphStyle.minimumLineHeight); + if (paragraphStyle.maximumLineHeight > 0) { + lineHeight = MIN(lineHeight, paragraphStyle.maximumLineHeight); + } + return lineHeight + font.descender; +} + +- (NSAttributedString *)attributedText +{ + ASLockScopeSelf(); + return _attributedText; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + + if (attributedText == nil) { + attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; + } + + { + ASLockScopeSelf(); + if (ASObjectIsEqual(attributedText, _attributedText)) { + return; + } + + NSAttributedString *cleanedAttributedString = ASCleanseAttributedStringOfCoreTextAttributes(attributedText); + + // Invalidating the truncation text must be done while we still hold the lock. Because after we release it, + // another thread may set a new truncation text that will then be cleared by this thread, other may draw + // this soon-to-be-invalidated text. + [self _locked_invalidateTruncationText]; + + NSUInteger length = cleanedAttributedString.length; + if (length > 0) { + // Updating ascender and descender in one transaction while holding the lock. + ASLayoutElementStyle *style = [self _locked_style]; + style.ascender = [[self class] ascenderWithAttributedString:cleanedAttributedString]; + style.descender = [[attributedText attribute:NSFontAttributeName atIndex:cleanedAttributedString.length - 1 effectiveRange:NULL] descender]; + } + + // Update attributed text with cleaned attributed string + _attributedText = cleanedAttributedString; + } + + // Tell the display node superclasses that the cached layout is incorrect now + [self setNeedsLayout]; + + // Force display to create renderer with new size and redisplay with new string + [self setNeedsDisplay]; + + // Accessiblity + const auto currentAttributedText = self.attributedText; // Grab attributed string again in case it changed in the meantime + self.accessibilityLabel = self.defaultAccessibilityLabel; + self.isAccessibilityElement = (currentAttributedText.length != 0); // We're an accessibility element by default if there is a string. + +#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS + [ASTextNode _registerAttributedText:_attributedText]; +#endif +} + +#pragma mark - Text Layout + +- (void)setExclusionPaths:(NSArray *)exclusionPaths +{ + if (ASLockedSelfCompareAssignCopy(_exclusionPaths, exclusionPaths)) { + [self setNeedsLayout]; + [self setNeedsDisplay]; + } +} + +- (NSArray *)exclusionPaths +{ + return ASLockedSelf(_exclusionPaths); +} + +#pragma mark - Drawing + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + ASLockScopeSelf(); + + return [[ASTextNodeDrawParameter alloc] initWithRendererAttributes:[self _locked_rendererAttributes] + backgroundColor:self.backgroundColor + textContainerInsets:_textContainerInset + contentScale:_contentsScaleForDisplay + opaque:self.isOpaque + bounds:[self threadSafeBounds]]; +} + ++ (UIImage *)displayWithParameters:(id)parameters isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled +{ + ASTextNodeDrawParameter *drawParameter = (ASTextNodeDrawParameter *)parameters; + + if (drawParameter->_bounds.size.width <= 0 || drawParameter->_bounds.size.height <= 0) { + return nil; + } + + UIImage *result = nil; + UIColor *backgroundColor = drawParameter->_backgroundColor; + UIEdgeInsets textContainerInsets = drawParameter ? drawParameter->_textContainerInsets : UIEdgeInsetsZero; + ASTextKitRenderer *renderer = [drawParameter rendererForBounds:drawParameter->_bounds]; + BOOL renderedWithGraphicsRenderer = NO; + + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + if (ASActivateExperimentalFeature(ASExperimentalTextDrawing)) { + renderedWithGraphicsRenderer = YES; + UIGraphicsImageRenderer *graphicsRenderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(drawParameter->_bounds.size.width, drawParameter->_bounds.size.height)]; + result = [graphicsRenderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + ASDisplayNodeAssert(context, @"This is no good without a context."); + + CGContextSaveGState(context); + CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top); + + // Fill background + if (backgroundColor != nil) { + [backgroundColor setFill]; + UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); + } + + // Draw text + [renderer drawInContext:context bounds:drawParameter->_bounds]; + CGContextRestoreGState(context); + }]; + } + } + + if (!renderedWithGraphicsRenderer) { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(drawParameter->_bounds.size.width, drawParameter->_bounds.size.height), drawParameter->_opaque, drawParameter->_contentScale); + + CGContextRef context = UIGraphicsGetCurrentContext(); + ASDisplayNodeAssert(context, @"This is no good without a context."); + + CGContextSaveGState(context); + CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top); + + // Fill background + if (backgroundColor != nil) { + [backgroundColor setFill]; + UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); + } + + // Draw text + [renderer drawInContext:context bounds:drawParameter->_bounds]; + CGContextRestoreGState(context); + + result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + return result; +} + +#pragma mark - Attributes + +- (id)linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut +{ + return [self _linkAttributeValueAtPoint:point + attributeName:attributeNameOut + range:rangeOut + inAdditionalTruncationMessage:NULL + forHighlighting:NO]; +} + +- (id)_linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString * __autoreleasing *)attributeNameOut + range:(out NSRange *)rangeOut + inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut + forHighlighting:(BOOL)highlighting +{ + ASDisplayNodeAssertMainThread(); + + ASLockScopeSelf(); + + ASTextKitRenderer *renderer = [self _locked_renderer]; + NSRange visibleRange = renderer.firstVisibleRange; + NSAttributedString *attributedString = _attributedText; + NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, attributedString.length)); + + // Check in a 9-point region around the actual touch point so we make sure + // we get the best attribute for the touch. + __block CGFloat minimumGlyphDistance = CGFLOAT_MAX; + + // Final output vars + __block id linkAttributeValue = nil; + __block BOOL inTruncationMessage = NO; + + [renderer enumerateTextIndexesAtPosition:point usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) { + CGPoint glyphLocation = CGPointMake(CGRectGetMidX(glyphBoundingRect), CGRectGetMidY(glyphBoundingRect)); + CGFloat currentDistance = std::sqrt(std::pow(point.x - glyphLocation.x, 2.f) + std::pow(point.y - glyphLocation.y, 2.f)); + if (currentDistance >= minimumGlyphDistance) { + // If the distance computed from the touch to the glyph location is + // not the minimum among the located link attributes, we can just skip + // to the next location. + return; + } + + // Check if it's outside the visible range, if so, then we mark this touch + // as inside the truncation message, because in at least one of the touch + // points it was. + if (!(NSLocationInRange(characterIndex, visibleRange))) { + inTruncationMessage = YES; + } + + if (inAdditionalTruncationMessageOut != NULL) { + *inAdditionalTruncationMessageOut = inTruncationMessage; + } + + // Short circuit here if it's just in the truncation message. Since the + // truncation message may be beyond the scope of the actual input string, + // we have to make sure that we don't start asking for attributes on it. + if (inTruncationMessage) { + return; + } + + for (NSString *attributeName in self->_linkAttributeNames) { + NSRange range; + id value = [attributedString attribute:attributeName atIndex:characterIndex longestEffectiveRange:&range inRange:clampedRange]; + NSString *name = attributeName; + + if (value == nil || name == nil) { + // Didn't find anything + continue; + } + + // If highlighting, check with delegate first. If not implemented, assume YES. + if (highlighting + && [self->_delegate respondsToSelector:@selector(textNode:shouldHighlightLinkAttribute:value:atPoint:)] + && ![self->_delegate textNode:self shouldHighlightLinkAttribute:name value:value atPoint:point]) { + value = nil; + name = nil; + } + + if (value != nil || name != nil) { + // We found a minimum glyph distance link attribute, so set the min + // distance, and the out params. + minimumGlyphDistance = currentDistance; + + if (rangeOut != NULL && value != nil) { + *rangeOut = range; + // Limit to only the visible range, because the attributed string will + // return values outside the visible range. + if (NSMaxRange(*rangeOut) > NSMaxRange(visibleRange)) { + (*rangeOut).length = MAX(NSMaxRange(visibleRange) - (*rangeOut).location, 0); + } + } + + if (attributeNameOut != NULL) { + *attributeNameOut = name; + } + + // Set the values for the next iteration + linkAttributeValue = value; + + break; + } + } + }]; + + return linkAttributeValue; +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + ASDisplayNodeAssertMainThread(); + + if (gestureRecognizer == _longPressGestureRecognizer) { + // Don't allow long press on truncation message + if ([self _pendingTruncationTap]) { + return NO; + } + + // Ask our delegate if a long-press on an attribute is relevant + if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) { + return [_delegate textNode:self + shouldLongPressLinkAttribute:_highlightedLinkAttributeName + value:_highlightedLinkAttributeValue + atPoint:[gestureRecognizer locationInView:self.view]]; + } + + // Otherwise we are good to go. + return YES; + } + + if (([self _pendingLinkTap] || [self _pendingTruncationTap]) + && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] + && CGRectContainsPoint(self.threadSafeBounds, [gestureRecognizer locationInView:self.view])) { + return NO; + } + + return [super gestureRecognizerShouldBegin:gestureRecognizer]; +} + +#pragma mark - Highlighting + +- (ASTextNodeHighlightStyle)highlightStyle +{ + ASLockScopeSelf(); + + return _highlightStyle; +} + +- (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle +{ + ASLockScopeSelf(); + + _highlightStyle = highlightStyle; +} + +- (NSRange)highlightRange +{ + ASDisplayNodeAssertMainThread(); + + return _highlightRange; +} + +- (void)setHighlightRange:(NSRange)highlightRange +{ + [self setHighlightRange:highlightRange animated:NO]; +} + +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated +{ + [self _setHighlightRange:highlightRange forAttributeName:nil value:nil animated:animated]; +} + +- (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + + _highlightedLinkAttributeName = highlightedAttributeName; + _highlightedLinkAttributeValue = highlightedAttributeValue; + + if (!NSEqualRanges(highlightRange, _highlightRange) && ((0 != highlightRange.length) || (0 != _highlightRange.length))) { + + _highlightRange = highlightRange; + + if (_activeHighlightLayer) { + if (animated) { + __weak CALayer *weakHighlightLayer = _activeHighlightLayer; + _activeHighlightLayer = nil; + + weakHighlightLayer.opacity = 0.0; + + CFTimeInterval beginTime = CACurrentMediaTime(); + CABasicAnimation *possibleFadeIn = (CABasicAnimation *)[weakHighlightLayer animationForKey:@"opacity"]; + if (possibleFadeIn) { + // Calculate when we should begin fading out based on the end of the fade in animation, + // Also check to make sure that the new begin time hasn't already passed + CGFloat newBeginTime = (possibleFadeIn.beginTime + possibleFadeIn.duration); + if (newBeginTime > beginTime) { + beginTime = newBeginTime; + } + } + + CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeOut.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + fadeOut.fromValue = possibleFadeIn.toValue ? : @(((CALayer *)weakHighlightLayer.presentationLayer).opacity); + fadeOut.toValue = @0.0; + fadeOut.fillMode = kCAFillModeBoth; + fadeOut.duration = ASTextNodeHighlightFadeOutDuration; + fadeOut.beginTime = beginTime; + + dispatch_block_t prev = [CATransaction completionBlock]; + [CATransaction setCompletionBlock:^{ + [weakHighlightLayer removeFromSuperlayer]; + }]; + + [weakHighlightLayer addAnimation:fadeOut forKey:fadeOut.keyPath]; + + [CATransaction setCompletionBlock:prev]; + + } else { + [_activeHighlightLayer removeFromSuperlayer]; + _activeHighlightLayer = nil; + } + } + if (0 != highlightRange.length) { + // Find layer in hierarchy that allows us to draw highlighting on. + CALayer *highlightTargetLayer = self.layer; + while (highlightTargetLayer != nil) { + if (highlightTargetLayer.as_allowsHighlightDrawing) { + break; + } + highlightTargetLayer = highlightTargetLayer.superlayer; + } + + if (highlightTargetLayer != nil) { + ASLockScopeSelf(); + ASTextKitRenderer *renderer = [self _locked_renderer]; + + NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; + NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; + for (NSValue *rectValue in highlightRects) { + UIEdgeInsets shadowPadding = renderer.shadower.shadowPadding; + CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding); + + // The rects returned from renderer don't have `textContainerInset`, + // as well as they are using the `constrainedSize` for layout, + // so we can simply increase the rect by insets to get the full blown layout. + rendererRect.size.width += _textContainerInset.left + _textContainerInset.right; + rendererRect.size.height += _textContainerInset.top + _textContainerInset.bottom; + + CGRect highlightedRect = [self.layer convertRect:rendererRect toLayer:highlightTargetLayer]; + + // We set our overlay layer's frame to the bounds of the highlight target layer. + // Offset highlight rects to avoid double-counting target layer's bounds.origin. + highlightedRect.origin.x -= highlightTargetLayer.bounds.origin.x; + highlightedRect.origin.y -= highlightTargetLayer.bounds.origin.y; + [converted addObject:[NSValue valueWithCGRect:highlightedRect]]; + } + + ASHighlightOverlayLayer *overlayLayer = [[ASHighlightOverlayLayer alloc] initWithRects:converted]; + overlayLayer.highlightColor = [[self class] _highlightColorForStyle:self.highlightStyle]; + overlayLayer.frame = highlightTargetLayer.bounds; + overlayLayer.masksToBounds = NO; + overlayLayer.opacity = [[self class] _highlightOpacityForStyle:self.highlightStyle]; + [highlightTargetLayer addSublayer:overlayLayer]; + + if (animated) { + CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeIn.fromValue = @0.0; + fadeIn.toValue = @(overlayLayer.opacity); + fadeIn.duration = ASTextNodeHighlightFadeInDuration; + fadeIn.beginTime = CACurrentMediaTime(); + + [overlayLayer addAnimation:fadeIn forKey:fadeIn.keyPath]; + } + + [overlayLayer setNeedsDisplay]; + + _activeHighlightLayer = overlayLayer; + } + } + } +} + +- (void)_clearHighlightIfNecessary +{ + ASDisplayNodeAssertMainThread(); + + if ([self _pendingLinkTap] || [self _pendingTruncationTap]) { + [self setHighlightRange:NSMakeRange(0, 0) animated:YES]; + } +} + ++ (CGColorRef)_highlightColorForStyle:(ASTextNodeHighlightStyle)style +{ + return [UIColor colorWithWhite:(style == ASTextNodeHighlightStyleLight ? 0.0 : 1.0) alpha:1.0].CGColor; +} + ++ (CGFloat)_highlightOpacityForStyle:(ASTextNodeHighlightStyle)style +{ + return (style == ASTextNodeHighlightStyleLight) ? ASTextNodeHighlightLightOpacity : ASTextNodeHighlightDarkOpacity; +} + +#pragma mark - Text rects + +static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UIEdgeInsets shadowPadding) { + rendererRect.origin.x -= shadowPadding.left; + rendererRect.origin.y -= shadowPadding.top; + return rendererRect; +} + +- (NSArray *)rectsForTextRange:(NSRange)textRange +{ + return [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionCapHeight]; +} + +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange +{ + return [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionBlock]; +} + +- (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption +{ + ASLockScopeSelf(); + + NSArray *rects = [[self _locked_renderer] rectsForTextRange:textRange measureOption:measureOption]; + const auto adjustedRects = [[NSMutableArray alloc] init]; + + for (NSValue *rectValue in rects) { + CGRect rect = [rectValue CGRectValue]; + rect = ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding); + + NSValue *adjustedRectValue = [NSValue valueWithCGRect:rect]; + [adjustedRects addObject:adjustedRectValue]; + } + + return adjustedRects; +} + +- (CGRect)trailingRect +{ + ASLockScopeSelf(); + + CGRect rect = [[self _locked_renderer] trailingRect]; + return ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding); +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + ASLockScopeSelf(); + + CGRect frame = [[self _locked_renderer] frameForTextRange:textRange]; + return ASTextNodeAdjustRenderRectForShadowPadding(frame, self.shadowPadding); +} + +#pragma mark - Placeholders + +- (UIColor *)placeholderColor +{ + return ASLockedSelf(_placeholderColor); +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + if (ASLockedSelfCompareAssignCopy(_placeholderColor, placeholderColor)) { + self.placeholderEnabled = CGColorGetAlpha(placeholderColor.CGColor) > 0; + } +} + +- (UIImage *)placeholderImage +{ + // FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set. + // This would completely eliminate the memory and performance cost of the backing store. + CGSize size = self.calculatedSize; + if ((size.width * size.height) < CGFLOAT_EPSILON) { + return nil; + } + + ASLockScopeSelf(); + + ASGraphicsBeginImageContextWithOptions(size, NO, 1.0); + [self.placeholderColor setFill]; + + ASTextKitRenderer *renderer = [self _locked_renderer]; + NSRange visibleRange = renderer.firstVisibleRange; + + // cap height is both faster and creates less subpixel blending + NSArray *lineRects = [self _rectsForTextRange:visibleRange measureOption:ASTextKitRendererMeasureOptionLineHeight]; + + // fill each line with the placeholder color + for (NSValue *rectValue in lineRects) { + CGRect lineRect = [rectValue CGRectValue]; + CGRect fillBounds = CGRectIntegral(UIEdgeInsetsInsetRect(lineRect, self.placeholderInsets)); + + if (fillBounds.size.width > 0.0 && fillBounds.size.height > 0.0) { + UIRectFill(fillBounds); + } + } + + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + return image; +} + +#pragma mark - Touch Handling + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_passthroughNonlinkTouches) { + return [super pointInside:point withEvent:event]; + } + + NSRange range = NSMakeRange(0, 0); + NSString *linkAttributeName = nil; + BOOL inAdditionalTruncationMessage = NO; + + id linkAttributeValue = [self _linkAttributeValueAtPoint:point + attributeName:&linkAttributeName + range:&range + inAdditionalTruncationMessage:&inAdditionalTruncationMessage + forHighlighting:YES]; + + NSUInteger lastCharIndex = NSIntegerMax; + BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); + + if (range.length > 0 && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { + return YES; + } else { + return NO; + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + [super touchesBegan:touches withEvent:event]; + + CGPoint point = [[touches anyObject] locationInView:self.view]; + + NSRange range = NSMakeRange(0, 0); + NSString *linkAttributeName = nil; + BOOL inAdditionalTruncationMessage = NO; + + id linkAttributeValue = [self _linkAttributeValueAtPoint:point + attributeName:&linkAttributeName + range:&range + inAdditionalTruncationMessage:&inAdditionalTruncationMessage + forHighlighting:YES]; + + NSUInteger lastCharIndex = NSIntegerMax; + BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); + + if (inAdditionalTruncationMessage) { + NSRange visibleRange = NSMakeRange(0, 0); + { + ASLockScopeSelf(); + visibleRange = [self _locked_renderer].firstVisibleRange; + } + NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange]; + [self _setHighlightRange:truncationMessageRange forAttributeName:ASTextNodeTruncationTokenAttributeName value:nil animated:YES]; + } else if (range.length > 0 && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { + [self _setHighlightRange:range forAttributeName:linkAttributeName value:linkAttributeValue animated:YES]; + } +} + + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesCancelled:touches withEvent:event]; + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesEnded:touches withEvent:event]; + + if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint point = [[touches anyObject] locationInView:self.view]; + [_delegate textNode:self tappedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:point textRange:_highlightRange]; + } + + if ([self _pendingTruncationTap]) { + if ([_delegate respondsToSelector:@selector(textNodeTappedTruncationToken:)]) { + [_delegate textNodeTappedTruncationToken:self]; + } + } + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesMoved:touches withEvent:event]; + + UITouch *touch = [touches anyObject]; + CGPoint locationInView = [touch locationInView:self.view]; + // on 3D Touch enabled phones, this gets fired with changes in force, and usually will get fired immediately after touchesBegan:withEvent: + if (CGPointEqualToPoint([touch previousLocationInView:self.view], locationInView)) + return; + + // If touch has moved out of the current highlight range, clear the highlight. + if (_highlightRange.length > 0) { + NSRange range = NSMakeRange(0, 0); + [self _linkAttributeValueAtPoint:locationInView + attributeName:NULL + range:&range + inAdditionalTruncationMessage:NULL + forHighlighting:YES]; + + if (!NSEqualRanges(_highlightRange, range)) { + [self _clearHighlightIfNecessary]; + } + } +} + +- (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer +{ + ASDisplayNodeAssertMainThread(); + + // Respond to long-press when it begins, not when it ends. + if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { + if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; + [_delegate textNode:self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; + } + } +} + +- (BOOL)_pendingLinkTap +{ + ASLockScopeSelf(); + + return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil; +} + +- (BOOL)_pendingTruncationTap +{ + ASLockScopeSelf(); + + return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName]; +} + +#pragma mark - Shadow Properties + +- (CGColorRef)shadowColor +{ + return ASLockedSelf(_shadowColor); +} + +- (void)setShadowColor:(CGColorRef)shadowColor +{ + [self lock]; + + if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { + CGColorRelease(_shadowColor); + _shadowColor = CGColorRetain(shadowColor); + _cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor]; + [self unlock]; + + [self setNeedsDisplay]; + return; + } + + [self unlock]; +} + +- (CGSize)shadowOffset +{ + return ASLockedSelf(_shadowOffset); +} + +- (void)setShadowOffset:(CGSize)shadowOffset +{ + if (ASLockedSelfCompareAssignCustom(_shadowOffset, shadowOffset, CGSizeEqualToSize)) { + [self setNeedsDisplay]; + } +} + +- (CGFloat)shadowOpacity +{ + return ASLockedSelf(_shadowOpacity); +} + +- (void)setShadowOpacity:(CGFloat)shadowOpacity +{ + if (ASLockedSelfCompareAssign(_shadowOpacity, shadowOpacity)) { + [self setNeedsDisplay]; + } +} + +- (CGFloat)shadowRadius +{ + return ASLockedSelf(_shadowRadius); +} + +- (void)setShadowRadius:(CGFloat)shadowRadius +{ + if (ASLockedSelfCompareAssign(_shadowRadius, shadowRadius)) { + [self setNeedsDisplay]; + } +} + +- (UIEdgeInsets)shadowPadding +{ + ASLockScopeSelf(); + return [self _locked_renderer].shadower.shadowPadding; +} + +#pragma mark - Truncation Message + +static NSAttributedString *DefaultTruncationAttributedString() +{ + static NSAttributedString *defaultTruncationAttributedString; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultTruncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; + }); + return defaultTruncationAttributedString; +} + +- (NSAttributedString *)truncationAttributedText +{ + return ASLockedSelf(_truncationAttributedText); +} + +- (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText +{ + if (ASLockedSelfCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) { + [self _invalidateTruncationText]; + [self setNeedsDisplay]; + } +} + +- (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage +{ + if (ASLockedSelfCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) { + [self _invalidateTruncationText]; + [self setNeedsDisplay]; + } +} + +- (NSAttributedString *)additionalTruncationMessage +{ + return ASLockedSelf(_additionalTruncationMessage); +} + +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + if (ASLockedSelfCompareAssign(_truncationMode, truncationMode)) { + [self setNeedsDisplay]; + } +} + +- (NSLineBreakMode)truncationMode +{ + return ASLockedSelf(_truncationMode); +} + +- (BOOL)isTruncated +{ + return ASLockedSelf([[self _locked_renderer] isTruncated]); +} + +- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize +{ + return ASLockedSelf([[self _locked_rendererWithBounds:{.size = constrainedSize.max}] isTruncated]); +} + +- (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors +{ + if (ASLockedSelfCompareAssignCopy(_pointSizeScaleFactors, pointSizeScaleFactors)) { + [self setNeedsDisplay]; + } +} + +- (NSArray *)pointSizeScaleFactors +{ + return ASLockedSelf(_pointSizeScaleFactors); +} + +- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines +{ + if (ASLockedSelfCompareAssign(_maximumNumberOfLines, maximumNumberOfLines)) { + [self setNeedsDisplay]; + } +} + +- (NSUInteger)maximumNumberOfLines +{ + return ASLockedSelf(_maximumNumberOfLines); +} + +- (NSUInteger)lineCount +{ + return ASLockedSelf([[self _locked_renderer] lineCount]); +} + +#pragma mark - Truncation Message + +- (void)_invalidateTruncationText +{ + ASLockScopeSelf(); + [self _locked_invalidateTruncationText]; +} + +- (void)_locked_invalidateTruncationText +{ + _composedTruncationText = nil; +} + +/** + * @return the additional truncation message range within the as-rendered text. + * Must be called from main thread + */ +- (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange +{ + ASLockScopeSelf(); + + // Check if we even have an additional truncation message. + if (!_additionalTruncationMessage) { + return NSMakeRange(NSNotFound, 0); + } + + // Character location of the unicode ellipsis (the first index after the visible range) + NSInteger truncationTokenIndex = NSMaxRange(visibleRange); + + NSUInteger additionalTruncationMessageLength = _additionalTruncationMessage.length; + // We get the location of the truncation token, then add the length of the + // truncation attributed string +1 for the space between. + return NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength); +} + +/** + * @return the truncation message for the string. If there are both an + * additional truncation message and a truncation attributed string, they will + * be properly composed. + */ +- (NSAttributedString *)_locked_composedTruncationText +{ + ASAssertLocked(__instanceLock__); + if (_composedTruncationText == nil) { + if (_truncationAttributedText != nil && _additionalTruncationMessage != nil) { + NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedText]; + [newComposedTruncationString.mutableString appendString:@" "]; + [newComposedTruncationString appendAttributedString:_additionalTruncationMessage]; + _composedTruncationText = newComposedTruncationString; + } else if (_truncationAttributedText != nil) { + _composedTruncationText = _truncationAttributedText; + } else if (_additionalTruncationMessage != nil) { + _composedTruncationText = _additionalTruncationMessage; + } else { + _composedTruncationText = DefaultTruncationAttributedString(); + } + _composedTruncationText = [self _locked_prepareTruncationStringForDrawing:_composedTruncationText]; + } + return _composedTruncationText; +} + +/** + * - cleanses it of core text attributes so TextKit doesn't crash + * - Adds whole-string attributes so the truncation message matches the styling + * of the body text + */ +- (NSAttributedString *)_locked_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString +{ + ASAssertLocked(__instanceLock__); + truncationString = ASCleanseAttributedStringOfCoreTextAttributes(truncationString); + NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy]; + // Grab the attributes from the full string + if (_attributedText.length > 0) { + NSAttributedString *originalString = _attributedText; + NSInteger originalStringLength = _attributedText.length; + // Add any of the original string's attributes to the truncation string, + // but don't overwrite any of the truncation string's attributes + NSDictionary *originalStringAttributes = [originalString attributesAtIndex:originalStringLength-1 effectiveRange:NULL]; + [truncationString enumerateAttributesInRange:NSMakeRange(0, truncationString.length) options:0 usingBlock: + ^(NSDictionary *attributes, NSRange range, BOOL *stop) { + NSMutableDictionary *futureTruncationAttributes = [originalStringAttributes mutableCopy]; + [futureTruncationAttributes addEntriesFromDictionary:attributes]; + [truncationMutableString setAttributes:futureTruncationAttributes range:range]; + }]; + } + return truncationMutableString; +} + +#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS ++ (void)_registerAttributedText:(NSAttributedString *)str +{ + static NSMutableArray *array; + static NSLock *lock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lock = [NSLock new]; + array = [NSMutableArray new]; + }); + [lock lock]; + [array addObject:str]; + if (array.count % 20 == 0) { + NSLog(@"Got %d strings", (int)array.count); + } + if (array.count == 2000) { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AttributedStrings.plist"]; + NSAssert([NSKeyedArchiver archiveRootObject:array toFile:path], nil); + NSLog(@"Saved to %@", path); + } + [lock unlock]; +} +#endif + +// All direct descendants of ASTextNode get their superclass replaced by ASTextNode2. ++ (void)initialize +{ + // Texture requires that node subclasses call [super initialize] + [super initialize]; + + if (class_getSuperclass(self) == [ASTextNode class] + && ASActivateExperimentalFeature(ASExperimentalTextNode)) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + class_setSuperclass(self, [ASTextNode2 class]); +#pragma clang diagnostic pop + } +} + +// For direct allocations of ASTextNode itself, we override allocWithZone: ++ (id)allocWithZone:(struct _NSZone *)zone +{ + if (ASActivateExperimentalFeature(ASExperimentalTextNode)) { + return (ASTextNode *)[ASTextNode2 allocWithZone:zone]; + } else { + return [super allocWithZone:zone]; + } +} + +@end + +@implementation ASTextNode (Unsupported) + +- (void)setTextContainerLinePositionModifier:(id)textContainerLinePositionModifier +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); +} + +- (id)textContainerLinePositionModifier +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return nil; +} + +@end + +@implementation ASTextNode (Deprecated) + +- (void)setAttributedString:(NSAttributedString *)attributedString +{ + self.attributedText = attributedString; +} + +- (NSAttributedString *)attributedString +{ + return self.attributedText; +} + +- (void)setTruncationAttributedString:(NSAttributedString *)truncationAttributedString +{ + self.truncationAttributedText = truncationAttributedString; +} + +- (NSAttributedString *)truncationAttributedString +{ + return self.truncationAttributedText; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextNode2.h b/submodules/AsyncDisplayKit/Source/ASTextNode2.h new file mode 100644 index 0000000000..254ce40a7e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNode2.h @@ -0,0 +1,242 @@ +// +// ASTextNode2.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@protocol ASTextLinePositionModifier; + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract Draws interactive rich text. + @discussion Backed by the code in TextExperiment folder, on top of CoreText. + */ +#if AS_ENABLE_TEXTNODE +@interface ASTextNode2 : ASControlNode +#else +@interface ASTextNode : ASControlNode +#endif + +/** + @abstract The styled text displayed by the node. + @discussion Defaults to nil, no text is shown. + For inline image attachments, add an attribute of key NSAttachmentAttributeName, with a value of an NSTextAttachment. + */ +@property (nullable, copy) NSAttributedString *attributedText; + +#pragma mark - Truncation + +/** + @abstract The attributedText to use when the text must be truncated. + @discussion Defaults to a localized ellipsis character. + */ +@property (nullable, copy) NSAttributedString *truncationAttributedText; + +/** + @summary The second attributed string appended for truncation. + @discussion This string will be highlighted on touches. + @default nil + */ +@property (nullable, copy) NSAttributedString *additionalTruncationMessage; + +/** + @abstract Determines how the text is truncated to fit within the receiver's maximum size. + @discussion Defaults to NSLineBreakByWordWrapping. + @note Setting a truncationMode in attributedString will override the truncation mode set here. + */ +@property NSLineBreakMode truncationMode; + +/** + @abstract If the text node is truncated. Text must have been sized first. + */ +@property (readonly, getter=isTruncated) BOOL truncated; + +/** + @abstract The maximum number of lines to render of the text before truncation. + @default 0 (No limit) + */ +@property NSUInteger maximumNumberOfLines; + +/** + @abstract The number of lines in the text. Text must have been sized first. + */ +@property (readonly) NSUInteger lineCount; + +/** + * An array of path objects representing the regions where text should not be displayed. + * + * @discussion The default value of this property is an empty array. You can + * assign an array of UIBezierPath objects to exclude text from one or more regions in + * the text node's bounds. You can use this property to have text wrap around images, + * shapes or other text like a fancy magazine. + */ +@property (nullable, copy) NSArray *exclusionPaths; + +#pragma mark - Placeholders + +/** + * @abstract ASTextNode has a special placeholder behavior when placeholderEnabled is YES. + * + * @discussion Defaults to NO. When YES, it draws rectangles for each line of text, + * following the true shape of the text's wrapping. This visually mirrors the overall + * shape and weight of paragraphs, making the appearance of the finished text less jarring. + */ +@property BOOL placeholderEnabled; + +/** + @abstract The placeholder color. + */ +@property (nullable, copy) UIColor *placeholderColor; + +/** + @abstract Inset each line of the placeholder. + */ +@property UIEdgeInsets placeholderInsets; + +#pragma mark - Shadow + +/** + @abstract When you set these ASDisplayNode properties, they are composited into the bitmap instead of being applied by CA. + + @property (nonatomic) CGColorRef shadowColor; + @property (nonatomic) CGFloat shadowOpacity; + @property (nonatomic) CGSize shadowOffset; + @property (nonatomic) CGFloat shadowRadius; + */ + +/** + @abstract The number of pixels used for shadow padding on each side of the receiver. + @discussion Each inset will be less than or equal to zero, so that applying + UIEdgeInsetsRect(boundingRectForText, shadowPadding) + will return a CGRect large enough to fit both the text and the appropriate shadow padding. + */ +@property (nonatomic, readonly) UIEdgeInsets shadowPadding; + +#pragma mark - Positioning + +/** + @abstract Returns an array of rects bounding the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns an array of rects used for highlighting the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles the highlights of a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. This method is useful for visual coordination with a + highlighted range of text. + */ +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns a bounding rect for the given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion The height of the frame returned is that of the receiver's line-height; adjustment for + cap-height and descenders is not performed. This method raises an exception if textRange is not + a valid substring range of the receiver's string. + */ +- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract Returns the trailing rectangle of space in the receiver, after the final character. + @discussion Use this method to detect which portion of the receiver is not occupied by characters. + The rect returned is in the coordinate system of the receiver. + */ +- (CGRect)trailingRect AS_WARN_UNUSED_RESULT; + + +#pragma mark - Actions + +/** + @abstract The set of attribute names to consider links. Defaults to NSLinkAttributeName. + */ +@property (nonatomic, copy) NSArray *linkAttributeNames; + +/** + @abstract Indicates whether the receiver has an entity at a given point. + @param point The point, in the receiver's coordinate system. + @param attributeNameOut The name of the attribute at the point. Can be NULL. + @param rangeOut The ultimate range of the found text. Can be NULL. + @result YES if an entity exists at `point`; NO otherwise. + */ +- (nullable id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString * _Nullable * _Nullable)attributeNameOut range:(out NSRange * _Nullable)rangeOut AS_WARN_UNUSED_RESULT; + +/** + @abstract The style to use when highlighting text. + */ +@property (nonatomic) ASTextNodeHighlightStyle highlightStyle; + +/** + @abstract The range of text highlighted by the receiver. Changes to this property are not animated by default. + */ +@property (nonatomic) NSRange highlightRange; + +/** + @abstract Set the range of text to highlight, with optional animation. + + @param highlightRange The range of text to highlight. + + @param animated Whether the text should be highlighted with an animation. + */ +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated; + +/** + @abstract Responds to actions from links in the text node. + @discussion The delegate must be set before the node is loaded, and implement + textNode:longPressedLinkAttribute:value:atPoint:textRange: in order for + the long press gesture recognizer to be installed. + */ +@property (weak) id delegate; + +/** + @abstract If YES and a long press is recognized, touches are cancelled. Default is NO + */ +@property (nonatomic) BOOL longPressCancelsTouches; + +/** + @abstract if YES will not intercept touches for non-link areas of the text. Default is NO. + */ +@property (nonatomic) BOOL passthroughNonlinkTouches; + ++ (void)enableDebugging; + +#pragma mark - Layout and Sizing + +@property (nullable, nonatomic) id textContainerLinePositionModifier; + +@end + +#if AS_ENABLE_TEXTNODE +@interface ASTextNode2 (Unavailable) +#else +@interface ASTextNode (Unavailable) +#endif + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +@end + +#if (!AS_ENABLE_TEXTNODE) +// For the time beeing remap ASTextNode2 to ASTextNode +#define ASTextNode2 ASTextNode +#endif + +NS_ASSUME_NONNULL_END + + diff --git a/submodules/AsyncDisplayKit/Source/ASTextNode2.mm b/submodules/AsyncDisplayKit/Source/ASTextNode2.mm new file mode 100644 index 0000000000..7fa844ba1d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNode2.mm @@ -0,0 +1,1333 @@ +// +// ASTextNode2.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import // Definition of ASTextNodeDelegate + +#import +#import + +#import +#import +#import +#import +#import +#import + +#import +#import +#import + +#import + +#import +#import +#import +#import + +@interface ASTextCacheValue : NSObject { + @package + AS::Mutex _m; + std::deque> _layouts; +} +@end +@implementation ASTextCacheValue +@end + +/** + * If set, we will record all values set to attributedText into an array + * and once we get 2000, we'll write them all out into a plist file. + * + * This is useful for gathering realistic text data sets from apps for performance + * testing. + */ +#define AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS 0 + +/** + * If it can't find a compatible layout, this method creates one. + * + * NOTE: Be careful to copy `text` if needed. + */ +static NS_RETURNS_RETAINED ASTextLayout *ASTextNodeCompatibleLayoutWithContainerAndText(ASTextContainer *container, NSAttributedString *text) { + static dispatch_once_t onceToken; + static AS::Mutex *layoutCacheLock; + static NSCache *textLayoutCache; + dispatch_once(&onceToken, ^{ + layoutCacheLock = new AS::Mutex(); + textLayoutCache = [[NSCache alloc] init]; + }); + + layoutCacheLock->lock(); + + ASTextCacheValue *cacheValue = [textLayoutCache objectForKey:text]; + if (cacheValue == nil) { + cacheValue = [[ASTextCacheValue alloc] init]; + [textLayoutCache setObject:cacheValue forKey:[text copy]]; + } + + // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. + AS::MutexLocker lock(cacheValue->_m); + layoutCacheLock->unlock(); + + CGRect containerBounds = (CGRect){ .size = container.size }; + { + for (const auto &t : cacheValue->_layouts) { + CGSize constrainedSize = std::get<0>(t); + ASTextLayout *layout = std::get<1>(t); + + CGSize layoutSize = layout.textBoundingSize; + // 1. CoreText can return frames that are narrower than the constrained width, for obvious reasons. + // 2. CoreText can return frames that are slightly wider than the constrained width, for some reason. + // We have to trust that somehow it's OK to try and draw within our size constraint, despite the return value. + // 3. Thus, those two values (constrained width & returned width) form a range, where + // intermediate values in that range will be snapped. Thus, we can use a given layout as long as our + // width is in that range, between the min and max of those two values. + CGRect minRect = CGRectMake(0, 0, MIN(layoutSize.width, constrainedSize.width), MIN(layoutSize.height, constrainedSize.height)); + if (!CGRectContainsRect(containerBounds, minRect)) { + continue; + } + CGRect maxRect = CGRectMake(0, 0, MAX(layoutSize.width, constrainedSize.width), MAX(layoutSize.height, constrainedSize.height)); + if (!CGRectContainsRect(maxRect, containerBounds)) { + continue; + } + if (!CGSizeEqualToSize(container.size, constrainedSize)) { + continue; + } + + // Now check container params. + ASTextContainer *otherContainer = layout.container; + if (!UIEdgeInsetsEqualToEdgeInsets(container.insets, otherContainer.insets)) { + continue; + } + if (!ASObjectIsEqual(container.exclusionPaths, otherContainer.exclusionPaths)) { + continue; + } + if (container.maximumNumberOfRows != otherContainer.maximumNumberOfRows) { + continue; + } + if (container.truncationType != otherContainer.truncationType) { + continue; + } + if (!ASObjectIsEqual(container.truncationToken, otherContainer.truncationToken)) { + continue; + } + // TODO: When we get a cache hit, move this entry to the front (LRU). + return layout; + } + } + + // Cache Miss. Compute the text layout. + ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; + + // Store the result in the cache. + { + // This is a critical section. However we also must hold the lock until this point, in case + // another thread requests this cache item while a layout is being calculated, so they don't race. + cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); + if (cacheValue->_layouts.size() > 3) { + cacheValue->_layouts.pop_back(); + } + } + + return layout; +} + +static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; +static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; +static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; + +#if AS_ENABLE_TEXTNODE +@interface ASTextNode2 () +#else +@interface ASTextNode () +#endif + +@end + +#if AS_ENABLE_TEXTNODE +@implementation ASTextNode2 { +#else +@implementation ASTextNode { +#endif + ASTextContainer *_textContainer; + + CGSize _shadowOffset; + CGColorRef _shadowColor; + CGFloat _shadowOpacity; + CGFloat _shadowRadius; + + NSAttributedString *_attributedText; + NSAttributedString *_truncationAttributedText; + NSAttributedString *_additionalTruncationMessage; + NSAttributedString *_composedTruncationText; + NSArray *_pointSizeScaleFactors; + NSLineBreakMode _truncationMode; + + NSString *_highlightedLinkAttributeName; + id _highlightedLinkAttributeValue; + ASTextNodeHighlightStyle _highlightStyle; + NSRange _highlightRange; + ASHighlightOverlayLayer *_activeHighlightLayer; + UIColor *_placeholderColor; + + UILongPressGestureRecognizer *_longPressGestureRecognizer; +} +@dynamic placeholderEnabled; + +static NSArray *DefaultLinkAttributeNames() { + static NSArray *names; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + names = @[ NSLinkAttributeName ]; + }); + return names; +} + +- (instancetype)init +{ + if (self = [super init]) { + _textContainer = [[ASTextContainer alloc] init]; + // Load default values from superclass. + _shadowOffset = [super shadowOffset]; + _shadowColor = CGColorRetain([super shadowColor]); + _shadowOpacity = [super shadowOpacity]; + _shadowRadius = [super shadowRadius]; + + // Disable user interaction for text node by default. + self.userInteractionEnabled = NO; + self.needsDisplayOnBoundsChange = YES; + + _textContainer.truncationType = ASTextTruncationTypeEnd; + + // The common case is for a text node to be non-opaque and blended over some background. + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + + self.linkAttributeNames = DefaultLinkAttributeNames(); + + // Accessibility + self.isAccessibilityElement = YES; + self.accessibilityTraits = self.defaultAccessibilityTraits; + + // Placeholders + // Disabled by default in ASDisplayNode, but add a few options for those who toggle + // on the special placeholder behavior of ASTextNode. + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); + _placeholderInsets = UIEdgeInsetsMake(1.0, 0.0, 1.0, 0.0); + } + + return self; +} + +- (void)dealloc +{ + CGColorRelease(_shadowColor); + + if (_longPressGestureRecognizer) { + _longPressGestureRecognizer.delegate = nil; + [_longPressGestureRecognizer removeTarget:nil action:NULL]; + [self.view removeGestureRecognizer:_longPressGestureRecognizer]; + } +} + +#pragma mark - Description + +- (NSString *)_plainStringForDescription +{ + NSString *plainString = [[self.attributedText string] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + if (plainString.length > 50) { + plainString = [[plainString substringToIndex:50] stringByAppendingString:@"…"]; + } + return plainString; +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [super propertiesForDescription]; + NSString *plainString = [self _plainStringForDescription]; + if (plainString.length > 0) { + [result insertObject:@{ @"text" : ASStringWithQuotesIfMultiword(plainString) } atIndex:0]; + } + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [super propertiesForDebugDescription]; + NSString *plainString = [self _plainStringForDescription]; + if (plainString.length > 0) { + [result insertObject:@{ @"text" : ASStringWithQuotesIfMultiword(plainString) } atIndex:0]; + } + return result; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + // If we are view-backed and the delegate cares, support the long-press callback. + // Locking is not needed, as all instance variables used are main-thread-only. + SEL longPressCallback = @selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:); + if (!self.isLayerBacked && [self.delegate respondsToSelector:longPressCallback]) { + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_handleLongPress:)]; + _longPressGestureRecognizer.cancelsTouchesInView = self.longPressCancelsTouches; + _longPressGestureRecognizer.delegate = self; + [self.view addGestureRecognizer:_longPressGestureRecognizer]; + } +} + +- (BOOL)supportsLayerBacking +{ + if (!super.supportsLayerBacking) { + return NO; + } + + ASLockScopeSelf(); + // If the text contains any links, return NO. + NSAttributedString *attributedText = _attributedText; + NSRange range = NSMakeRange(0, attributedText.length); + for (NSString *linkAttributeName in _linkAttributeNames) { + __block BOOL hasLink = NO; + [attributedText enumerateAttribute:linkAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + hasLink = (value != nil); + *stop = YES; + }]; + if (hasLink) { + return NO; + } + } + return YES; +} + +- (NSString *)defaultAccessibilityLabel +{ + ASLockScopeSelf(); + return _attributedText.string; +} + +- (UIAccessibilityTraits)defaultAccessibilityTraits +{ + return UIAccessibilityTraitStaticText; +} + +#pragma mark - Layout and Sizing + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + ASLockScopeSelf(); + if (ASCompareAssignCustom(_textContainer.insets, textContainerInset, UIEdgeInsetsEqualToEdgeInsets)) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)textContainerInset +{ + // textContainer is invariant and has an atomic accessor. + return _textContainer.insets; +} + +- (void)setTextContainerLinePositionModifier:(id)modifier +{ + ASLockedSelfCompareAssignObjects(_textContainer.linePositionModifier, modifier); +} + +- (id)textContainerLinePositionModifier +{ + ASLockScopeSelf(); + return _textContainer.linePositionModifier; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); + ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); + + ASLockScopeSelf(); + + _textContainer.size = constrainedSize; + [self _ensureTruncationText]; + + // If the constrained size has a max/inf value on the text's forward direction, the text node is calculating its intrinsic size. + // Need to consider both width and height when determining if it is calculating instrinsic size. Even the constrained width is provided, the height can be inf + // it may provide a text that is longer than the width and require a wordWrapping line break mode and looking for the height to be calculated. + BOOL isCalculatingIntrinsicSize = (_textContainer.size.width >= ASTextContainerMaxSize.width) || (_textContainer.size.height >= ASTextContainerMaxSize.height); + + NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; + [self prepareAttributedString:mutableText isForIntrinsicSize:isCalculatingIntrinsicSize]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, mutableText); + if (layout.truncatedLine != nil && layout.truncatedLine.size.width > layout.textBoundingSize.width) { + return (CGSize) {MIN(constrainedSize.width, layout.truncatedLine.size.width), layout.textBoundingSize.height}; + } + + return layout.textBoundingSize; +} + +#pragma mark - Modifying User Text + +// Returns the ascender of the first character in attributedString by also including the line height if specified in paragraph style. ++ (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString +{ + UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL]; + NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL]; + if (!paragraphStyle) { + return font.ascender; + } + CGFloat lineHeight = MAX(font.lineHeight, paragraphStyle.minimumLineHeight); + if (paragraphStyle.maximumLineHeight > 0) { + lineHeight = MIN(lineHeight, paragraphStyle.maximumLineHeight); + } + return lineHeight + font.descender; +} + +- (NSAttributedString *)attributedText +{ + ASLockScopeSelf(); + return _attributedText; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + if (attributedText == nil) { + attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; + } + + // Many accessors in this method will acquire the lock (including ASDisplayNode methods). + // Holding it for the duration of the method is more efficient in this case. + ASLockScopeSelf(); + + if (!ASCompareAssignCopy(_attributedText, attributedText)) { + return; + } + + // Since truncation text matches style of attributedText, invalidate it now. + [self _locked_invalidateTruncationText]; + + NSUInteger length = attributedText.length; + if (length > 0) { + ASLayoutElementStyle *style = [self _locked_style]; + style.ascender = [[self class] ascenderWithAttributedString:attributedText]; + style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender]; + } + + // Tell the display node superclasses that the cached layout is incorrect now + [self setNeedsLayout]; + + // Force display to create renderer with new size and redisplay with new string + [self setNeedsDisplay]; + + // Accessiblity + self.accessibilityLabel = self.defaultAccessibilityLabel; + self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string. + +#if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS + [ASTextNode _registerAttributedText:_attributedText]; +#endif +} + +#pragma mark - Text Layout + +- (void)setExclusionPaths:(NSArray *)exclusionPaths +{ + ASLockScopeSelf(); + _textContainer.exclusionPaths = exclusionPaths; + + [self setNeedsLayout]; + [self setNeedsDisplay]; +} + +- (NSArray *)exclusionPaths +{ + ASLockScopeSelf(); + return _textContainer.exclusionPaths; +} + +- (void)prepareAttributedString:(NSMutableAttributedString *)attributedString isForIntrinsicSize:(BOOL)isForIntrinsicSize +{ + ASLockScopeSelf(); + NSLineBreakMode innerMode; + switch (_truncationMode) { + case NSLineBreakByWordWrapping: + case NSLineBreakByCharWrapping: + case NSLineBreakByClipping: + innerMode = _truncationMode; + break; + default: + innerMode = NSLineBreakByWordWrapping; + } + + // Apply/Fix paragraph style if needed + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { + + BOOL applyTruncationMode = YES; + NSMutableParagraphStyle *paragraphStyle = nil; + // Only "left" and "justified" alignments are supported while calculating intrinsic size. + // Other alignments like "right", "center" and "natural" cause the size to be bigger than needed and thus should be ignored/overridden. + const BOOL forceLeftAlignment = (style != nil + && isForIntrinsicSize + && style.alignment != NSTextAlignmentLeft + && style.alignment != NSTextAlignmentJustified); + if (style != nil) { + if (innerMode == style.lineBreakMode) { + applyTruncationMode = NO; + } + paragraphStyle = [style mutableCopy]; + } else { + if (innerMode == NSLineBreakByWordWrapping) { + applyTruncationMode = NO; + } + paragraphStyle = [NSMutableParagraphStyle new]; + } + if (!applyTruncationMode && !forceLeftAlignment) { + return; + } + paragraphStyle.lineBreakMode = innerMode; + + if (applyTruncationMode) { + paragraphStyle.lineBreakMode = _truncationMode; + } + if (forceLeftAlignment) { + paragraphStyle.alignment = NSTextAlignmentLeft; + } + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + }]; + + // Apply shadow if needed + if (_shadowOpacity > 0 && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)) && CGColorGetAlpha(_shadowColor) > 0) { + NSShadow *shadow = [[NSShadow alloc] init]; + if (_shadowOpacity != 1) { + CGColorRef shadowColorRef = CGColorCreateCopyWithAlpha(_shadowColor, _shadowOpacity * CGColorGetAlpha(_shadowColor)); + shadow.shadowColor = [UIColor colorWithCGColor:shadowColorRef]; + CGColorRelease(shadowColorRef); + } else { + shadow.shadowColor = [UIColor colorWithCGColor:_shadowColor]; + } + shadow.shadowOffset = _shadowOffset; + shadow.shadowBlurRadius = _shadowRadius; + [attributedString addAttribute:NSShadowAttributeName value:shadow range:NSMakeRange(0, attributedString.length)]; + } +} + +#pragma mark - Drawing + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + ASLockScopeSelf(); + [self _ensureTruncationText]; + + // Unlike layout, here we must copy the container since drawing is asynchronous. + ASTextContainer *copiedContainer = [_textContainer copy]; + copiedContainer.size = self.bounds.size; + [copiedContainer makeImmutable]; + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + + return @{ + @"container": copiedContainer, + @"text": mutableText, + @"bgColor": self.backgroundColor ?: [NSNull null] + }; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + ASTextContainer *container = layoutDict[@"container"]; + NSAttributedString *text = layoutDict[@"text"]; + UIColor *bgColor = layoutDict[@"bgColor"]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(container, text); + + if (isCancelledBlock()) { + return; + } + + // Fill background color. + if (bgColor == (id)[NSNull null]) { + bgColor = nil; + } + + // They may have already drawn into this context in the pre-context block + // so unfortunately we have to use the normal blend mode, not copy. + if (bgColor && CGColorGetAlpha(bgColor.CGColor) > 0) { + [bgColor setFill]; + UIRectFillUsingBlendMode(bounds, kCGBlendModeNormal); + } + + CGContextRef context = UIGraphicsGetCurrentContext(); + ASDisplayNodeAssert(context, @"This is no good without a context."); + + [layout drawInContext:context size:bounds.size point:bounds.origin view:nil layer:nil debug:[ASTextDebugOption sharedDebugOption] cancel:isCancelledBlock]; +} + +#pragma mark - Attributes + +- (id)linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut +{ + return [self _linkAttributeValueAtPoint:point + attributeName:attributeNameOut + range:rangeOut + inAdditionalTruncationMessage:NULL + forHighlighting:NO]; +} + +- (id)_linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut + inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut + forHighlighting:(BOOL)highlighting +{ + ASLockScopeSelf(); + + // TODO: The copy and application of size shouldn't be required, but it is currently. + // See discussion in https://github.com/TextureGroup/Texture/pull/396 + ASTextContainer *containerCopy = [_textContainer copy]; + containerCopy.size = self.calculatedSize; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, _attributedText); + + if ([self _locked_pointInsideAdditionalTruncationMessage:point withLayout:layout]) { + if (inAdditionalTruncationMessageOut != NULL) { + *inAdditionalTruncationMessageOut = YES; + } + return nil; + } + + NSRange visibleRange = layout.visibleRange; + NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); + + // Search the 9 points of a 44x44 square around the touch until we find a link. + // Start from center, then do sides, then do top/bottom, then do corners. + static constexpr CGSize kRectOffsets[9] = { + { 0, 0 }, + { -22, 0 }, { 22, 0 }, + { 0, -22 }, { 0, 22 }, + { -22, -22 }, { -22, 22 }, + { 22, -22 }, { 22, 22 } + }; + + for (const CGSize &offset : kRectOffsets) { + const CGPoint testPoint = CGPointMake(point.x + offset.width, + point.y + offset.height); + ASTextPosition *pos = [layout closestPositionToPoint:testPoint]; + if (!pos || !NSLocationInRange(pos.offset, clampedRange)) { + continue; + } + for (NSString *attributeName in _linkAttributeNames) { + NSRange effectiveRange = NSMakeRange(0, 0); + id value = [_attributedText attribute:attributeName atIndex:pos.offset + longestEffectiveRange:&effectiveRange inRange:clampedRange]; + if (value == nil) { + // Didn't find any links specified with this attribute. + continue; + } + + // If highlighting, check with delegate first. If not implemented, assume YES. + if (highlighting + && [_delegate respondsToSelector:@selector(textNode:shouldHighlightLinkAttribute:value:atPoint:)] + && ![_delegate textNode:(ASTextNode *)self shouldHighlightLinkAttribute:attributeName + value:value atPoint:point]) { + continue; + } + + *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); + + if (attributeNameOut != NULL) { + *attributeNameOut = attributeName; + } + + return value; + } + } + + return nil; +} + +- (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout:(ASTextLayout *)layout +{ + // Check if the range is within the additional truncation range + BOOL inAdditionalTruncationMessage = NO; + + CTLineRef truncatedCTLine = layout.truncatedLine.CTLine; + if (truncatedCTLine != NULL && _additionalTruncationMessage != nil) { + CFIndex stringIndexForPosition = CTLineGetStringIndexForPosition(truncatedCTLine, point); + if (stringIndexForPosition != kCFNotFound) { + CFIndex truncatedCTLineGlyphCount = CTLineGetGlyphCount(truncatedCTLine); + + CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_truncationAttributedText); + CFIndex truncationTokenLineGlyphCount = truncationTokenLine ? CTLineGetGlyphCount(truncationTokenLine) : 0; + CFRelease(truncationTokenLine); + + CTLineRef additionalTruncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_additionalTruncationMessage); + CFIndex additionalTruncationTokenLineGlyphCount = additionalTruncationTokenLine ? CTLineGetGlyphCount(additionalTruncationTokenLine) : 0; + CFRelease(additionalTruncationTokenLine); + + switch (_textContainer.truncationType) { + case ASTextTruncationTypeStart: { + CFIndex composedTruncationTextLineGlyphCount = truncationTokenLineGlyphCount + additionalTruncationTokenLineGlyphCount; + if (stringIndexForPosition > truncationTokenLineGlyphCount && + stringIndexForPosition < composedTruncationTextLineGlyphCount) { + inAdditionalTruncationMessage = YES; + } + break; + } + case ASTextTruncationTypeMiddle: { + CFIndex composedTruncationTextLineGlyphCount = truncationTokenLineGlyphCount + additionalTruncationTokenLineGlyphCount; + CFIndex firstTruncatedTokenIndex = (truncatedCTLineGlyphCount - composedTruncationTextLineGlyphCount) / 2.0; + if ((firstTruncatedTokenIndex + truncationTokenLineGlyphCount) < stringIndexForPosition && + stringIndexForPosition < (firstTruncatedTokenIndex + composedTruncationTextLineGlyphCount)) { + inAdditionalTruncationMessage = YES; + } + break; + } + case ASTextTruncationTypeEnd: { + if (stringIndexForPosition > (truncatedCTLineGlyphCount - additionalTruncationTokenLineGlyphCount)) { + inAdditionalTruncationMessage = YES; + } + break; + } + default: + // For now, assume that a tap inside this text, but outside the text range is a tap on the + // truncation token. + if (![layout textRangeAtPoint:point]) { + inAdditionalTruncationMessage = YES; + } + break; + } + } + } + + return inAdditionalTruncationMessage; +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + ASDisplayNodeAssertMainThread(); + ASLockScopeSelf(); // Protect usage of _highlight* ivars. + + if (gestureRecognizer == _longPressGestureRecognizer) { + // Don't allow long press on truncation message + if ([self _pendingTruncationTap]) { + return NO; + } + + // Ask our delegate if a long-press on an attribute is relevant + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) { + return [delegate textNode:(ASTextNode *)self + shouldLongPressLinkAttribute:_highlightedLinkAttributeName + value:_highlightedLinkAttributeValue + atPoint:[gestureRecognizer locationInView:self.view]]; + } + + // Otherwise we are good to go. + return YES; + } + + if (([self _pendingLinkTap] || [self _pendingTruncationTap]) + && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] + && CGRectContainsPoint(self.threadSafeBounds, [gestureRecognizer locationInView:self.view])) { + return NO; + } + + return [super gestureRecognizerShouldBegin:gestureRecognizer]; +} + +#pragma mark - Highlighting + +- (ASTextNodeHighlightStyle)highlightStyle +{ + ASLockScopeSelf(); + + return _highlightStyle; +} + +- (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle +{ + ASLockScopeSelf(); + + _highlightStyle = highlightStyle; +} + +- (NSRange)highlightRange +{ + ASLockScopeSelf(); + + return _highlightRange; +} + +- (void)setHighlightRange:(NSRange)highlightRange +{ + [self setHighlightRange:highlightRange animated:NO]; +} + +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated +{ + [self _setHighlightRange:highlightRange forAttributeName:nil value:nil animated:animated]; +} + +- (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated +{ + ASLockScopeSelf(); // Protect usage of _highlight* ivars. + + // Set these so that link tapping works. + _highlightedLinkAttributeName = highlightedAttributeName; + _highlightedLinkAttributeValue = highlightedAttributeValue; + _highlightRange = highlightRange; + + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + // Much of the code from original ASTextNode is probably usable here. + + return; +} + +- (void)_clearHighlightIfNecessary +{ + ASDisplayNodeAssertMainThread(); + + if ([self _pendingLinkTap] || [self _pendingTruncationTap]) { + [self setHighlightRange:NSMakeRange(0, 0) animated:YES]; + } +} + ++ (CGColorRef)_highlightColorForStyle:(ASTextNodeHighlightStyle)style +{ + return [UIColor colorWithWhite:(style == ASTextNodeHighlightStyleLight ? 0.0 : 1.0) alpha:1.0].CGColor; +} + ++ (CGFloat)_highlightOpacityForStyle:(ASTextNodeHighlightStyle)style +{ + return (style == ASTextNodeHighlightStyleLight) ? ASTextNodeHighlightLightOpacity : ASTextNodeHighlightDarkOpacity; +} + +#pragma mark - Text rects + +- (NSArray *)rectsForTextRange:(NSRange)textRange +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return @[]; +} + +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return @[]; +} + +- (CGRect)trailingRect +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return CGRectZero; +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return CGRectZero; +} + +#pragma mark - Placeholders + +- (UIColor *)placeholderColor +{ + return ASLockedSelf(_placeholderColor); +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) { + self.placeholderEnabled = CGColorGetAlpha(placeholderColor.CGColor) > 0; + } +} + +- (UIImage *)placeholderImage +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return nil; +} + +#pragma mark - Touch Handling + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_passthroughNonlinkTouches) { + return [super pointInside:point withEvent:event]; + } + + NSRange range = NSMakeRange(0, 0); + NSString *linkAttributeName = nil; + BOOL inAdditionalTruncationMessage = NO; + + id linkAttributeValue = [self _linkAttributeValueAtPoint:point + attributeName:&linkAttributeName + range:&range + inAdditionalTruncationMessage:&inAdditionalTruncationMessage + forHighlighting:YES]; + + NSUInteger lastCharIndex = NSIntegerMax; + BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); + + if (range.length > 0 && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { + return YES; + } else { + return NO; + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + [super touchesBegan:touches withEvent:event]; + + CGPoint point = [[touches anyObject] locationInView:self.view]; + + NSRange range = NSMakeRange(0, 0); + NSString *linkAttributeName = nil; + BOOL inAdditionalTruncationMessage = NO; + + id linkAttributeValue = [self _linkAttributeValueAtPoint:point + attributeName:&linkAttributeName + range:&range + inAdditionalTruncationMessage:&inAdditionalTruncationMessage + forHighlighting:YES]; + + NSUInteger lastCharIndex = NSIntegerMax; + BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); + + if (inAdditionalTruncationMessage) { + NSRange visibleRange = NSMakeRange(0, 0); + { + ASLockScopeSelf(); + // TODO: The copy and application of size shouldn't be required, but it is currently. + // See discussion in https://github.com/TextureGroup/Texture/pull/396 + ASTextContainer *containerCopy = [_textContainer copy]; + containerCopy.size = self.calculatedSize; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, _attributedText); + visibleRange = layout.visibleRange; + } + NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange]; + [self _setHighlightRange:truncationMessageRange forAttributeName:ASTextNodeTruncationTokenAttributeName value:nil animated:YES]; + } else if (range.length > 0 && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { + [self _setHighlightRange:range forAttributeName:linkAttributeName value:linkAttributeValue animated:YES]; + } + + return; +} + + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesCancelled:touches withEvent:event]; + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesEnded:touches withEvent:event]; + + ASLockScopeSelf(); // Protect usage of _highlight* ivars. + id delegate = self.delegate; + if ([self _pendingLinkTap] && [delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint point = [[touches anyObject] locationInView:self.view]; + [delegate textNode:(ASTextNode *)self tappedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:point textRange:_highlightRange]; + } + + if ([self _pendingTruncationTap]) { + if ([delegate respondsToSelector:@selector(textNodeTappedTruncationToken:)]) { + [delegate textNodeTappedTruncationToken:(ASTextNode *)self]; + } + } + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + [super touchesMoved:touches withEvent:event]; + + ASLockScopeSelf(); // Protect usage of _highlight* ivars. + UITouch *touch = [touches anyObject]; + CGPoint locationInView = [touch locationInView:self.view]; + // on 3D Touch enabled phones, this gets fired with changes in force, and usually will get fired immediately after touchesBegan:withEvent: + if (CGPointEqualToPoint([touch previousLocationInView:self.view], locationInView)) + return; + + // If touch has moved out of the current highlight range, clear the highlight. + if (_highlightRange.length > 0) { + NSRange range = NSMakeRange(0, 0); + [self _linkAttributeValueAtPoint:locationInView + attributeName:NULL + range:&range + inAdditionalTruncationMessage:NULL + forHighlighting:YES]; + + if (!NSEqualRanges(_highlightRange, range)) { + [self _clearHighlightIfNecessary]; + } + } +} + +- (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer +{ + ASDisplayNodeAssertMainThread(); + + // Respond to long-press when it begins, not when it ends. + if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { + ASLockScopeSelf(); // Protect usage of _highlight* ivars. + CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; + [delegate textNode:(ASTextNode *)self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; + } + } +} + +- (BOOL)_pendingLinkTap +{ + ASLockScopeSelf(); + + return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && self.delegate != nil; +} + +- (BOOL)_pendingTruncationTap +{ + return [ASLockedSelf(_highlightedLinkAttributeName) isEqualToString:ASTextNodeTruncationTokenAttributeName]; +} + +#pragma mark - Shadow Properties + +/** + * Note about shadowed text: + * + * Shadowed text is pretty rare, and we are a framework that targets serious developers. + * We should probably ignore these properties and tell developers to set the shadow into their attributed text instead. + */ +- (CGColorRef)shadowColor +{ + return ASLockedSelf(_shadowColor); +} + +- (void)setShadowColor:(CGColorRef)shadowColor +{ + ASLockScopeSelf(); + if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { + CGColorRelease(_shadowColor); + _shadowColor = CGColorRetain(shadowColor); + [self setNeedsDisplay]; + } +} + +- (CGSize)shadowOffset +{ + return ASLockedSelf(_shadowOffset); +} + +- (void)setShadowOffset:(CGSize)shadowOffset +{ + ASLockScopeSelf(); + if (ASCompareAssignCustom(_shadowOffset, shadowOffset, CGSizeEqualToSize)) { + [self setNeedsDisplay]; + } +} + +- (CGFloat)shadowOpacity +{ + return ASLockedSelf(_shadowOpacity); +} + +- (void)setShadowOpacity:(CGFloat)shadowOpacity +{ + ASLockScopeSelf(); + if (ASCompareAssign(_shadowOpacity, shadowOpacity)) { + [self setNeedsDisplay]; + } +} + +- (CGFloat)shadowRadius +{ + return ASLockedSelf(_shadowRadius); +} + +- (void)setShadowRadius:(CGFloat)shadowRadius +{ + ASLockScopeSelf(); + if (ASCompareAssign(_shadowRadius, shadowRadius)) { + [self setNeedsDisplay]; + } +} + +- (UIEdgeInsets)shadowPadding +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return UIEdgeInsetsZero; +} + +- (void)setPointSizeScaleFactors:(NSArray *)scaleFactors +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + ASLockScopeSelf(); + if (ASCompareAssignCopy(_pointSizeScaleFactors, scaleFactors)) { + [self setNeedsLayout]; + } +} + +- (NSArray *)pointSizeScaleFactors +{ + return ASLockedSelf(_pointSizeScaleFactors); +} + +#pragma mark - Truncation Message + +static NSAttributedString *DefaultTruncationAttributedString() +{ + static NSAttributedString *defaultTruncationAttributedString; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultTruncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; + }); + return defaultTruncationAttributedString; +} + +- (void)_ensureTruncationText +{ + ASLockScopeSelf(); + if (_textContainer.truncationToken == nil) { + _textContainer.truncationToken = [self _locked_composedTruncationText]; + } +} + +- (NSAttributedString *)truncationAttributedText +{ + return ASLockedSelf(_truncationAttributedText); +} + +- (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) { + [self _invalidateTruncationText]; + } +} + +- (NSAttributedString *)additionalTruncationMessage +{ + return ASLockedSelf(_additionalTruncationMessage); +} + +- (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) { + [self _invalidateTruncationText]; + } +} + +- (NSLineBreakMode)truncationMode +{ + return ASLockedSelf(_truncationMode); +} + +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + ASLockScopeSelf(); + if (ASCompareAssign(_truncationMode, truncationMode)) { + ASTextTruncationType truncationType; + switch (truncationMode) { + case NSLineBreakByTruncatingHead: + truncationType = ASTextTruncationTypeStart; + break; + case NSLineBreakByTruncatingTail: + truncationType = ASTextTruncationTypeEnd; + break; + case NSLineBreakByTruncatingMiddle: + truncationType = ASTextTruncationTypeMiddle; + break; + default: + truncationType = ASTextTruncationTypeNone; + } + + _textContainer.truncationType = truncationType; + + [self setNeedsDisplay]; + } +} + +- (BOOL)isTruncated +{ + return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine != nil); +} + +- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize +{ + return ASLockedSelf([self locked_textLayoutForSize:constrainedSize.max].truncatedLine != nil); +} + +- (ASTextLayout *)locked_textLayoutForSize:(CGSize)size +{ + ASTextContainer *container = [_textContainer copy]; + container.size = size; + return ASTextNodeCompatibleLayoutWithContainerAndText(container, _attributedText); +} + +- (NSUInteger)maximumNumberOfLines +{ + // _textContainer is invariant and this is just atomic access. + return _textContainer.maximumNumberOfRows; +} + +- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines +{ + ASLockScopeSelf(); + if (ASCompareAssign(_textContainer.maximumNumberOfRows, maximumNumberOfLines)) { + [self setNeedsDisplay]; + } +} + +- (NSUInteger)lineCount +{ + ASLockScopeSelf(); + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return 0; +} + +#pragma mark - Truncation Message + +- (void)_invalidateTruncationText +{ + ASLockScopeSelf(); + [self _locked_invalidateTruncationText]; + [self setNeedsDisplay]; +} + +- (void)_locked_invalidateTruncationText +{ + _textContainer.truncationToken = nil; +} + +/** + * @return the additional truncation message range within the as-rendered text. + * Must be called from main thread + */ +- (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange +{ + ASLockScopeSelf(); + + // Check if we even have an additional truncation message. + if (!_additionalTruncationMessage) { + return NSMakeRange(NSNotFound, 0); + } + + // Character location of the unicode ellipsis (the first index after the visible range) + NSInteger truncationTokenIndex = NSMaxRange(visibleRange); + + NSUInteger additionalTruncationMessageLength = _additionalTruncationMessage.length; + // We get the location of the truncation token, then add the length of the + // truncation attributed string +1 for the space between. + return NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength); +} + +/** + * @return the truncation message for the string. If there are both an + * additional truncation message and a truncation attributed string, they will + * be properly composed. + */ +- (NSAttributedString *)_locked_composedTruncationText +{ + ASAssertLocked(__instanceLock__); + if (_composedTruncationText == nil) { + if (_truncationAttributedText != nil && _additionalTruncationMessage != nil) { + NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedText]; + [newComposedTruncationString.mutableString appendString:@" "]; + [newComposedTruncationString appendAttributedString:_additionalTruncationMessage]; + _composedTruncationText = newComposedTruncationString; + } else if (_truncationAttributedText != nil) { + _composedTruncationText = _truncationAttributedText; + } else if (_additionalTruncationMessage != nil) { + _composedTruncationText = _additionalTruncationMessage; + } else { + _composedTruncationText = DefaultTruncationAttributedString(); + } + _composedTruncationText = [self _locked_prepareTruncationStringForDrawing:_composedTruncationText]; + } + return _composedTruncationText; +} + +/** + * - cleanses it of core text attributes so TextKit doesn't crash + * - Adds whole-string attributes so the truncation message matches the styling + * of the body text + */ +- (NSAttributedString *)_locked_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString +{ + ASAssertLocked(__instanceLock__); + NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy]; + // Grab the attributes from the full string + if (_attributedText.length > 0) { + NSAttributedString *originalString = _attributedText; + NSInteger originalStringLength = _attributedText.length; + // Add any of the original string's attributes to the truncation string, + // but don't overwrite any of the truncation string's attributes + NSDictionary *originalStringAttributes = [originalString attributesAtIndex:originalStringLength-1 effectiveRange:NULL]; + [truncationString enumerateAttributesInRange:NSMakeRange(0, truncationString.length) options:0 usingBlock: + ^(NSDictionary *attributes, NSRange range, BOOL *stop) { + NSMutableDictionary *futureTruncationAttributes = [originalStringAttributes mutableCopy]; + [futureTruncationAttributes addEntriesFromDictionary:attributes]; + [truncationMutableString setAttributes:futureTruncationAttributes range:range]; + }]; + } + return truncationMutableString; +} + +#if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS ++ (void)_registerAttributedText:(NSAttributedString *)str +{ + static NSMutableArray *array; + static NSLock *lock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lock = [NSLock new]; + array = [NSMutableArray new]; + }); + [lock lock]; + [array addObject:str]; + if (array.count % 20 == 0) { + NSLog(@"Got %d strings", (int)array.count); + } + if (array.count == 2000) { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AttributedStrings.plist"]; + NSAssert([NSKeyedArchiver archiveRootObject:array toFile:path], nil); + NSLog(@"Saved to %@", path); + } + [lock unlock]; +} +#endif + ++ (void)enableDebugging +{ + ASTextDebugOption *debugOption = [[ASTextDebugOption alloc] init]; + debugOption.CTLineFillColor = [UIColor colorWithRed:0 green:0.3 blue:1 alpha:0.1]; + [ASTextDebugOption setSharedDebugOption:debugOption]; +} + +- (BOOL)usingExperiment +{ + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h b/submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h new file mode 100644 index 0000000000..ab4e134bfd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h @@ -0,0 +1,91 @@ +// +// ASTextNodeCommon.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@class ASTextNode; + +#define AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE() { \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + NSLog(@"[Texture] Warning: Feature %@ is unimplemented in %@.", NSStringFromSelector(_cmd), NSStringFromClass(self.class)); \ + });\ +} + +/** + * Highlight styles. + */ +typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { + /** + * Highlight style for text on a light background. + */ + ASTextNodeHighlightStyleLight, + + /** + * Highlight style for text on a dark background. + */ + ASTextNodeHighlightStyleDark +}; + +/** + * @abstract Text node delegate. + */ +@protocol ASTextNodeDelegate +@optional + +/** + @abstract Indicates to the delegate that a link was tapped within a text node. + @param textNode The ASTextNode containing the link that was tapped. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was tapped. + @param textRange The range of highlighted text. + */ +- (void)textNode:(ASTextNode *)textNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; + +/** + @abstract Indicates to the delegate that a link was tapped within a text node. + @param textNode The ASTextNode containing the link that was tapped. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was tapped. + @param textRange The range of highlighted text. + @discussion In addition to implementing this method, the delegate must be set on the text + node before it is loaded (the recognizer is created in -didLoad) + */ +- (void)textNode:(ASTextNode *)textNode longPressedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; + +//! @abstract Called when the text node's truncation string has been tapped. +- (void)textNodeTappedTruncationToken:(ASTextNode *)textNode; + +/** + @abstract Indicates to the text node if an attribute should be considered a link. + @param textNode The text node containing the entity attribute. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was touched to trigger a highlight. + @discussion If not implemented, the default value is YES. + @return YES if the entity attribute should be a link, NO otherwise. + */ +- (BOOL)textNode:(ASTextNode *)textNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point; + +/** + @abstract Indicates to the text node if an attribute is a valid long-press target + @param textNode The text node containing the entity attribute. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was long-pressed. + @discussion If not implemented, the default value is NO. + @return YES if the entity attribute should be treated as a long-press target, NO otherwise. + */ +- (BOOL)textNode:(ASTextNode *)textNode shouldLongPressLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point; + +@end + diff --git a/submodules/AsyncDisplayKit/Source/ASVideoNode.h b/submodules/AsyncDisplayKit/Source/ASVideoNode.h new file mode 100644 index 0000000000..5c38a6f641 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASVideoNode.h @@ -0,0 +1,173 @@ +// +// ASVideoNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +#if AS_USE_VIDEO + +@class AVAsset, AVPlayer, AVPlayerLayer, AVPlayerItem, AVVideoComposition, AVAudioMix; +@protocol ASVideoNodeDelegate; + +typedef NS_ENUM(NSInteger, ASVideoNodePlayerState) { + ASVideoNodePlayerStateUnknown, + ASVideoNodePlayerStateInitialLoading, + ASVideoNodePlayerStateReadyToPlay, + ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying, + ASVideoNodePlayerStatePlaying, + ASVideoNodePlayerStateLoading, + ASVideoNodePlayerStatePaused, + ASVideoNodePlayerStateFinished +}; + +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 +// there is room for further expansion and optimization. Please report any issues or requests +// in an issue on GitHub: https://github.com/facebook/AsyncDisplayKit/issues + +@interface ASVideoNode : ASNetworkImageNode + +- (void)play; +- (void)pause; +- (BOOL)isPlaying; +- (void)resetToPlaceholder; + +// TODO: copy +@property (nullable) AVAsset *asset; + +/** + ** @abstract The URL with which the asset was initialized. + ** @discussion Setting the URL will override the current asset with a newly created AVURLAsset created from the given URL, and AVAsset *asset will point to that newly created AVURLAsset. Please don't set both assetURL and asset. + ** @return Current URL the asset was initialized or nil if no URL was given. + **/ +@property (nullable, copy) NSURL *assetURL; + +// TODO: copy both of these. +@property (nullable) AVVideoComposition *videoComposition; +@property (nullable) AVAudioMix *audioMix; + +@property (nullable, readonly) AVPlayer *player; + +// TODO: copy +@property (nullable, readonly) AVPlayerItem *currentItem; + +@property (nullable, nonatomic, readonly) AVPlayerLayer *playerLayer; + + +/** + * 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 BOOL shouldAutoplay; +@property BOOL shouldAutorepeat; + +@property BOOL muted; +@property BOOL shouldAggressivelyRecoverFromStall; + +@property (readonly) ASVideoNodePlayerState playerState; +//! Defaults to 1000 +@property int32_t periodicTimeObserverTimescale; + +//! Defaults to AVLayerVideoGravityResizeAspect +@property (null_resettable, copy) NSString *gravity; + +@property (nullable, weak) id delegate; + +@end + +@protocol ASVideoNodeDelegate +@optional +/** + * @abstract Delegate method invoked when the node's video has played to its end time. + * @param videoNode The video node has played to its end time. + */ +- (void)videoDidPlayToEnd:(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)didTapVideoNode:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked when player changes state. + * @param videoNode The video node. + * @param state player state before this change. + * @param toState player new state. + * @discussion This method is called after each state change + */ +- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState; +/** + * @abstract Ssks delegate if state change is allowed + * ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused. + * asks delegate if state change is allowed. + * @param videoNode The video node. + * @param state player state that is going to be set. + * @discussion Delegate method invoked when player changes it's state to + * ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused + * and asks delegate if state change is valid + */ +- (BOOL)videoNode:(ASVideoNode*)videoNode shouldChangePlayerStateTo:(ASVideoNodePlayerState)state; +/** + * @abstract Delegate method invoked when player playback time is updated. + * @param videoNode The video node. + * @param timeInterval current playback time in seconds. + */ +- (void)videoNode:(ASVideoNode *)videoNode didPlayToTimeInterval:(NSTimeInterval)timeInterval; +/** + * @abstract Delegate method invoked when the video player stalls. + * @param videoNode The video node that has experienced the stall + * @param timeInterval Current playback time when the stall happens + */ +- (void)videoNode:(ASVideoNode *)videoNode didStallAtTimeInterval:(NSTimeInterval)timeInterval; +/** + * @abstract Delegate method invoked when the video player starts the inital asset loading + * @param videoNode The videoNode + */ +- (void)videoNodeDidStartInitialLoading:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked when the video is done loading the asset and can start the playback + * @param videoNode The videoNode + */ +- (void)videoNodeDidFinishInitialLoading:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked when the AVPlayerItem for the asset has been set up and can be accessed throught currentItem. + * @param videoNode The videoNode. + * @param currentItem The AVPlayerItem that was constructed from the asset. + */ +- (void)videoNode:(ASVideoNode *)videoNode didSetCurrentItem:(AVPlayerItem *)currentItem; +/** + * @abstract Delegate method invoked when the video node has recovered from the stall + * @param videoNode The videoNode + */ +- (void)videoNodeDidRecoverFromStall:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked when an error occurs while trying to load an asset + * @param videoNode The videoNode. + * @param key The key of value that failed to load. + * @param asset The asset. + * @param error The error that occurs. + */ +- (void)videoNode:(ASVideoNode *)videoNode didFailToLoadValueForKey:(NSString *)key asset:(AVAsset *)asset error:(NSError *)error; + +@end + +@interface ASVideoNode (Unavailable) + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASVideoNode.mm b/submodules/AsyncDisplayKit/Source/ASVideoNode.mm new file mode 100644 index 0000000000..05478d91d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASVideoNode.mm @@ -0,0 +1,864 @@ +// +// ASVideoNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK + +#import + +#if AS_USE_VIDEO + +#import +#import +#import +#import +#import +#import +#import +#import + +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 kplaybackBufferEmpty = @"playbackBufferEmpty"; +static NSString * const kStatus = @"status"; +static NSString * const kRate = @"rate"; + +@interface ASVideoNode () +{ + struct { + unsigned int delegateVideNodeShouldChangePlayerStateTo:1; + unsigned int delegateVideoDidPlayToEnd:1; + unsigned int delegateDidTapVideoNode:1; + unsigned int delegateVideoNodeWillChangePlayerStateToState:1; + unsigned int delegateVideoNodeDidPlayToTimeInterval:1; + unsigned int delegateVideoNodeDidStartInitialLoading:1; + unsigned int delegateVideoNodeDidFinishInitialLoading:1; + unsigned int delegateVideoNodeDidSetCurrentItem:1; + unsigned int delegateVideoNodeDidStallAtTimeInterval:1; + unsigned int delegateVideoNodeDidRecoverFromStall:1; + unsigned int delegateVideoNodeDidFailToLoadValueForKey:1; + } _delegateFlags; + + BOOL _shouldBePlaying; + + BOOL _shouldAutorepeat; + BOOL _shouldAutoplay; + BOOL _shouldAggressivelyRecoverFromStall; + BOOL _muted; + + ASVideoNodePlayerState _playerState; + + AVAsset *_asset; + NSURL *_assetURL; + AVVideoComposition *_videoComposition; + AVAudioMix *_audioMix; + + AVPlayerItem *_currentPlayerItem; + AVPlayer *_player; + + id _timeObserver; + int32_t _periodicTimeObserverTimescale; + CMTime _timeObserverInterval; + + CMTime _lastPlaybackTime; + + ASDisplayNode *_playerNode; + NSString *_gravity; +} + +@end + +@implementation ASVideoNode + +@dynamic delegate; + +// TODO: Support preview images with HTTP Live Streaming videos. + +#pragma mark - Construction and Layout + +- (instancetype)initWithCache:(id)cache downloader:(id)downloader +{ + if (!(self = [super initWithCache:cache downloader:downloader])) { + return nil; + } + + self.gravity = AVLayerVideoGravityResizeAspect; + _periodicTimeObserverTimescale = 10000; + [self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; + _lastPlaybackTime = kCMTimeZero; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; + + return self; +} + +- (ASDisplayNode *)constructPlayerNode +{ + ASVideoNode * __weak weakSelf = self; + + return [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ + AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; + playerLayer.player = weakSelf.player; + playerLayer.videoGravity = weakSelf.gravity; + return playerLayer; + }]; +} + +- (AVPlayerItem *)constructPlayerItem +{ + ASDisplayNodeAssertMainThread(); + ASLockScopeSelf(); + + AVPlayerItem *playerItem = nil; + if (_assetURL != nil) { + playerItem = [[AVPlayerItem alloc] initWithURL:_assetURL]; + _asset = [playerItem asset]; + } else { + playerItem = [[AVPlayerItem alloc] initWithAsset:_asset]; + } + + playerItem.videoComposition = _videoComposition; + playerItem.audioMix = _audioMix; + return playerItem; +} + +- (void)prepareToPlayAsset:(AVAsset *)asset withKeys:(NSArray *)requestedKeys +{ + ASDisplayNodeAssertMainThread(); + + for (NSString *key in requestedKeys) { + NSError *error = nil; + AVKeyValueStatus keyStatus = [asset statusOfValueForKey:key error:&error]; + if (keyStatus == AVKeyValueStatusFailed) { + NSLog(@"Asset loading failed with error: %@", error); + if (_delegateFlags.delegateVideoNodeDidFailToLoadValueForKey) { + [self.delegate videoNode:self didFailToLoadValueForKey:key asset:asset error:error]; + } + } + } + + if ([asset isPlayable] == NO) { + NSLog(@"Asset is not playable."); + return; + } + + AVPlayerItem *playerItem = [self constructPlayerItem]; + [self setCurrentItem:playerItem]; + + if (_player != nil) { + [_player replaceCurrentItemWithPlayerItem:playerItem]; + } else { + self.player = [AVPlayer playerWithPlayerItem:playerItem]; + } + + if (_delegateFlags.delegateVideoNodeDidSetCurrentItem) { + [self.delegate videoNode:self didSetCurrentItem:playerItem]; + } + + if (self.image == nil && self.URL == nil) { + [self generatePlaceholderImage]; + } +} + +- (void)addPlayerItemObservers:(AVPlayerItem *)playerItem +{ + if (playerItem == nil) { + return; + } + + [playerItem addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + [playerItem addObserver:self forKeyPath:kPlaybackLikelyToKeepUpKey options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + [playerItem addObserver:self forKeyPath:kplaybackBufferEmpty options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + [notificationCenter addObserver:self selector:@selector(videoNodeDidStall:) name:AVPlayerItemPlaybackStalledNotification object:playerItem]; + [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; + [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; +} + +- (void)removePlayerItemObservers:(AVPlayerItem *)playerItem +{ + @try { + [playerItem removeObserver:self forKeyPath:kStatus context:ASVideoNodeContext]; + [playerItem removeObserver:self forKeyPath:kPlaybackLikelyToKeepUpKey context:ASVideoNodeContext]; + [playerItem removeObserver:self forKeyPath:kplaybackBufferEmpty 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: AVPlayerItemPlaybackStalledNotification object:playerItem]; + [notificationCenter removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; + [notificationCenter removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; +} + +- (void)addPlayerObservers:(AVPlayer *)player +{ + if (player == nil) { + return; + } + + [player addObserver:self forKeyPath:kRate options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + + __weak __typeof(self) weakSelf = self; + _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); + _timeObserver = [player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ + [weakSelf periodicTimeObserver:time]; + }]; +} + +- (void) removePlayerObservers:(AVPlayer *)player +{ + if (_timeObserver != nil) { + [player removeTimeObserver:_timeObserver]; + _timeObserver = nil; + } + + @try { + [player removeObserver:self forKeyPath:kRate context:ASVideoNodeContext]; + } + @catch (NSException * __unused exception) { + NSLog(@"Unnecessary KVO removal"); + } +} + +- (void)layout +{ + [super layout]; + // The _playerNode wraps AVPlayerLayer, and therefore should extend across the entire bounds. + _playerNode.frame = self.bounds; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNode *playerNode = ASLockedSelf(_playerNode); + + CGSize calculatedSize = constrainedSize; + + // Prevent crashes through if infinite width or height + if (isinf(calculatedSize.width) || isinf(calculatedSize.height)) { + ASDisplayNodeAssert(NO, @"Infinite width or height in ASVideoNode"); + calculatedSize = CGSizeZero; + } + + if (playerNode != nil) { + playerNode.style.preferredSize = calculatedSize; + [playerNode layoutThatFits:ASSizeRangeMake(CGSizeZero, calculatedSize)]; + } + + return calculatedSize; +} + +- (void)generatePlaceholderImage +{ + ASVideoNode * __weak weakSelf = self; + AVAsset *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(^{ + AVAsset *asset = self.asset; + + // Skip the asset image generation if we don't have any tracks available that are capable of supporting it + NSArray* visualAssetArray = [asset tracksWithMediaCharacteristic:AVMediaCharacteristicVisual]; + if (visualAssetArray.count == 0) { + completionHandler(nil); + return; + } + + AVAssetImageGenerator *previewImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; + previewImageGenerator.appliesPreferredTrackTransform = YES; + previewImageGenerator.videoComposition = _videoComposition; + + [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)setVideoPlaceholderImage:(UIImage *)image +{ + NSString *gravity = self.gravity; + + if (image != nil) { + self.contentMode = ASContentModeFromVideoGravity(gravity); + } + self.image = image; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + AS::UniqueLock l(__instanceLock__); + + if (object == _currentPlayerItem) { + if ([keyPath isEqualToString:kStatus]) { + if ([change[NSKeyValueChangeNewKey] integerValue] == AVPlayerItemStatusReadyToPlay) { + if (self.playerState != ASVideoNodePlayerStatePlaying) { + self.playerState = ASVideoNodePlayerStateReadyToPlay; + if (_shouldBePlaying && ASInterfaceStateIncludesVisible(self.interfaceState)) { + l.unlock(); + [self play]; + l.lock(); + } + } + // If we don't yet have a placeholder image update it now that we should have data available for it + if (self.image == nil && self.URL == nil) { + [self generatePlaceholderImage]; + } + } + } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUpKey]) { + BOOL likelyToKeepUp = [change[NSKeyValueChangeNewKey] boolValue]; + if (likelyToKeepUp && self.playerState == ASVideoNodePlayerStatePlaying) { + return; + } + if (!likelyToKeepUp) { + self.playerState = ASVideoNodePlayerStateLoading; + } else if (self.playerState != ASVideoNodePlayerStateFinished) { + self.playerState = ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying; + } + if (_shouldBePlaying && (_shouldAggressivelyRecoverFromStall || likelyToKeepUp) && ASInterfaceStateIncludesVisible(self.interfaceState)) { + if (self.playerState == ASVideoNodePlayerStateLoading && _delegateFlags.delegateVideoNodeDidRecoverFromStall) { + [self.delegate videoNodeDidRecoverFromStall:self]; + } + + l.unlock(); + [self play]; // autoresume after buffer catches up + // NOTE: Early return without re-locking. + return; + } + } else if ([keyPath isEqualToString:kplaybackBufferEmpty]) { + if (_shouldBePlaying && [change[NSKeyValueChangeNewKey] boolValue] == YES && ASInterfaceStateIncludesVisible(self.interfaceState)) { + self.playerState = ASVideoNodePlayerStateLoading; + } + } + } else if (object == _player) { + if ([keyPath isEqualToString:kRate]) { + if ([change[NSKeyValueChangeNewKey] floatValue] == 0.0) { + if (self.playerState == ASVideoNodePlayerStatePlaying) { + self.playerState = ASVideoNodePlayerStatePaused; + } + } else { + self.playerState = ASVideoNodePlayerStatePlaying; + } + } + } + + // NOTE: Early return above. +} + +- (void)tapped +{ + if (_delegateFlags.delegateDidTapVideoNode) { + [self.delegate didTapVideoNode:self]; + + } else { + if (_shouldBePlaying) { + [self pause]; + } else { + [self play]; + } + } +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + + ASLockScopeSelf(); + AVAsset *asset = self.asset; + // Return immediately if the asset is nil; + if (asset == nil || self.playerState != ASVideoNodePlayerStateUnknown) { + return; + } + + self.playerState = ASVideoNodePlayerStateLoading; + if (_delegateFlags.delegateVideoNodeDidStartInitialLoading) { + [self.delegate videoNodeDidStartInitialLoading:self]; + } + + NSArray *requestedKeys = @[@"playable"]; + [asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^{ + ASPerformBlockOnMainThread(^{ + if (_delegateFlags.delegateVideoNodeDidFinishInitialLoading) { + [self.delegate videoNodeDidFinishInitialLoading:self]; + } + [self prepareToPlayAsset:asset withKeys:requestedKeys]; + }); + }]; +} + +- (void)periodicTimeObserver:(CMTime)time +{ + NSTimeInterval timeInSeconds = CMTimeGetSeconds(time); + if (timeInSeconds <= 0) { + return; + } + + if (_delegateFlags.delegateVideoNodeDidPlayToTimeInterval) { + [self.delegate videoNode:self didPlayToTimeInterval:timeInSeconds]; + + } +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + + { + ASLockScopeSelf(); + + self.player = nil; + self.currentItem = nil; + self.playerState = ASVideoNodePlayerStateUnknown; + } +} + +- (void)didEnterVisibleState +{ + [super didEnterVisibleState]; + + BOOL shouldPlay = NO; + { + ASLockScopeSelf(); + if (_shouldBePlaying || _shouldAutoplay) { + if (_player != nil && CMTIME_IS_VALID(_lastPlaybackTime)) { + [_player seekToTime:_lastPlaybackTime]; + } + shouldPlay = YES; + } + } + + if (shouldPlay) { + [self play]; + } +} + +- (void)didExitVisibleState +{ + [super didExitVisibleState]; + + ASLockScopeSelf(); + + if (_shouldBePlaying) { + [self pause]; + if (_player != nil && CMTIME_IS_VALID(_player.currentTime)) { + _lastPlaybackTime = _player.currentTime; + } + _shouldBePlaying = YES; + } +} + +#pragma mark - Video Properties + +- (void)setPlayerState:(ASVideoNodePlayerState)playerState +{ + ASLockScopeSelf(); + + ASVideoNodePlayerState oldState = _playerState; + + if (oldState == playerState) { + return; + } + + if (_delegateFlags.delegateVideoNodeWillChangePlayerStateToState) { + [self.delegate videoNode:self willChangePlayerState:oldState toState:playerState]; + } + + _playerState = playerState; +} + +- (void)setAssetURL:(NSURL *)assetURL +{ + ASDisplayNodeAssertMainThread(); + + if (ASObjectIsEqual(assetURL, self.assetURL) == NO) { + [self setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL]; + } +} + +- (NSURL *)assetURL +{ + ASLockScopeSelf(); + + if (_assetURL != nil) { + return _assetURL; + } else if ([_asset isKindOfClass:AVURLAsset.class]) { + return ((AVURLAsset *)_asset).URL; + } + + return nil; +} + +- (void)setAsset:(AVAsset *)asset +{ + ASDisplayNodeAssertMainThread(); + + if (ASAssetIsEqual(asset, self.asset) == NO) { + [self setAndFetchAsset:asset url:nil]; + } +} + +- (AVAsset *)asset +{ + ASLockScopeSelf(); + return _asset; +} + +- (void)setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL +{ + ASDisplayNodeAssertMainThread(); + + [self didExitPreloadState]; + + { + ASLockScopeSelf(); + self.videoPlaceholderImage = nil; + _asset = asset; + _assetURL = assetURL; + } + + [self setNeedsPreload]; +} + +- (void)setVideoComposition:(AVVideoComposition *)videoComposition +{ + ASLockScopeSelf(); + + _videoComposition = videoComposition; + _currentPlayerItem.videoComposition = videoComposition; +} + +- (AVVideoComposition *)videoComposition +{ + ASLockScopeSelf(); + return _videoComposition; +} + +- (void)setAudioMix:(AVAudioMix *)audioMix +{ + ASLockScopeSelf(); + + _audioMix = audioMix; + _currentPlayerItem.audioMix = audioMix; +} + +- (AVAudioMix *)audioMix +{ + ASLockScopeSelf(); + return _audioMix; +} + +- (AVPlayer *)player +{ + ASLockScopeSelf(); + return _player; +} + +- (AVPlayerLayer *)playerLayer +{ + ASLockScopeSelf(); + return (AVPlayerLayer *)_playerNode.layer; +} + +- (void)setDelegate:(id)delegate +{ + [super setDelegate:delegate]; + + if (delegate == nil) { + memset(&_delegateFlags, 0, sizeof(_delegateFlags)); + } else { + _delegateFlags.delegateVideNodeShouldChangePlayerStateTo = [delegate respondsToSelector:@selector(videoNode:shouldChangePlayerStateTo:)]; + _delegateFlags.delegateVideoDidPlayToEnd = [delegate respondsToSelector:@selector(videoDidPlayToEnd:)]; + _delegateFlags.delegateDidTapVideoNode = [delegate respondsToSelector:@selector(didTapVideoNode:)]; + _delegateFlags.delegateVideoNodeWillChangePlayerStateToState = [delegate respondsToSelector:@selector(videoNode:willChangePlayerState:toState:)]; + _delegateFlags.delegateVideoNodeDidPlayToTimeInterval = [delegate respondsToSelector:@selector(videoNode:didPlayToTimeInterval:)]; + _delegateFlags.delegateVideoNodeDidStartInitialLoading = [delegate respondsToSelector:@selector(videoNodeDidStartInitialLoading:)]; + _delegateFlags.delegateVideoNodeDidFinishInitialLoading = [delegate respondsToSelector:@selector(videoNodeDidFinishInitialLoading:)]; + _delegateFlags.delegateVideoNodeDidSetCurrentItem = [delegate respondsToSelector:@selector(videoNode:didSetCurrentItem:)]; + _delegateFlags.delegateVideoNodeDidStallAtTimeInterval = [delegate respondsToSelector:@selector(videoNode:didStallAtTimeInterval:)]; + _delegateFlags.delegateVideoNodeDidRecoverFromStall = [delegate respondsToSelector:@selector(videoNodeDidRecoverFromStall:)]; + _delegateFlags.delegateVideoNodeDidFailToLoadValueForKey = [delegate respondsToSelector:@selector(videoNode:didFailToLoadValueForKey:asset:error:)]; + } +} + +- (void)setGravity:(NSString *)gravity +{ + ASLockScopeSelf(); + if (!gravity) { + gravity = AVLayerVideoGravityResizeAspect; + } + + if (!ASCompareAssignObjects(_gravity, gravity)) { + return; + } + + if (_playerNode.isNodeLoaded) { + ((AVPlayerLayer *)_playerNode.layer).videoGravity = gravity; + } + self.contentMode = ASContentModeFromVideoGravity(gravity); + _gravity = gravity; +} + +- (NSString *)gravity +{ + ASLockScopeSelf(); + return _gravity; +} + +- (BOOL)muted +{ + ASLockScopeSelf(); + return _muted; +} + +- (void)setMuted:(BOOL)muted +{ + ASLockScopeSelf(); + + _player.muted = muted; + _muted = muted; +} + +#pragma mark - Video Playback + +- (void)play +{ + ASLockScopeSelf(); + + if (![self isStateChangeValid:ASVideoNodePlayerStatePlaying]) { + return; + } + + if (_player == nil) { + ASUnlockScope(self); + [self setNeedsPreload]; + } + + if (_playerNode == nil) { + _playerNode = [self constructPlayerNode]; + + { + ASUnlockScope(self); + [self addSubnode:_playerNode]; + } + + [self setNeedsLayout]; + } + + + [_player play]; + _shouldBePlaying = YES; +} + +- (BOOL)ready +{ + return _currentPlayerItem.status == AVPlayerItemStatusReadyToPlay; +} + +- (void)pause +{ + ASLockScopeSelf(); + if (![self isStateChangeValid:ASVideoNodePlayerStatePaused]) { + return; + } + [_player pause]; + _shouldBePlaying = NO; +} + +- (BOOL)isPlaying +{ + ASLockScopeSelf(); + + return (_player.rate > 0 && !_player.error); +} + +- (BOOL)isStateChangeValid:(ASVideoNodePlayerState)state +{ + if (_delegateFlags.delegateVideNodeShouldChangePlayerStateTo) { + if (![self.delegate videoNode:self shouldChangePlayerStateTo:state]) { + return NO; + } + } + return YES; +} + +- (void)resetToPlaceholder +{ + ASLockScopeSelf(); + + if (_playerNode != nil) { + [_playerNode removeFromSupernode]; + _playerNode = nil; + } + + [_player seekToTime:kCMTimeZero]; + [self pause]; +} + + +#pragma mark - Playback observers + +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + if (self.shouldBePlaying && self.isVisible) { + [self play]; + } +} + +- (void)didPlayToEnd:(NSNotification *)notification +{ + self.playerState = ASVideoNodePlayerStateFinished; + if (_delegateFlags.delegateVideoDidPlayToEnd) { + [self.delegate videoDidPlayToEnd:self]; + } + + if (_shouldAutorepeat) { + [_player seekToTime:kCMTimeZero]; + [self play]; + } else { + [self pause]; + } +} + +- (void)videoNodeDidStall:(NSNotification *)notification +{ + self.playerState = ASVideoNodePlayerStateLoading; + if (_delegateFlags.delegateVideoNodeDidStallAtTimeInterval) { + [self.delegate videoNode:self didStallAtTimeInterval:CMTimeGetSeconds(_player.currentItem.currentTime)]; + } +} + +- (void)errorWhilePlaying:(NSNotification *)notification +{ + 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; + 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) { + NSLog(@"Log code %ld domain %@ comment %@", (long)logEvent.errorStatusCode, logEvent.errorDomain, logEvent.errorComment); + } + } +} + +#pragma mark - Internal Properties + +- (AVPlayerItem *)currentItem +{ + ASLockScopeSelf(); + return _currentPlayerItem; +} + +- (void)setCurrentItem:(AVPlayerItem *)currentItem +{ + ASLockScopeSelf(); + + [self removePlayerItemObservers:_currentPlayerItem]; + + _currentPlayerItem = currentItem; + + if (currentItem != nil) { + [self addPlayerItemObservers:currentItem]; + } +} + +- (ASDisplayNode *)playerNode +{ + ASLockScopeSelf(); + return _playerNode; +} + +- (void)setPlayerNode:(ASDisplayNode *)playerNode +{ + { + ASLockScopeSelf(); + _playerNode = playerNode; + } + + [self setNeedsLayout]; +} + +- (void)setPlayer:(AVPlayer *)player +{ + ASLockScopeSelf(); + + [self removePlayerObservers:_player]; + + _player = player; + player.muted = _muted; + ((AVPlayerLayer *)_playerNode.layer).player = player; + + if (player != nil) { + [self addPlayerObservers:player]; + } +} + +- (BOOL)shouldBePlaying +{ + ASLockScopeSelf(); + return _shouldBePlaying; +} + +- (void)setShouldBePlaying:(BOOL)shouldBePlaying +{ + ASLockScopeSelf(); + _shouldBePlaying = shouldBePlaying; +} + +#pragma mark - Lifecycle + +- (void)dealloc +{ + [self removePlayerItemObservers:_currentPlayerItem]; + [self removePlayerObservers:_player]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; +} + +@end +#endif +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASVideoPlayerNode.h b/submodules/AsyncDisplayKit/Source/ASVideoPlayerNode.h new file mode 100644 index 0000000000..879628051a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASVideoPlayerNode.h @@ -0,0 +1,228 @@ +// +// ASVideoPlayerNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#if AS_USE_VIDEO + +#if TARGET_OS_IOS +#import +#import +#import +#import + +@class AVAsset; +@class ASButtonNode; +@protocol ASVideoPlayerNodeDelegate; + +typedef NS_ENUM(NSInteger, ASVideoPlayerNodeControlType) { + ASVideoPlayerNodeControlTypePlaybackButton, + ASVideoPlayerNodeControlTypeElapsedText, + ASVideoPlayerNodeControlTypeDurationText, + ASVideoPlayerNodeControlTypeScrubber, + ASVideoPlayerNodeControlTypeFullScreenButton, + ASVideoPlayerNodeControlTypeFlexGrowSpacer, +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface ASVideoPlayerNode : ASDisplayNode + +@property (nullable, nonatomic, weak) id delegate; + +@property (nonatomic, readonly) CMTime duration; + +@property (nonatomic) BOOL controlsDisabled; + +#pragma mark - ASVideoNode property proxy +/** + * 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) BOOL shouldAutoPlay; +@property (nonatomic) BOOL shouldAutoRepeat; +@property (nonatomic) BOOL muted; +@property (nonatomic, readonly) ASVideoNodePlayerState playerState; +@property (nonatomic) BOOL shouldAggressivelyRecoverFromStall; +@property (nullable, nonatomic) NSURL *placeholderImageURL; + +@property (nullable, nonatomic) AVAsset *asset; +/** + ** @abstract The URL with which the asset was initialized. + ** @discussion Setting the URL will override the current asset with a newly created AVURLAsset created from the given URL, and AVAsset *asset will point to that newly created AVURLAsset. Please don't set both assetURL and asset. + ** @return Current URL the asset was initialized or nil if no URL was given. + **/ +@property (nullable, nonatomic) NSURL *assetURL; + +/// You should never set any value on the backing video node. Use exclusivively the video player node to set properties +@property (nonatomic, readonly) ASVideoNode *videoNode; + +//! Defaults to 100 +@property (nonatomic) int32_t periodicTimeObserverTimescale; +//! Defaults to AVLayerVideoGravityResizeAspect +@property (nonatomic, copy) NSString *gravity; + +#pragma mark - Lifecycle +- (instancetype)initWithURL:(NSURL *)URL; +- (instancetype)initWithAsset:(AVAsset *)asset; +- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix; + +#pragma mark - Public API +- (void)seekToTime:(CGFloat)percentComplete; +- (void)play; +- (void)pause; +- (BOOL)isPlaying; +- (void)resetToPlaceholder; + +@end + +#pragma mark - ASVideoPlayerNodeDelegate - +@protocol ASVideoPlayerNodeDelegate +@optional +/** + * @abstract Delegate method invoked before creating controlbar controls + * @param videoPlayer The sender + */ +- (NSArray *)videoPlayerNodeNeededDefaultControls:(ASVideoPlayerNode*)videoPlayer; + +/** + * @abstract Delegate method invoked before creating default controls, asks delegate for custom controls dictionary. + * This dictionary must constain only ASDisplayNode subclass objects. + * @param videoPlayer The sender + * @discussion - This method is invoked only when developer implements videoPlayerNodeLayoutSpec:forControls:forMaximumSize: + * and gives ability to add custom constrols to ASVideoPlayerNode, for example mute button. + */ +- (NSDictionary *)videoPlayerNodeCustomControls:(ASVideoPlayerNode*)videoPlayer; + +/** + * @abstract Delegate method invoked in layoutSpecThatFits: + * @param videoPlayer The sender + * @param controls - Dictionary of controls which are used in videoPlayer; Dictionary keys are ASVideoPlayerNodeControlType + * @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 + forMaximumSize:(CGSize)maxSize; + +#pragma mark Text delegate methods +/** + * @abstract Delegate method invoked before creating ASVideoPlayerNodeControlTypeElapsedText and ASVideoPlayerNodeControlTypeDurationText + * @param videoPlayer The sender + * @param timeLabelType The of the time label + */ +- (NSDictionary *)videoPlayerNodeTimeLabelAttributes:(ASVideoPlayerNode *)videoPlayer timeLabelType:(ASVideoPlayerNodeControlType)timeLabelType; +- (NSString *)videoPlayerNode:(ASVideoPlayerNode *)videoPlayerNode + timeStringForTimeLabelType:(ASVideoPlayerNodeControlType)timeLabelType + forTime:(CMTime)time; + +#pragma mark Scrubber delegate methods +- (UIColor *)videoPlayerNodeScrubberMaximumTrackTint:(ASVideoPlayerNode *)videoPlayer; +- (UIColor *)videoPlayerNodeScrubberMinimumTrackTint:(ASVideoPlayerNode *)videoPlayer; +- (UIColor *)videoPlayerNodeScrubberThumbTint:(ASVideoPlayerNode *)videoPlayer; +- (UIImage *)videoPlayerNodeScrubberThumbImage:(ASVideoPlayerNode *)videoPlayer; + +#pragma mark - Spinner delegate methods +- (UIColor *)videoPlayerNodeSpinnerTint:(ASVideoPlayerNode *)videoPlayer; +- (UIActivityIndicatorViewStyle)videoPlayerNodeSpinnerStyle:(ASVideoPlayerNode *)videoPlayer; + +#pragma mark - Playback button delegate methods +- (UIColor *)videoPlayerNodePlaybackButtonTint:(ASVideoPlayerNode *)videoPlayer; + +#pragma mark - Fullscreen button delegate methods + +- (UIImage *)videoPlayerNodeFullScreenButtonImage:(ASVideoPlayerNode *)videoPlayer; + + +#pragma mark ASVideoNodeDelegate proxy methods +/** + * @abstract Delegate method invoked when ASVideoPlayerNode is taped. + * @param videoPlayer The ASVideoPlayerNode that was tapped. + */ +- (void)didTapVideoPlayerNode:(ASVideoPlayerNode *)videoPlayer; + +/** + * @abstract Delegate method invoked when fullcreen button is taped. + * @param buttonNode The fullscreen button node that was tapped. + */ +- (void)didTapFullScreenButtonNode:(ASButtonNode *)buttonNode; + +/** + * @abstract Delegate method invoked when ASVideoNode playback time is updated. + * @param videoPlayer The video player node + * @param time current playback time. + */ +- (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer didPlayToTime:(CMTime)time; + +/** + * @abstract Delegate method invoked when ASVideoNode changes state. + * @param videoPlayer The ASVideoPlayerNode whose ASVideoNode is changing state. + * @param state ASVideoNode state before this change. + * @param toState ASVideoNode new state. + * @discussion This method is called after each state change + */ +- (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer willChangeVideoNodeState:(ASVideoNodePlayerState)state toVideoNodeState:(ASVideoNodePlayerState)toState; + +/** + * @abstract Delegate method is invoked when ASVideoNode decides to change state. + * @param videoPlayer The ASVideoPlayerNode whose ASVideoNode is changing state. + * @param state ASVideoNode that is going to be set. + * @discussion Delegate method invoked when player changes it's state to + * ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused + * and asks delegate if state change is valid + */ +- (BOOL)videoPlayerNode:(ASVideoPlayerNode*)videoPlayer shouldChangeVideoNodeStateTo:(ASVideoNodePlayerState)state; + +/** + * @abstract Delegate method invoked when the ASVideoNode has played to its end time. + * @param videoPlayer The video node has played to its end time. + */ +- (void)videoPlayerNodeDidPlayToEnd:(ASVideoPlayerNode *)videoPlayer; + +/** + * @abstract Delegate method invoked when the ASVideoNode has constructed its AVPlayerItem for the asset. + * @param videoPlayer The video player node. + * @param currentItem The AVPlayerItem that was constructed from the asset. + */ +- (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer didSetCurrentItem:(AVPlayerItem *)currentItem; + +/** + * @abstract Delegate method invoked when the ASVideoNode stalls. + * @param videoPlayer The video player node that has experienced the stall + * @param timeInterval Current playback time when the stall happens + */ +- (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer didStallAtTimeInterval:(NSTimeInterval)timeInterval; + +/** + * @abstract Delegate method invoked when the ASVideoNode starts the inital asset loading + * @param videoPlayer The videoPlayer + */ +- (void)videoPlayerNodeDidStartInitialLoading:(ASVideoPlayerNode *)videoPlayer; + +/** + * @abstract Delegate method invoked when the ASVideoNode is done loading the asset and can start the playback + * @param videoPlayer The videoPlayer + */ +- (void)videoPlayerNodeDidFinishInitialLoading:(ASVideoPlayerNode *)videoPlayer; + +/** + * @abstract Delegate method invoked when the ASVideoNode has recovered from the stall + * @param videoPlayer The videoplayer + */ +- (void)videoPlayerNodeDidRecoverFromStall:(ASVideoPlayerNode *)videoPlayer; + + +@end +NS_ASSUME_NONNULL_END +#endif + +#endif +#endif // TARGET_OS_IOS diff --git a/submodules/AsyncDisplayKit/Source/ASVideoPlayerNode.mm b/submodules/AsyncDisplayKit/Source/ASVideoPlayerNode.mm new file mode 100644 index 0000000000..eaa8cd91a9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASVideoPlayerNode.mm @@ -0,0 +1,1024 @@ +// +// ASVideoPlayerNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#ifndef MINIMAL_ASDK + +#import + +#if AS_USE_VIDEO +#if TARGET_OS_IOS + +#import + +#import +#import +#import +#import +#import + +static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; + +@interface ASVideoPlayerNode() +{ + __weak id _delegate; + + struct { + unsigned int delegateNeededDefaultControls:1; + unsigned int delegateCustomControls:1; + unsigned int delegateSpinnerTintColor:1; + unsigned int delegateSpinnerStyle:1; + unsigned int delegatePlaybackButtonTint:1; + unsigned int delegateFullScreenButtonImage:1; + unsigned int delegateScrubberMaximumTrackTintColor:1; + unsigned int delegateScrubberMinimumTrackTintColor:1; + unsigned int delegateScrubberThumbTintColor:1; + unsigned int delegateScrubberThumbImage:1; + unsigned int delegateTimeLabelAttributes:1; + unsigned int delegateTimeLabelAttributedString:1; + unsigned int delegateLayoutSpecForControls:1; + unsigned int delegateVideoNodeDidPlayToTime:1; + unsigned int delegateVideoNodeWillChangeState:1; + unsigned int delegateVideoNodeShouldChangeState:1; + unsigned int delegateVideoNodePlaybackDidFinish:1; + unsigned int delegateDidTapVideoPlayerNode:1; + unsigned int delegateDidTapFullScreenButtonNode:1; + unsigned int delegateVideoPlayerNodeDidSetCurrentItem:1; + unsigned int delegateVideoPlayerNodeDidStallAtTimeInterval:1; + unsigned int delegateVideoPlayerNodeDidStartInitialLoading:1; + unsigned int delegateVideoPlayerNodeDidFinishInitialLoading:1; + unsigned int delegateVideoPlayerNodeDidRecoverFromStall:1; + } _delegateFlags; + + // The asset passed in the initializer will be assigned as pending asset. As soon as the first + // preload state happened all further asset handling is made by using the asset of the backing + // video node + AVAsset *_pendingAsset; + + // The backing video node. Ideally this is the source of truth and the video player node should + // not handle anything related to asset management + ASVideoNode *_videoNode; + + NSArray *_neededDefaultControls; + + NSMutableDictionary *_cachedControls; + + ASDefaultPlaybackButton *_playbackButtonNode; + ASButtonNode *_fullScreenButtonNode; + ASTextNode *_elapsedTextNode; + ASTextNode *_durationTextNode; + ASDisplayNode *_scrubberNode; + ASStackLayoutSpec *_controlFlexGrowSpacerSpec; + ASDisplayNode *_spinnerNode; + + BOOL _isSeeking; + CMTime _duration; + + BOOL _controlsDisabled; + + BOOL _shouldAutoPlay; + BOOL _shouldAutoRepeat; + BOOL _muted; + int32_t _periodicTimeObserverTimescale; + NSString *_gravity; + + BOOL _shouldAggressivelyRecoverFromStall; + + UIColor *_defaultControlsColor; +} + +@end + +@implementation ASVideoPlayerNode + +@dynamic placeholderImageURL; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + [self _initControlsAndVideoNode]; + + return self; +} + +- (instancetype)initWithAsset:(AVAsset *)asset +{ + if (!(self = [self init])) { + return nil; + } + + _pendingAsset = asset; + + return self; +} + +- (instancetype)initWithURL:(NSURL *)URL +{ + return [self initWithAsset:[AVAsset assetWithURL:URL]]; +} + +- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix +{ + if (!(self = [self initWithAsset:asset])) { + return nil; + } + + _videoNode.videoComposition = videoComposition; + _videoNode.audioMix = audioMix; + + return self; +} + +- (void)_initControlsAndVideoNode +{ + _defaultControlsColor = [UIColor whiteColor]; + _cachedControls = [[NSMutableDictionary alloc] init]; + + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.delegate = self; + [self addSubnode:_videoNode]; +} + +#pragma mark - Setter / Getter + +- (void)setAssetURL:(NSURL *)assetURL +{ + ASDisplayNodeAssertMainThread(); + + self.asset = [AVAsset assetWithURL:assetURL]; +} + +- (NSURL *)assetURL +{ + NSURL *url = nil; + { + ASLockScopeSelf(); + if ([_pendingAsset isKindOfClass:AVURLAsset.class]) { + url = ((AVURLAsset *)_pendingAsset).URL; + } + } + + return url ?: _videoNode.assetURL; +} + +- (void)setAsset:(AVAsset *)asset +{ + ASDisplayNodeAssertMainThread(); + + [self lock]; + + // Clean out pending asset + _pendingAsset = nil; + + // Set asset based on interface state + if ((ASInterfaceStateIncludesPreload(self.interfaceState))) { + // Don't hold the lock while accessing the subnode + [self unlock]; + _videoNode.asset = asset; + return; + } + + _pendingAsset = asset; + [self unlock]; +} + +- (AVAsset *)asset +{ + return ASLockedSelf(_pendingAsset) ?: _videoNode.asset; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + [super didLoad]; + + [self createControls]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + + AVAsset *pendingAsset = nil; + { + ASLockScopeSelf(); + pendingAsset = _pendingAsset; + _pendingAsset = nil; + } + + // If we enter preload state we apply the pending asset to load to the video node so it can start and fetch the asset + if (pendingAsset != nil && _videoNode.asset != pendingAsset) { + _videoNode.asset = pendingAsset; + } +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +#pragma mark - UI + +- (void)createControls +{ + { + ASLockScopeSelf(); + + if (_controlsDisabled) { + return; + } + + if (_neededDefaultControls == nil) { + _neededDefaultControls = [self createDefaultControlElementArray]; + } + + if (_cachedControls == nil) { + _cachedControls = [[NSMutableDictionary alloc] init]; + } + + for (id object in _neededDefaultControls) { + ASVideoPlayerNodeControlType type = (ASVideoPlayerNodeControlType)[object integerValue]; + switch (type) { + case ASVideoPlayerNodeControlTypePlaybackButton: + [self _locked_createPlaybackButton]; + break; + case ASVideoPlayerNodeControlTypeElapsedText: + [self _locked_createElapsedTextField]; + break; + case ASVideoPlayerNodeControlTypeDurationText: + [self _locked_createDurationTextField]; + break; + case ASVideoPlayerNodeControlTypeScrubber: + [self _locked_createScrubber]; + break; + case ASVideoPlayerNodeControlTypeFullScreenButton: + [self _locked_createFullScreenButton]; + break; + case ASVideoPlayerNodeControlTypeFlexGrowSpacer: + [self _locked_createControlFlexGrowSpacer]; + break; + default: + break; + } + } + + if (_delegateFlags.delegateCustomControls && _delegateFlags.delegateLayoutSpecForControls) { + NSDictionary *customControls = [_delegate videoPlayerNodeCustomControls:self]; + std::vector subnodes; + for (id key in customControls) { + id node = customControls[key]; + if (![node isKindOfClass:[ASDisplayNode class]]) { + continue; + } + + subnodes.push_back(node); + [_cachedControls setObject:node forKey:key]; + } + + { + ASUnlockScope(self); + for (ASDisplayNode *subnode : subnodes) { + [self addSubnode:subnode]; + } + } + } + } + + ASPerformBlockOnMainThread(^{ + [self setNeedsLayout]; + }); +} + +- (NSArray *)createDefaultControlElementArray +{ + if (_delegateFlags.delegateNeededDefaultControls) { + return [_delegate videoPlayerNodeNeededDefaultControls:self]; + } + + return @[ @(ASVideoPlayerNodeControlTypePlaybackButton), + @(ASVideoPlayerNodeControlTypeElapsedText), + @(ASVideoPlayerNodeControlTypeScrubber), + @(ASVideoPlayerNodeControlTypeDurationText) ]; +} + +- (void)removeControls +{ + NSMutableDictionary *cachedControls = nil; + { + ASLockScope(self); + + // Grab the cached controls for removing it + cachedControls = [_cachedControls copy]; + [self _locked_cleanCachedControls]; + } + + for (ASDisplayNode *node in [cachedControls objectEnumerator]) { + [node removeFromSupernode]; + } +} + +- (void)_locked_cleanCachedControls +{ + [_cachedControls removeAllObjects]; + + _playbackButtonNode = nil; + _fullScreenButtonNode = nil; + _elapsedTextNode = nil; + _durationTextNode = nil; + _scrubberNode = nil; +} + +- (void)_locked_createPlaybackButton +{ + ASAssertLocked(__instanceLock__); + + if (_playbackButtonNode == nil) { + _playbackButtonNode = [[ASDefaultPlaybackButton alloc] init]; + _playbackButtonNode.style.preferredSize = CGSizeMake(16.0, 22.0); + + if (_delegateFlags.delegatePlaybackButtonTint) { + _playbackButtonNode.tintColor = [_delegate videoPlayerNodePlaybackButtonTint:self]; + } else { + _playbackButtonNode.tintColor = _defaultControlsColor; + } + + if (_videoNode.playerState == ASVideoNodePlayerStatePlaying) { + _playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePause; + } + + [_playbackButtonNode addTarget:self action:@selector(didTapPlaybackButton:) forControlEvents:ASControlNodeEventTouchUpInside]; + [_cachedControls setObject:_playbackButtonNode forKey:@(ASVideoPlayerNodeControlTypePlaybackButton)]; + } + + { + ASUnlockScope(self); + [self addSubnode:_playbackButtonNode]; + } +} + +- (void)_locked_createFullScreenButton +{ + ASAssertLocked(__instanceLock__); + + if (_fullScreenButtonNode == nil) { + _fullScreenButtonNode = [[ASButtonNode alloc] init]; + _fullScreenButtonNode.style.preferredSize = CGSizeMake(16.0, 22.0); + + if (_delegateFlags.delegateFullScreenButtonImage) { + [_fullScreenButtonNode setImage:[_delegate videoPlayerNodeFullScreenButtonImage:self] forState:UIControlStateNormal]; + } + + [_fullScreenButtonNode addTarget:self action:@selector(didTapFullScreenButton:) forControlEvents:ASControlNodeEventTouchUpInside]; + [_cachedControls setObject:_fullScreenButtonNode forKey:@(ASVideoPlayerNodeControlTypeFullScreenButton)]; + } + + { + ASUnlockScope(self); + [self addSubnode:_fullScreenButtonNode]; + } +} + +- (void)_locked_createElapsedTextField +{ + ASAssertLocked(__instanceLock__); + + if (_elapsedTextNode == nil) { + _elapsedTextNode = [[ASTextNode alloc] init]; + _elapsedTextNode.attributedText = [self timeLabelAttributedStringForString:@"00:00" + forControlType:ASVideoPlayerNodeControlTypeElapsedText]; + _elapsedTextNode.truncationMode = NSLineBreakByClipping; + + [_cachedControls setObject:_elapsedTextNode forKey:@(ASVideoPlayerNodeControlTypeElapsedText)]; + } + { + ASUnlockScope(self); + [self addSubnode:_elapsedTextNode]; + } +} + +- (void)_locked_createDurationTextField +{ + ASAssertLocked(__instanceLock__); + + if (_durationTextNode == nil) { + _durationTextNode = [[ASTextNode alloc] init]; + _durationTextNode.attributedText = [self timeLabelAttributedStringForString:@"00:00" + forControlType:ASVideoPlayerNodeControlTypeDurationText]; + _durationTextNode.truncationMode = NSLineBreakByClipping; + + [_cachedControls setObject:_durationTextNode forKey:@(ASVideoPlayerNodeControlTypeDurationText)]; + } + [self updateDurationTimeLabel]; + { + ASUnlockScope(self); + [self addSubnode:_durationTextNode]; + } +} + +- (void)_locked_createScrubber +{ + ASAssertLocked(__instanceLock__); + + if (_scrubberNode == nil) { + __weak __typeof__(self) weakSelf = self; + _scrubberNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull { + __typeof__(self) strongSelf = weakSelf; + + UISlider *slider = [[UISlider alloc] initWithFrame:CGRectZero]; + slider.minimumValue = 0.0; + slider.maximumValue = 1.0; + + if (_delegateFlags.delegateScrubberMinimumTrackTintColor) { + slider.minimumTrackTintColor = [strongSelf.delegate videoPlayerNodeScrubberMinimumTrackTint:strongSelf]; + } + + if (_delegateFlags.delegateScrubberMaximumTrackTintColor) { + slider.maximumTrackTintColor = [strongSelf.delegate videoPlayerNodeScrubberMaximumTrackTint:strongSelf]; + } + + if (_delegateFlags.delegateScrubberThumbTintColor) { + slider.thumbTintColor = [strongSelf.delegate videoPlayerNodeScrubberThumbTint:strongSelf]; + } + + if (_delegateFlags.delegateScrubberThumbImage) { + UIImage *thumbImage = [strongSelf.delegate videoPlayerNodeScrubberThumbImage:strongSelf]; + [slider setThumbImage:thumbImage forState:UIControlStateNormal]; + } + + + [slider addTarget:strongSelf action:@selector(beginSeek) forControlEvents:UIControlEventTouchDown]; + [slider addTarget:strongSelf action:@selector(endSeek) forControlEvents:UIControlEventTouchUpInside|UIControlEventTouchUpOutside|UIControlEventTouchCancel]; + [slider addTarget:strongSelf action:@selector(seekTimeDidChange:) forControlEvents:UIControlEventValueChanged]; + + return slider; + }]; + + _scrubberNode.style.flexShrink = 1; + + [_cachedControls setObject:_scrubberNode forKey:@(ASVideoPlayerNodeControlTypeScrubber)]; + } + { + ASUnlockScope(self); + [self addSubnode:_scrubberNode]; + } +} + +- (void)_locked_createControlFlexGrowSpacer +{ + ASAssertLocked(__instanceLock__); + + if (_controlFlexGrowSpacerSpec == nil) { + _controlFlexGrowSpacerSpec = [[ASStackLayoutSpec alloc] init]; + _controlFlexGrowSpacerSpec.style.flexGrow = 1.0; + } + + [_cachedControls setObject:_controlFlexGrowSpacerSpec forKey:@(ASVideoPlayerNodeControlTypeFlexGrowSpacer)]; +} + +- (void)updateDurationTimeLabel +{ + if (!_durationTextNode) { + return; + } + NSString *formattedDuration = [self timeStringForCMTime:_duration forTimeLabelType:ASVideoPlayerNodeControlTypeDurationText]; + _durationTextNode.attributedText = [self timeLabelAttributedStringForString:formattedDuration forControlType:ASVideoPlayerNodeControlTypeDurationText]; +} + +- (void)updateElapsedTimeLabel:(NSTimeInterval)seconds +{ + if (!_elapsedTextNode) { + return; + } + NSString *formattedElapsed = [self timeStringForCMTime:CMTimeMakeWithSeconds( seconds, _videoNode.periodicTimeObserverTimescale ) forTimeLabelType:ASVideoPlayerNodeControlTypeElapsedText]; + _elapsedTextNode.attributedText = [self timeLabelAttributedStringForString:formattedElapsed forControlType:ASVideoPlayerNodeControlTypeElapsedText]; +} + +- (NSAttributedString*)timeLabelAttributedStringForString:(NSString*)string forControlType:(ASVideoPlayerNodeControlType)controlType +{ + NSDictionary *options; + if (_delegateFlags.delegateTimeLabelAttributes) { + options = [_delegate videoPlayerNodeTimeLabelAttributes:self timeLabelType:controlType]; + } else { + options = @{ + NSFontAttributeName : [UIFont systemFontOfSize:12.0], + NSForegroundColorAttributeName: _defaultControlsColor + }; + } + + + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:options]; + + return attributedString; +} + +#pragma mark - ASVideoNodeDelegate +- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState +{ + if (_delegateFlags.delegateVideoNodeWillChangeState) { + [_delegate videoPlayerNode:self willChangeVideoNodeState:state toVideoNodeState:toState]; + } + + if (toState == ASVideoNodePlayerStateReadyToPlay) { + _duration = _videoNode.currentItem.duration; + [self updateDurationTimeLabel]; + } + + if (toState == ASVideoNodePlayerStatePlaying) { + _playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePause; + [self removeSpinner]; + } else if (toState != ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying && toState != ASVideoNodePlayerStateReadyToPlay) { + _playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePlay; + } + + if (toState == ASVideoNodePlayerStateLoading || toState == ASVideoNodePlayerStateInitialLoading) { + [self showSpinner]; + } + + if (toState == ASVideoNodePlayerStateReadyToPlay || toState == ASVideoNodePlayerStatePaused || toState == ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying) { + [self removeSpinner]; + } +} + +- (BOOL)videoNode:(ASVideoNode *)videoNode shouldChangePlayerStateTo:(ASVideoNodePlayerState)state +{ + if (_delegateFlags.delegateVideoNodeShouldChangeState) { + return [_delegate videoPlayerNode:self shouldChangeVideoNodeStateTo:state]; + } + return YES; +} + +- (void)videoNode:(ASVideoNode *)videoNode didPlayToTimeInterval:(NSTimeInterval)timeInterval +{ + if (_delegateFlags.delegateVideoNodeDidPlayToTime) { + [_delegate videoPlayerNode:self didPlayToTime:_videoNode.player.currentTime]; + } + + if (_isSeeking) { + return; + } + + if (_elapsedTextNode) { + [self updateElapsedTimeLabel:timeInterval]; + } + + if (_scrubberNode) { + [(UISlider*)_scrubberNode.view setValue:( timeInterval / CMTimeGetSeconds(_duration) ) animated:NO]; + } +} + +- (void)videoDidPlayToEnd:(ASVideoNode *)videoNode +{ + if (_delegateFlags.delegateVideoNodePlaybackDidFinish) { + [_delegate videoPlayerNodeDidPlayToEnd:self]; + } +} + +- (void)didTapVideoNode:(ASVideoNode *)videoNode +{ + if (_delegateFlags.delegateDidTapVideoPlayerNode) { + [_delegate didTapVideoPlayerNode:self]; + } else { + [self togglePlayPause]; + } +} + +- (void)videoNode:(ASVideoNode *)videoNode didSetCurrentItem:(AVPlayerItem *)currentItem +{ + if (_delegateFlags.delegateVideoPlayerNodeDidSetCurrentItem) { + [_delegate videoPlayerNode:self didSetCurrentItem:currentItem]; + } +} + +- (void)videoNode:(ASVideoNode *)videoNode didStallAtTimeInterval:(NSTimeInterval)timeInterval +{ + if (_delegateFlags.delegateVideoPlayerNodeDidStallAtTimeInterval) { + [_delegate videoPlayerNode:self didStallAtTimeInterval:timeInterval]; + } +} + +- (void)videoNodeDidStartInitialLoading:(ASVideoNode *)videoNode +{ + if (_delegateFlags.delegateVideoPlayerNodeDidStartInitialLoading) { + [_delegate videoPlayerNodeDidStartInitialLoading:self]; + } +} + +- (void)videoNodeDidFinishInitialLoading:(ASVideoNode *)videoNode +{ + if (_delegateFlags.delegateVideoPlayerNodeDidFinishInitialLoading) { + [_delegate videoPlayerNodeDidFinishInitialLoading:self]; + } +} + +- (void)videoNodeDidRecoverFromStall:(ASVideoNode *)videoNode +{ + if (_delegateFlags.delegateVideoPlayerNodeDidRecoverFromStall) { + [_delegate videoPlayerNodeDidRecoverFromStall:self]; + } +} + +#pragma mark - Actions +- (void)togglePlayPause +{ + if (_videoNode.playerState == ASVideoNodePlayerStatePlaying) { + [_videoNode pause]; + } else { + [_videoNode play]; + } +} + +- (void)showSpinner +{ + ASLockScopeSelf(); + + if (!_spinnerNode) { + __weak __typeof__(self) weakSelf = self; + _spinnerNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + __typeof__(self) strongSelf = weakSelf; + UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init]; + spinnnerView.backgroundColor = [UIColor clearColor]; + + if (_delegateFlags.delegateSpinnerTintColor) { + spinnnerView.color = [_delegate videoPlayerNodeSpinnerTint:strongSelf]; + } else { + spinnnerView.color = _defaultControlsColor; + } + + if (_delegateFlags.delegateSpinnerStyle) { + spinnnerView.activityIndicatorViewStyle = [_delegate videoPlayerNodeSpinnerStyle:strongSelf]; + } + + return spinnnerView; + }]; + _spinnerNode.style.preferredSize = CGSizeMake(44.0, 44.0); + + const auto spinnerNode = _spinnerNode; + { + ASUnlockScope(self); + [self addSubnode:spinnerNode]; + [self setNeedsLayout]; + } + } + [(UIActivityIndicatorView *)_spinnerNode.view startAnimating]; +} + +- (void)removeSpinner +{ + ASDisplayNode *spinnerNode = nil; + { + ASLockScopeSelf(); + if (!_spinnerNode) { + return; + } + + spinnerNode = _spinnerNode; + _spinnerNode = nil; + } + + [spinnerNode removeFromSupernode]; +} + +- (void)didTapPlaybackButton:(ASControlNode*)node +{ + [self togglePlayPause]; +} + +- (void)didTapFullScreenButton:(ASButtonNode*)node +{ + if (_delegateFlags.delegateDidTapFullScreenButtonNode) { + [_delegate didTapFullScreenButtonNode:node]; + } +} + +- (void)beginSeek +{ + _isSeeking = YES; +} + +- (void)endSeek +{ + _isSeeking = NO; +} + +- (void)seekTimeDidChange:(UISlider*)slider +{ + CGFloat percentage = slider.value * 100; + [self seekToTime:percentage]; +} + +#pragma mark - Public API +- (void)seekToTime:(CGFloat)percentComplete +{ + CGFloat seconds = ( CMTimeGetSeconds(_duration) * percentComplete ) / 100; + + [self updateElapsedTimeLabel:seconds]; + [_videoNode.player seekToTime:CMTimeMakeWithSeconds(seconds, _videoNode.periodicTimeObserverTimescale)]; + + if (_videoNode.playerState != ASVideoNodePlayerStatePlaying) { + [self togglePlayPause]; + } +} + +- (void)play +{ + [_videoNode play]; +} + +- (void)pause +{ + [_videoNode pause]; +} + +- (BOOL)isPlaying +{ + return [_videoNode isPlaying]; +} + +- (void)resetToPlaceholder +{ + [_videoNode resetToPlaceholder]; +} + +- (NSArray *)controlsForLayoutSpec +{ + NSMutableArray *controls = [[NSMutableArray alloc] initWithCapacity:_cachedControls.count]; + + if (_cachedControls[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]) { + [controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]]; + } + + if (_cachedControls[ @(ASVideoPlayerNodeControlTypeElapsedText) ]) { + [controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeElapsedText) ]]; + } + + if (_cachedControls[ @(ASVideoPlayerNodeControlTypeScrubber) ]) { + [controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeScrubber) ]]; + } + + if (_cachedControls[ @(ASVideoPlayerNodeControlTypeDurationText) ]) { + [controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeDurationText) ]]; + } + + if (_cachedControls[ @(ASVideoPlayerNodeControlTypeFullScreenButton) ]) { + [controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeFullScreenButton) ]]; + } + + return controls; +} + + +#pragma mark - Layout + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGSize maxSize = constrainedSize.max; + + // 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.style.preferredSize = maxSize; + + ASLayoutSpec *layoutSpec; + if (_delegateFlags.delegateLayoutSpecForControls) { + layoutSpec = [_delegate videoPlayerNodeLayoutSpec:self forControls:_cachedControls forMaximumSize:maxSize]; + } else { + layoutSpec = [self defaultLayoutSpecThatFits:maxSize]; + } + + NSMutableArray *children = [[NSMutableArray alloc] init]; + + if (_spinnerNode) { + ASCenterLayoutSpec *centerLayoutSpec = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:_spinnerNode]; + centerLayoutSpec.style.preferredSize = maxSize; + [children addObject:centerLayoutSpec]; + } + + ASOverlayLayoutSpec *overlaySpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:_videoNode overlay:layoutSpec]; + overlaySpec.style.preferredSize = maxSize; + [children addObject:overlaySpec]; + + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:children]; +} + +- (ASLayoutSpec *)defaultLayoutSpecThatFits:(CGSize)maxSize +{ + _scrubberNode.style.preferredSize = CGSizeMake(maxSize.width, 44.0); + + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.style.flexGrow = 1.0; + + ASStackLayoutSpec *controlbarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children: [self controlsForLayoutSpec] ]; + controlbarSpec.style.alignSelf = ASStackLayoutAlignSelfStretch; + + UIEdgeInsets insets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); + + ASInsetLayoutSpec *controlbarInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:controlbarSpec]; + + controlbarInsetSpec.style.alignSelf = ASStackLayoutAlignSelfStretch; + + ASStackLayoutSpec *mainVerticalStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[spacer,controlbarInsetSpec]]; + + return mainVerticalStack; +} + +#pragma mark - Properties +- (id)delegate +{ + return _delegate; +} + +- (void)setDelegate:(id)delegate +{ + if (delegate == _delegate) { + return; + } + + _delegate = delegate; + + if (_delegate == nil) { + memset(&_delegateFlags, 0, sizeof(_delegateFlags)); + } else { + _delegateFlags.delegateNeededDefaultControls = [_delegate respondsToSelector:@selector(videoPlayerNodeNeededDefaultControls:)]; + _delegateFlags.delegateCustomControls = [_delegate respondsToSelector:@selector(videoPlayerNodeCustomControls:)]; + _delegateFlags.delegateSpinnerTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeSpinnerTint:)]; + _delegateFlags.delegateSpinnerStyle = [_delegate respondsToSelector:@selector(videoPlayerNodeSpinnerStyle:)]; + _delegateFlags.delegateScrubberMaximumTrackTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberMaximumTrackTint:)]; + _delegateFlags.delegateScrubberMinimumTrackTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberMinimumTrackTint:)]; + _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: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.delegateFullScreenButtonImage = [_delegate respondsToSelector:@selector(videoPlayerNodeFullScreenButtonImage:)]; + _delegateFlags.delegateDidTapVideoPlayerNode = [_delegate respondsToSelector:@selector(didTapVideoPlayerNode:)]; + _delegateFlags.delegateDidTapFullScreenButtonNode = [_delegate respondsToSelector:@selector(didTapFullScreenButtonNode:)]; + _delegateFlags.delegateVideoPlayerNodeDidSetCurrentItem = [_delegate respondsToSelector:@selector(videoPlayerNode:didSetCurrentItem:)]; + _delegateFlags.delegateVideoPlayerNodeDidStallAtTimeInterval = [_delegate respondsToSelector:@selector(videoPlayerNode:didStallAtTimeInterval:)]; + _delegateFlags.delegateVideoPlayerNodeDidStartInitialLoading = [_delegate respondsToSelector:@selector(videoPlayerNodeDidStartInitialLoading:)]; + _delegateFlags.delegateVideoPlayerNodeDidFinishInitialLoading = [_delegate respondsToSelector:@selector(videoPlayerNodeDidFinishInitialLoading:)]; + _delegateFlags.delegateVideoPlayerNodeDidRecoverFromStall = [_delegate respondsToSelector:@selector(videoPlayerNodeDidRecoverFromStall:)]; + } +} + +- (void)setControlsDisabled:(BOOL)controlsDisabled +{ + if (_controlsDisabled == controlsDisabled) { + return; + } + + _controlsDisabled = controlsDisabled; + + if (_controlsDisabled && _cachedControls.count > 0) { + [self removeControls]; + } else if (!_controlsDisabled) { + [self createControls]; + } +} + +- (void)setShouldAutoPlay:(BOOL)shouldAutoPlay +{ + if (_shouldAutoPlay == shouldAutoPlay) { + return; + } + _shouldAutoPlay = shouldAutoPlay; + _videoNode.shouldAutoplay = _shouldAutoPlay; +} + +- (void)setShouldAutoRepeat:(BOOL)shouldAutoRepeat +{ + if (_shouldAutoRepeat == shouldAutoRepeat) { + return; + } + _shouldAutoRepeat = shouldAutoRepeat; + _videoNode.shouldAutorepeat = _shouldAutoRepeat; +} + +- (void)setMuted:(BOOL)muted +{ + if (_muted == muted) { + return; + } + _muted = muted; + _videoNode.muted = _muted; +} + +- (void)setPeriodicTimeObserverTimescale:(int32_t)periodicTimeObserverTimescale +{ + if (_periodicTimeObserverTimescale == periodicTimeObserverTimescale) { + return; + } + _periodicTimeObserverTimescale = periodicTimeObserverTimescale; + _videoNode.periodicTimeObserverTimescale = _periodicTimeObserverTimescale; +} + +- (NSString *)gravity +{ + if (_gravity == nil) { + _gravity = _videoNode.gravity; + } + return _gravity; +} + +- (void)setGravity:(NSString *)gravity +{ + if (_gravity == gravity) { + return; + } + _gravity = gravity; + _videoNode.gravity = _gravity; +} + +- (ASVideoNodePlayerState)playerState +{ + return _videoNode.playerState; +} + +- (BOOL)shouldAggressivelyRecoverFromStall +{ + return _videoNode.shouldAggressivelyRecoverFromStall; +} + +- (void) setPlaceholderImageURL:(NSURL *)placeholderImageURL +{ + _videoNode.URL = placeholderImageURL; +} + +- (NSURL*) placeholderImageURL +{ + return _videoNode.URL; +} + +- (ASVideoNode*)videoNode +{ + return _videoNode; +} + +- (void)setShouldAggressivelyRecoverFromStall:(BOOL)shouldAggressivelyRecoverFromStall +{ + if (_shouldAggressivelyRecoverFromStall == shouldAggressivelyRecoverFromStall) { + return; + } + _shouldAggressivelyRecoverFromStall = shouldAggressivelyRecoverFromStall; + _videoNode.shouldAggressivelyRecoverFromStall = _shouldAggressivelyRecoverFromStall; +} + +#pragma mark - Helpers +- (NSString *)timeStringForCMTime:(CMTime)time forTimeLabelType:(ASVideoPlayerNodeControlType)type +{ + if (!CMTIME_IS_VALID(time)) { + return @"00:00"; + } + if (_delegateFlags.delegateTimeLabelAttributedString) { + return [_delegate videoPlayerNode:self timeStringForTimeLabelType:type forTime:time]; + } + + NSUInteger dTotalSeconds = CMTimeGetSeconds(time); + + NSUInteger dHours = floor(dTotalSeconds / 3600); + NSUInteger dMinutes = floor(dTotalSeconds % 3600 / 60); + NSUInteger dSeconds = floor(dTotalSeconds % 3600 % 60); + + NSString *videoDurationText; + if (dHours > 0) { + videoDurationText = [NSString stringWithFormat:@"%i:%02i:%02i", (int)dHours, (int)dMinutes, (int)dSeconds]; + } else { + videoDurationText = [NSString stringWithFormat:@"%02i:%02i", (int)dMinutes, (int)dSeconds]; + } + return videoDurationText; +} + +@end + +#endif // TARGET_OS_IOS +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASViewController.h b/submodules/AsyncDisplayKit/Source/ASViewController.h new file mode 100644 index 0000000000..18c1be5456 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASViewController.h @@ -0,0 +1,96 @@ +// +// ASViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASTraitCollection; + +NS_ASSUME_NONNULL_BEGIN + +typedef ASTraitCollection * _Nonnull (^ASDisplayTraitsForTraitCollectionBlock)(UITraitCollection *traitCollection); +typedef ASTraitCollection * _Nonnull (^ASDisplayTraitsForTraitWindowSizeBlock)(CGSize windowSize); + +/** + * ASViewController allows you to have a completely node backed hierarchy. It automatically + * handles @c ASVisibilityDepth, automatic range mode and propogating @c ASDisplayTraits to contained nodes. + * + * You can opt-out of node backed hierarchy and use it like a normal UIViewController. + * More importantly, you can use it as a base class for all of your view controllers among which some use a node hierarchy and some don't. + * See examples/ASDKgram project for actual implementation. + */ +@interface ASViewController<__covariant DisplayNodeType : ASDisplayNode *> : UIViewController + +/** + * ASViewController initializer. + * + * @param node An ASDisplayNode which will provide the root view (self.view) + * @return An ASViewController instance whose root view will be backed by the provided ASDisplayNode. + * + * @see ASVisibilityDepth + */ +- (instancetype)initWithNode:(DisplayNodeType)node NS_DESIGNATED_INITIALIZER; + +NS_ASSUME_NONNULL_END + +/** + * @return node Returns the ASDisplayNode which provides the backing view to the view controller. + */ +@property (nonatomic, readonly, null_unspecified) DisplayNodeType node; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Set this block to customize the ASDisplayTraits returned when the VC transitions to the given traitCollection. + */ +@property (nonatomic, copy) ASDisplayTraitsForTraitCollectionBlock overrideDisplayTraitsWithTraitCollection; + +/** + * Set this block to customize the ASDisplayTraits returned when the VC transitions to the given window size. + */ +@property (nonatomic, copy) ASDisplayTraitsForTraitWindowSizeBlock overrideDisplayTraitsWithWindowSize ASDISPLAYNODE_DEPRECATED_MSG("This property is actually never accessed inside the framework"); + +/** + * @abstract Passthrough property to the the .interfaceState of the node. + * @return The current ASInterfaceState of the node, indicating whether it is visible and other situational properties. + * @see ASInterfaceState + */ +@property (nonatomic, readonly) ASInterfaceState interfaceState; + + +// AsyncDisplayKit 2.0 BETA: This property is still being tested, but it allows +// blocking as a view controller becomes visible to ensure no placeholders flash onscreen. +// Refer to examples/SynchronousConcurrency, AsyncViewController.m +@property (nonatomic) BOOL neverShowPlaceholders; + +/* Custom container UIViewController subclasses can use this property to add to the overlay + that UIViewController calculates for the safeAreaInsets for contained view controllers. + */ +@property(nonatomic) UIEdgeInsets additionalSafeAreaInsets; + +@end + +@interface ASViewController (ASRangeControllerUpdateRangeProtocol) + +/** + * Automatically adjust range mode based on view events. If you set this to YES, the view controller or its node + * must conform to the ASRangeControllerUpdateRangeProtocol. + * + * Default value is YES *if* node or view controller conform to ASRangeControllerUpdateRangeProtocol otherwise it is NO. + */ +@property (nonatomic) BOOL automaticallyAdjustRangeModeBasedOnViewEvents; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASViewController.mm b/submodules/AsyncDisplayKit/Source/ASViewController.mm new file mode 100644 index 0000000000..5992c050f1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASViewController.mm @@ -0,0 +1,362 @@ +// +// ASViewController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import +#import +#import +#import +#import +#import +#import + +@implementation ASViewController +{ + BOOL _ensureDisplayed; + BOOL _automaticallyAdjustRangeModeBasedOnViewEvents; + BOOL _parentManagesVisibilityDepth; + NSInteger _visibilityDepth; + BOOL _selfConformsToRangeModeProtocol; + BOOL _nodeConformsToRangeModeProtocol; + UIEdgeInsets _fallbackAdditionalSafeAreaInsets; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if (!(self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { + return nil; + } + + [self _initializeInstance]; + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + if (!(self = [super initWithCoder:aDecoder])) { + return nil; + } + + [self _initializeInstance]; + + return self; +} + +#pragma clang diagnostic pop + +- (instancetype)initWithNode:(ASDisplayNode *)node +{ + if (!(self = [super initWithNibName:nil bundle:nil])) { + return nil; + } + + _node = node; + [self _initializeInstance]; + + return self; +} + +- (void)_initializeInstance +{ + if (_node == nil) { + return; + } + + _node.viewControllerRoot = YES; + + _selfConformsToRangeModeProtocol = [self conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]; + _nodeConformsToRangeModeProtocol = [_node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]; + _automaticallyAdjustRangeModeBasedOnViewEvents = _selfConformsToRangeModeProtocol || _nodeConformsToRangeModeProtocol; + + _fallbackAdditionalSafeAreaInsets = UIEdgeInsetsZero; + + // In case the node will get loaded + if (_node.nodeLoaded) { + // Node already loaded the view + [self view]; + } else { + // If the node didn't load yet add ourselves as on did load observer to load the view in case the node gets loaded + // before the view controller + __weak __typeof__(self) weakSelf = self; + [_node onDidLoad:^(__kindof ASDisplayNode * _Nonnull node) { + if ([weakSelf isViewLoaded] == NO) { + [weakSelf view]; + } + }]; + } +} + +- (void)dealloc +{ + ASPerformBackgroundDeallocation(&_node); +} + +- (void)loadView +{ + // Apple applies a frame and autoresizing masks we need. Allocating a view is not + // nearly as expensive as adding and removing it from a hierarchy, and fortunately + // we can avoid that here. Enabling layerBacking on a single node in the hierarchy + // will have a greater performance benefit than the impact of this transient view. + [super loadView]; + + if (_node == nil) { + return; + } + + ASDisplayNodeAssertTrue(!_node.layerBacked); + + UIView *view = self.view; + CGRect frame = view.frame; + UIViewAutoresizing autoresizingMask = view.autoresizingMask; + + // We have what we need, so now create and assign the view we actually want. + view = _node.view; + _node.frame = frame; + _node.autoresizingMask = autoresizingMask; + self.view = view; + + // ensure that self.node has a valid trait collection before a subclass's implementation of viewDidLoad. + // Any subnodes added in viewDidLoad will then inherit the proper environment. + ASPrimitiveTraitCollection traitCollection = [self primitiveTraitCollectionForUITraitCollection:self.traitCollection]; + [self propagateNewTraitCollection:traitCollection]; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + // Before layout, make sure that our trait collection containerSize actually matches the size of our bounds. + // If not, we need to update the traits and propagate them. + + CGSize boundsSize = self.view.bounds.size; + if (CGSizeEqualToSize(self.node.primitiveTraitCollection.containerSize, boundsSize) == NO) { + [UIView performWithoutAnimation:^{ + ASPrimitiveTraitCollection traitCollection = [self primitiveTraitCollectionForUITraitCollection:self.traitCollection]; + traitCollection.containerSize = boundsSize; + + // this method will call measure + [self propagateNewTraitCollection:traitCollection]; + }]; + } else { + // Call layoutThatFits: to let the node prepare for a layout that will happen shortly in the layout pass of the view. + // If the node's constrained size didn't change between the last layout pass it's a no-op + [_node layoutThatFits:[self nodeConstrainedSize]]; + } +} + +- (void)viewDidLayoutSubviews +{ + if (_ensureDisplayed && self.neverShowPlaceholders) { + _ensureDisplayed = NO; + [_node recursivelyEnsureDisplaySynchronously:YES]; + } + [super viewDidLayoutSubviews]; + + if (!AS_AT_LEAST_IOS11) { + [self _updateNodeFallbackSafeArea]; + } +} + +- (void)_updateNodeFallbackSafeArea +{ + UIEdgeInsets safeArea = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0); + UIEdgeInsets additionalInsets = self.additionalSafeAreaInsets; + + safeArea = ASConcatInsets(safeArea, additionalInsets); + + _node.fallbackSafeAreaInsets = safeArea; +} + +ASVisibilityDidMoveToParentViewController; + +- (void)viewWillAppear:(BOOL)animated +{ + as_activity_create_for_scope("ASViewController will appear"); + as_log_debug(ASNodeLog(), "View controller %@ will appear", self); + + [super viewWillAppear:animated]; + + _ensureDisplayed = YES; + + // A layout pass is forced this early to get nodes like ASCollectionNode, ASTableNode etc. + // into the hierarchy before UIKit applies the scroll view inset adjustments, if automatic subnode management + // is enabled. Otherwise the insets would not be applied. + [_node.view layoutIfNeeded]; + + if (_parentManagesVisibilityDepth == NO) { + [self setVisibilityDepth:0]; + } +} + +ASVisibilitySetVisibilityDepth; + +ASVisibilityViewDidDisappearImplementation; + +ASVisibilityDepthImplementation; + +- (void)visibilityDepthDidChange +{ + ASLayoutRangeMode rangeMode = ASLayoutRangeModeForVisibilityDepth(self.visibilityDepth); +#if ASEnableVerboseLogging + NSString *rangeModeString; + switch (rangeMode) { + case ASLayoutRangeModeMinimum: + rangeModeString = @"Minimum"; + break; + + case ASLayoutRangeModeFull: + rangeModeString = @"Full"; + break; + + case ASLayoutRangeModeVisibleOnly: + rangeModeString = @"Visible Only"; + break; + + case ASLayoutRangeModeLowMemory: + rangeModeString = @"Low Memory"; + break; + + default: + break; + } + as_log_verbose(ASNodeLog(), "Updating visibility of %@ to: %@ (visibility depth: %zd)", self, rangeModeString, self.visibilityDepth); +#endif + [self updateCurrentRangeModeWithModeIfPossible:rangeMode]; +} + +#pragma mark - Automatic range mode + +- (BOOL)automaticallyAdjustRangeModeBasedOnViewEvents +{ + return _automaticallyAdjustRangeModeBasedOnViewEvents; +} + +- (void)setAutomaticallyAdjustRangeModeBasedOnViewEvents:(BOOL)automaticallyAdjustRangeModeBasedOnViewEvents +{ + if (automaticallyAdjustRangeModeBasedOnViewEvents != _automaticallyAdjustRangeModeBasedOnViewEvents) { + if (automaticallyAdjustRangeModeBasedOnViewEvents && _selfConformsToRangeModeProtocol == NO && _nodeConformsToRangeModeProtocol == NO) { + NSLog(@"Warning: automaticallyAdjustRangeModeBasedOnViewEvents set to YES in %@, but range mode updating is not possible because neither view controller nor node %@ conform to ASRangeControllerUpdateRangeProtocol.", self, _node); + } + _automaticallyAdjustRangeModeBasedOnViewEvents = automaticallyAdjustRangeModeBasedOnViewEvents; + } +} + +- (void)updateCurrentRangeModeWithModeIfPossible:(ASLayoutRangeMode)rangeMode +{ + if (!_automaticallyAdjustRangeModeBasedOnViewEvents) { + return; + } + + if (_selfConformsToRangeModeProtocol) { + id rangeUpdater = (id)self; + [rangeUpdater updateCurrentRangeWithMode:rangeMode]; + } + + if (_nodeConformsToRangeModeProtocol) { + id rangeUpdater = (id)_node; + [rangeUpdater updateCurrentRangeWithMode:rangeMode]; + } +} + +#pragma mark - Layout Helpers + +- (ASSizeRange)nodeConstrainedSize +{ + return ASSizeRangeMake(self.view.bounds.size); +} + +- (ASInterfaceState)interfaceState +{ + return _node.interfaceState; +} + +- (UIEdgeInsets)additionalSafeAreaInsets +{ + if (AS_AVAILABLE_IOS(11.0)) { + return super.additionalSafeAreaInsets; + } + + return _fallbackAdditionalSafeAreaInsets; +} + +- (void)setAdditionalSafeAreaInsets:(UIEdgeInsets)additionalSafeAreaInsets +{ + if (AS_AVAILABLE_IOS(11.0)) { + [super setAdditionalSafeAreaInsets:additionalSafeAreaInsets]; + } else { + _fallbackAdditionalSafeAreaInsets = additionalSafeAreaInsets; + [self _updateNodeFallbackSafeArea]; + } +} + +#pragma mark - ASTraitEnvironment + +- (ASPrimitiveTraitCollection)primitiveTraitCollectionForUITraitCollection:(UITraitCollection *)traitCollection +{ + if (self.overrideDisplayTraitsWithTraitCollection) { + ASTraitCollection *asyncTraitCollection = self.overrideDisplayTraitsWithTraitCollection(traitCollection); + return [asyncTraitCollection primitiveTraitCollection]; + } + + ASDisplayNodeAssertMainThread(); + ASPrimitiveTraitCollection asyncTraitCollection = ASPrimitiveTraitCollectionFromUITraitCollection(traitCollection); + asyncTraitCollection.containerSize = self.view.frame.size; + return asyncTraitCollection; +} + +- (void)propagateNewTraitCollection:(ASPrimitiveTraitCollection)traitCollection +{ + ASPrimitiveTraitCollection oldTraitCollection = self.node.primitiveTraitCollection; + + if (ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, oldTraitCollection) == NO) { + as_activity_scope_verbose(as_activity_create("Propagate ASViewController trait collection", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_debug(ASNodeLog(), "Propagating new traits for %@: %@", self, NSStringFromASPrimitiveTraitCollection(traitCollection)); + self.node.primitiveTraitCollection = traitCollection; + + NSArray> *children = [self.node sublayoutElements]; + for (id child in children) { + ASTraitCollectionPropagateDown(child, traitCollection); + } + + // Once we've propagated all the traits, layout this node. + // Remeasure the node with the latest constrained size – old constrained size may be incorrect. + as_activity_scope_verbose(as_activity_create("Layout ASViewController node with new traits", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + [_node layoutThatFits:[self nodeConstrainedSize]]; + } +} + +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection +{ + [super traitCollectionDidChange:previousTraitCollection]; + + ASPrimitiveTraitCollection traitCollection = [self primitiveTraitCollectionForUITraitCollection:self.traitCollection]; + traitCollection.containerSize = self.view.bounds.size; + [self propagateNewTraitCollection:traitCollection]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation +{ + [super didRotateFromInterfaceOrientation:fromInterfaceOrientation]; + + ASPrimitiveTraitCollection traitCollection = _node.primitiveTraitCollection; + traitCollection.containerSize = self.view.bounds.size; + [self propagateNewTraitCollection:traitCollection]; +} +#pragma clang diagnostic pop + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASVisibilityProtocols.h b/submodules/AsyncDisplayKit/Source/ASVisibilityProtocols.h new file mode 100644 index 0000000000..837e848269 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASVisibilityProtocols.h @@ -0,0 +1,145 @@ +// +// ASVisibilityProtocols.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UIViewController; + +AS_EXTERN ASLayoutRangeMode ASLayoutRangeModeForVisibilityDepth(NSUInteger visibilityDepth); + +/** + * ASVisibilityDepth + * + * @discussion "Visibility Depth" represents the number of user actions required to make an ASDisplayNode or + * ASViewController visibile. AsyncDisplayKit uses this information to intelligently manage memory and focus + * resources where they are most visible to the user. + * + * The ASVisibilityDepth protocol describes how custom view controllers can integrate with this system. + * + * Parent view controllers should also implement @c ASManagesChildVisibilityDepth + * + * @see ASManagesChildVisibilityDepth + */ + +@protocol ASVisibilityDepth + +/** + * Visibility depth + * + * @discussion Represents the number of user actions necessary to reach the view controller. An increased visibility + * depth indicates a higher number of user interactions for the view controller to be visible again. For example, + * an onscreen navigation controller's top view controller should have a visibility depth of 0. The view controller + * one from the top should have a visibility deptch of 1 as should the root view controller in the stack (because + * the user can hold the back button to pop to the root view controller). + * + * Visibility depth is used to automatically adjust ranges on range controllers (and thus free up memory) and can + * be used to reduce memory usage of other items as well. + */ +- (NSInteger)visibilityDepth; + +/** + * Called when visibility depth changes + * + * @discussion @c visibilityDepthDidChange is called whenever the visibility depth of the represented view controller + * has changed. + * + * If implemented by a view controller container, use this method to notify child view controllers that their view + * depth has changed @see ASNavigationController.m + * + * If implemented on an ASViewController, use this method to reduce or increase the resources that your + * view controller uses. A higher visibility depth view controller should decrease it's resource usage, a lower + * visibility depth controller should pre-warm resources in preperation for a display at 0 depth. + * + * ASViewController implements this method and reduces / increases range mode of supporting nodes (such as ASCollectionNode + * and ASTableNode). + * + * @see visibilityDepth + */ +- (void)visibilityDepthDidChange; + +@end + +/** + * ASManagesChildVisibilityDepth + * + * @discussion A protocol which should be implemented by container view controllers to allow proper + * propagation of visibility depth + * + * @see ASVisibilityDepth + */ +@protocol ASManagesChildVisibilityDepth + +/** + * @abstract Container view controllers should adopt this protocol to indicate that they will manage their child's + * visibilityDepth. For example, ASNavigationController adopts this protocol and manages its childrens visibility + * depth. + * + * If you adopt this protocol, you *must* also emit visibilityDepthDidChange messages to child view controllers. + * + * @param childViewController Expected to return the visibility depth of the child view controller. + */ +- (NSInteger)visibilityDepthOfChildViewController:(UIViewController *)childViewController; + +@end + +#define ASVisibilitySetVisibilityDepth \ +- (void)setVisibilityDepth:(NSUInteger)visibilityDepth \ +{ \ + if (_visibilityDepth == visibilityDepth) { \ + return; \ + } \ + _visibilityDepth = visibilityDepth; \ + [self visibilityDepthDidChange]; \ +} + +#define ASVisibilityDepthImplementation \ +- (NSInteger)visibilityDepth \ +{ \ + if (self.parentViewController && _parentManagesVisibilityDepth == NO) { \ + _parentManagesVisibilityDepth = [self.parentViewController conformsToProtocol:@protocol(ASManagesChildVisibilityDepth)]; \ + } \ + \ + if (_parentManagesVisibilityDepth) { \ + return [(id )self.parentViewController visibilityDepthOfChildViewController:self]; \ + } \ + return _visibilityDepth; \ +} + +#define ASVisibilityViewDidDisappearImplementation \ +- (void)viewDidDisappear:(BOOL)animated \ +{ \ + [super viewDidDisappear:animated]; \ + \ + if (_parentManagesVisibilityDepth == NO) { \ + [self setVisibilityDepth:1]; \ + } \ +} + +#define ASVisibilityViewWillAppear \ +- (void)viewWillAppear:(BOOL)animated \ +{ \ + [super viewWillAppear:animated]; \ + \ + if (_parentManagesVisibilityDepth == NO) { \ + [self setVisibilityDepth:0]; \ + } \ +} + +#define ASVisibilityDidMoveToParentViewController \ +- (void)didMoveToParentViewController:(UIViewController *)parent \ +{ \ + [super didMoveToParentViewController:parent]; \ + _parentManagesVisibilityDepth = NO; \ + [self visibilityDepthDidChange]; \ +} + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASVisibilityProtocols.mm b/submodules/AsyncDisplayKit/Source/ASVisibilityProtocols.mm new file mode 100644 index 0000000000..b13a683bea --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASVisibilityProtocols.mm @@ -0,0 +1,22 @@ +// +// ASVisibilityProtocols.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +ASLayoutRangeMode ASLayoutRangeModeForVisibilityDepth(NSUInteger visibilityDepth) +{ + if (visibilityDepth == 0) { + return ASLayoutRangeModeFull; + } else if (visibilityDepth == 1) { + return ASLayoutRangeModeMinimum; + } else if (visibilityDepth == 2) { + return ASLayoutRangeModeVisibleOnly; + } + return ASLayoutRangeModeLowMemory; +} diff --git a/submodules/AsyncDisplayKit/Source/AsyncDisplayKit+IGListKitMethods.h b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit+IGListKitMethods.h new file mode 100644 index 0000000000..aa0de42510 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit+IGListKitMethods.h @@ -0,0 +1,64 @@ +// +// AsyncDisplayKit+IGListKitMethods.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_IG_LIST_KIT + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * If you are using AsyncDisplayKit with IGListKit, you should use + * these methods to provide implementations for methods like + * -cellForItemAtIndex: that don't apply when used with AsyncDisplayKit. + * + * Your section controllers should also conform to @c ASSectionController and your + * supplementary view sources should conform to @c ASSupplementaryNodeSource. + */ + +AS_SUBCLASSING_RESTRICTED +@interface ASIGListSectionControllerMethods : NSObject + +/** + * Call this for your section controller's @c cellForItemAtIndex: method. + */ ++ (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController; + +/** + * Call this for your section controller's @c sizeForItemAtIndex: method. + */ ++ (CGSize)sizeForItemAtIndex:(NSInteger)index; + +@end + +AS_SUBCLASSING_RESTRICTED +@interface ASIGListSupplementaryViewSourceMethods : NSObject + +/** + * Call this for your supplementary source's @c viewForSupplementaryElementOfKind:atIndex: method. + */ ++ (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind + atIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController; + +/** + * Call this for your supplementary source's @c sizeForSupplementaryViewOfKind:atIndex: method. + */ ++ (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/AsyncDisplayKit+IGListKitMethods.mm b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit+IGListKitMethods.mm new file mode 100644 index 0000000000..50d7a219f8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit+IGListKitMethods.mm @@ -0,0 +1,53 @@ +// +// AsyncDisplayKit+IGListKitMethods.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_IG_LIST_KIT + +#import "AsyncDisplayKit+IGListKitMethods.h" +#import +#import +#import + + +@implementation ASIGListSectionControllerMethods + ++ (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController +{ + // Cast to id for backwards-compatibility until 3.0.0 is officially released – IGListSectionType was removed. This is safe. + return [sectionController.collectionContext dequeueReusableCellOfClass:[_ASCollectionViewCell class] forSectionController:(id)sectionController atIndex:index]; +} + ++ (CGSize)sizeForItemAtIndex:(NSInteger)index +{ + ASDisplayNodeFailAssert(@"Did not expect %@ to be called.", NSStringFromSelector(_cmd)); + return CGSizeZero; +} + +@end + +@implementation ASIGListSupplementaryViewSourceMethods + ++ (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind + atIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController +{ + return [sectionController.collectionContext dequeueReusableSupplementaryViewOfKind:elementKind forSectionController:(id)sectionController class:[_ASCollectionReusableView class] atIndex:index]; +} + ++ (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + ASDisplayNodeFailAssert(@"Did not expect %@ to be called.", NSStringFromSelector(_cmd)); + return CGSizeZero; +} + +@end + +#endif // AS_IG_LIST_KIT diff --git a/submodules/AsyncDisplayKit/Source/AsyncDisplayKit.h b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit.h new file mode 100644 index 0000000000..00a0fb37f1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit.h @@ -0,0 +1,103 @@ +// +// AsyncDisplayKit.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#define MINIMAL_ASDK 1 +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import + +#import + +#import +#import + +#import + +#import + +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import diff --git a/submodules/AsyncDisplayKit/Source/AsyncDisplayKit.modulemap b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit.modulemap new file mode 100644 index 0000000000..fd7d49e620 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/AsyncDisplayKit.modulemap @@ -0,0 +1,19 @@ +framework module AsyncDisplayKit { + umbrella header "AsyncDisplayKit.h" + + export * + module * { + export * + } + + explicit module ASControlNode_Subclasses { + header "ASControlNode+Subclasses.h" + export * + } + + explicit module ASDisplayNode_Subclasses { + header "ASDisplayNode+Subclasses.h" + export * + } + +} diff --git a/submodules/AsyncDisplayKit/Source/Base/ASAssert.h b/submodules/AsyncDisplayKit/Source/Base/ASAssert.h new file mode 100644 index 0000000000..f00ce967d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASAssert.h @@ -0,0 +1,101 @@ +// +// ASAssert.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import +#import +#import + +#if !defined(NS_BLOCK_ASSERTIONS) + #define ASDISPLAYNODE_ASSERTIONS_ENABLED 1 +#else + #define ASDISPLAYNODE_ASSERTIONS_ENABLED 0 +#endif + +/** + * Note: In some cases it would be sufficient to do e.g.: + * ASDisplayNodeAssert(...) NSAssert(__VA_ARGS__) + * but we prefer not to, because we want to match the autocomplete behavior of NSAssert. + * The construction listed above does not show the user what arguments are required and what are optional. + */ + +#define ASDisplayNodeAssert(condition, desc, ...) NSAssert(condition, desc, ##__VA_ARGS__) +#define ASDisplayNodeCAssert(condition, desc, ...) NSCAssert(condition, desc, ##__VA_ARGS__) + +#define ASDisplayNodeAssertNil(condition, desc, ...) ASDisplayNodeAssert((condition) == nil, desc, ##__VA_ARGS__) +#define ASDisplayNodeCAssertNil(condition, desc, ...) ASDisplayNodeCAssert((condition) == nil, desc, ##__VA_ARGS__) + +#define ASDisplayNodeAssertNotNil(condition, desc, ...) ASDisplayNodeAssert((condition) != nil, desc, ##__VA_ARGS__) +#define ASDisplayNodeCAssertNotNil(condition, desc, ...) ASDisplayNodeCAssert((condition) != nil, desc, ##__VA_ARGS__) + +#define ASDisplayNodeAssertImplementedBySubclass() ASDisplayNodeAssert(NO, @"This method must be implemented by subclass %@", [self class]); +#define ASDisplayNodeAssertNotInstantiable() ASDisplayNodeAssert(NO, nil, @"This class is not instantiable."); +#define ASDisplayNodeAssertNotSupported() ASDisplayNodeAssert(NO, nil, @"This method is not supported by class %@", [self class]); + +#define ASDisplayNodeAssertMainThread() ASDisplayNodeAssert(ASMainThreadAssertionsAreDisabled() || 0 != pthread_main_np(), @"This method must be called on the main thread") +#define ASDisplayNodeCAssertMainThread() ASDisplayNodeCAssert(ASMainThreadAssertionsAreDisabled() || 0 != pthread_main_np(), @"This function must be called on the main thread") + +#define ASDisplayNodeAssertNotMainThread() ASDisplayNodeAssert(0 == pthread_main_np(), @"This method must be called off the main thread") +#define ASDisplayNodeCAssertNotMainThread() ASDisplayNodeCAssert(0 == pthread_main_np(), @"This function must be called off the main thread") + +#define ASDisplayNodeAssertFlag(X, desc, ...) ASDisplayNodeAssert((1 == __builtin_popcount(X)), desc, ##__VA_ARGS__) +#define ASDisplayNodeCAssertFlag(X, desc, ...) ASDisplayNodeCAssert((1 == __builtin_popcount(X)), desc, ##__VA_ARGS__) + +#define ASDisplayNodeAssertTrue(condition) ASDisplayNodeAssert((condition), @"Expected %s to be true.", #condition) +#define ASDisplayNodeCAssertTrue(condition) ASDisplayNodeCAssert((condition), @"Expected %s to be true.", #condition) + +#define ASDisplayNodeAssertFalse(condition) ASDisplayNodeAssert(!(condition), @"Expected %s to be false.", #condition) +#define ASDisplayNodeCAssertFalse(condition) ASDisplayNodeCAssert(!(condition), @"Expected %s to be false.", #condition) + +#define ASDisplayNodeFailAssert(desc, ...) ASDisplayNodeAssert(NO, desc, ##__VA_ARGS__) +#define ASDisplayNodeCFailAssert(desc, ...) ASDisplayNodeCAssert(NO, desc, ##__VA_ARGS__) + +#define ASDisplayNodeConditionalAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__) +#define ASDisplayNodeConditionalCAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeCAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__) + +#define ASDisplayNodeCAssertPositiveReal(description, num) ASDisplayNodeCAssert(num >= 0 && num <= CGFLOAT_MAX, @"%@ must be a real positive integer: %f.", description, (CGFloat)num) +#define ASDisplayNodeCAssertInfOrPositiveReal(description, num) ASDisplayNodeCAssert(isinf(num) || (num >= 0 && num <= CGFLOAT_MAX), @"%@ must be infinite or a real positive integer: %f.", description, (CGFloat)num) + +#define ASDisplayNodeCAssertPermanent(object) ASDisplayNodeCAssert(CFGetRetainCount((__bridge CFTypeRef)(object)) == CFGetRetainCount(kCFNull), @"Expected %s to be a permanent object.", #object) +#define ASDisplayNodeErrorDomain @"ASDisplayNodeErrorDomain" +#define ASDisplayNodeNonFatalErrorCode 1 + +/** + * In debug methods, it can be useful to disable main thread assertions to get valuable information, + * even if it means violating threading requirements. These functions are used in -debugDescription and let + * threads decide to suppress/re-enable main thread assertions. + */ +#pragma mark - Main Thread Assertions Disabling + +AS_EXTERN BOOL ASMainThreadAssertionsAreDisabled(void); + +AS_EXTERN void ASPushMainThreadAssertionsDisabled(void); + +AS_EXTERN void ASPopMainThreadAssertionsDisabled(void); + +#pragma mark - Non-Fatal Assertions + +/// Returns YES if assertion passed, NO otherwise. +#define ASDisplayNodeAssertNonFatal(condition, desc, ...) ({ \ + BOOL __evaluated = condition; \ + if (__evaluated == NO) { \ + ASDisplayNodeFailAssert(desc, ##__VA_ARGS__); \ + ASDisplayNodeNonFatalErrorBlock block = [ASDisplayNode nonFatalErrorBlock]; \ + if (block != nil) { \ + NSDictionary *userInfo = nil; \ + if (desc.length > 0) { \ + userInfo = @{ NSLocalizedDescriptionKey : desc }; \ + } \ + NSError *error = [NSError errorWithDomain:ASDisplayNodeErrorDomain code:ASDisplayNodeNonFatalErrorCode userInfo:userInfo]; \ + block(error); \ + } \ + } \ + __evaluated; \ +}) \ diff --git a/submodules/AsyncDisplayKit/Source/Base/ASAssert.m.orig b/submodules/AsyncDisplayKit/Source/Base/ASAssert.m.orig new file mode 100644 index 0000000000..45a3452180 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASAssert.m.orig @@ -0,0 +1,72 @@ +// +// ASAssert.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK +static _Thread_local int tls_mainThreadAssertionsDisabledCount; +#endif +======= +#if AS_TLS_AVAILABLE +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + +static _Thread_local int tls_mainThreadAssertionsDisabledCount; +BOOL ASMainThreadAssertionsAreDisabled() { +#ifdef MINIMAL_ASDK + return false; +#else + return tls_mainThreadAssertionsDisabledCount > 0; +#endif +} + +void ASPushMainThreadAssertionsDisabled() { +#ifndef MINIMAL_ASDK + tls_mainThreadAssertionsDisabledCount += 1; +#endif +} + +void ASPopMainThreadAssertionsDisabled() { +#ifndef MINIMAL_ASDK + tls_mainThreadAssertionsDisabledCount -= 1; + ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +#endif +} + +#else + +#import + +static pthread_key_t ASMainThreadAssertionsDisabledKey() { + static pthread_key_t k; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_key_create(&k, NULL); + }); + return k; +} + +BOOL ASMainThreadAssertionsAreDisabled() { + return (pthread_getspecific(ASMainThreadAssertionsDisabledKey()) > 0); +} + +void ASPushMainThreadAssertionsDisabled() { + let key = ASMainThreadAssertionsDisabledKey(); + let oldVal = pthread_getspecific(key); + pthread_setspecific(key, oldVal + 1); +} + +void ASPopMainThreadAssertionsDisabled() { + let key = ASMainThreadAssertionsDisabledKey(); + let oldVal = pthread_getspecific(key); + pthread_setspecific(key, oldVal - 1); + ASDisplayNodeCAssert(oldVal > 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +} + +#endif // AS_TLS_AVAILABLE diff --git a/submodules/AsyncDisplayKit/Source/Base/ASAssert.mm b/submodules/AsyncDisplayKit/Source/Base/ASAssert.mm new file mode 100644 index 0000000000..6a78b06eaf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASAssert.mm @@ -0,0 +1,58 @@ +// +// ASAssert.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_TLS_AVAILABLE + +static _Thread_local int tls_mainThreadAssertionsDisabledCount; +BOOL ASMainThreadAssertionsAreDisabled() { + return tls_mainThreadAssertionsDisabledCount > 0; +} + +void ASPushMainThreadAssertionsDisabled() { + tls_mainThreadAssertionsDisabledCount += 1; +} + +void ASPopMainThreadAssertionsDisabled() { + tls_mainThreadAssertionsDisabledCount -= 1; + ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +} + +#else + +#import + +static pthread_key_t ASMainThreadAssertionsDisabledKey() { + static pthread_key_t k; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_key_create(&k, NULL); + }); + return k; +} + +BOOL ASMainThreadAssertionsAreDisabled() { + return (nullptr != pthread_getspecific(ASMainThreadAssertionsDisabledKey())); +} + +void ASPushMainThreadAssertionsDisabled() { + const auto key = ASMainThreadAssertionsDisabledKey(); + const auto oldVal = (intptr_t)pthread_getspecific(key); + pthread_setspecific(key, (void *)(oldVal + 1)); +} + +void ASPopMainThreadAssertionsDisabled() { + const auto key = ASMainThreadAssertionsDisabledKey(); + const auto oldVal = (intptr_t)pthread_getspecific(key); + pthread_setspecific(key, (void *)(oldVal - 1)); + ASDisplayNodeCAssert(oldVal > 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +} + +#endif // AS_TLS_AVAILABLE diff --git a/submodules/AsyncDisplayKit/Source/Base/ASAvailability.h b/submodules/AsyncDisplayKit/Source/Base/ASAvailability.h new file mode 100644 index 0000000000..955782f41a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASAvailability.h @@ -0,0 +1,85 @@ +// +// ASAvailability.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#pragma once + +#define AS_TLS_AVAILABLE 0 + +#ifndef AS_ENABLE_TEXTNODE + #define AS_ENABLE_TEXTNODE 1 // Enable old TextNode by default +#endif + +// This needs to stay in sync with Weaver +#ifndef AS_USE_VIDEO + #define AS_USE_VIDEO 0 +#endif + +#ifndef AS_USE_PHOTOS + #define AS_USE_PHOTOS 0 +#endif + +#ifndef AS_USE_MAPKIT + #define AS_USE_MAPKIT 0 +#endif + +#ifndef AS_USE_ASSETS_LIBRARY + #define AS_USE_ASSETS_LIBRARY 0 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_10_0 + #define kCFCoreFoundationVersionNumber_iOS_10_0 1348.00 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_11_0 + #define kCFCoreFoundationVersionNumber_iOS_11_0 1438.10 +#endif + +#ifndef __IPHONE_11_0 + #define __IPHONE_11_0 110000 +#endif + +#define AS_AT_LEAST_IOS10 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_10_0) +#define AS_AT_LEAST_IOS11 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_11_0) + +// Use __builtin_available if we're on Xcode >= 9, AS_AT_LEAST otherwise. +#if __has_builtin(__builtin_available) + #define AS_AVAILABLE_IOS(ver) __builtin_available(iOS ver, *) + #define AS_AVAILABLE_TVOS(ver) __builtin_available(tvOS ver, *) + #define AS_AVAILABLE_IOS_TVOS(ver1, ver2) __builtin_available(iOS ver1, tvOS ver2, *) +#else + #define AS_AVAILABLE_IOS(ver) (TARGET_OS_IOS && AS_AT_LEAST_IOS##ver) + #define AS_AVAILABLE_TVOS(ver) (TARGET_OS_TV && AS_AT_LEAST_IOS##ver) + #define AS_AVAILABLE_IOS_TVOS(ver1, ver2) (AS_AVAILABLE_IOS(ver1) || AS_AVAILABLE_TVOS(ver2)) +#endif + +// If Yoga is available, make it available anywhere we use ASAvailability. +// This reduces Yoga-specific code in other files. +// NOTE: Yoga integration is experimental and not fully tested. Use with caution and test layouts carefully. +#ifndef YOGA_HEADER_PATH + #define YOGA_HEADER_PATH +#endif + +#ifndef YOGA + #define YOGA __has_include(YOGA_HEADER_PATH) +#endif + +#ifdef ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE + #error "ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE is unavailable. See ASConfiguration.h." +#endif + +#define AS_PIN_REMOTE_IMAGE __has_include() +#define AS_IG_LIST_KIT __has_include() + +/** + * For IGListKit versions < 3.0, you have to use IGListCollectionView. + * For 3.0 and later, that class is removed and you use UICollectionView. + */ +#define IG_LIST_COLLECTION_VIEW __has_include() diff --git a/submodules/AsyncDisplayKit/Source/Base/ASBaseDefines.h b/submodules/AsyncDisplayKit/Source/Base/ASBaseDefines.h new file mode 100644 index 0000000000..4e880f9024 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASBaseDefines.h @@ -0,0 +1,258 @@ +// +// ASBaseDefines.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define AS_EXTERN FOUNDATION_EXTERN +#define unowned __unsafe_unretained + +/** + * Hack to support building for iOS with Xcode 9. UIUserInterfaceStyle was previously tvOS-only, + * and it was added to iOS 12. Xcode 9 (iOS 11 SDK) will flat-out refuse to build anything that + * references this enum targeting iOS, even if it's guarded with the right availability macros, + * because it thinks the entire platform isn't compatible with the enum. + */ +#if TARGET_OS_TV || (defined(__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0) +#define AS_BUILD_UIUSERINTERFACESTYLE 1 +#else +#define AS_BUILD_UIUSERINTERFACESTYLE 0 +#endif + +/** + * Decorates methods that clients can implement in categories on our base class. These methods + * will be stubbed with an empty implementation if no implementation is provided. + */ +#define AS_CATEGORY_IMPLEMENTABLE + +#ifdef __GNUC__ +# define ASDISPLAYNODE_GNUC(major, minor) \ +(__GNUC__ > (major) || (__GNUC__ == (major) && __GNUC_MINOR__ >= (minor))) +#else +# define ASDISPLAYNODE_GNUC(major, minor) 0 +#endif + +#ifndef ASDISPLAYNODE_INLINE +# if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +# define ASDISPLAYNODE_INLINE static inline +# elif defined (__MWERKS__) || defined (__cplusplus) +# define ASDISPLAYNODE_INLINE static inline +# elif ASDISPLAYNODE_GNUC (3, 0) +# define ASDISPLAYNODE_INLINE static __inline__ __attribute__ ((always_inline)) +# else +# define ASDISPLAYNODE_INLINE static +# endif +#endif + +#ifndef ASDISPLAYNODE_WARN_DEPRECATED +# define ASDISPLAYNODE_WARN_DEPRECATED 1 +#endif + +#ifndef ASDISPLAYNODE_DEPRECATED +# if ASDISPLAYNODE_GNUC (3, 0) && ASDISPLAYNODE_WARN_DEPRECATED +# define ASDISPLAYNODE_DEPRECATED __attribute__ ((deprecated)) +# else +# define ASDISPLAYNODE_DEPRECATED +# endif +#endif + +#ifndef ASDISPLAYNODE_DEPRECATED_MSG +# if ASDISPLAYNODE_GNUC (3, 0) && ASDISPLAYNODE_WARN_DEPRECATED +# define ASDISPLAYNODE_DEPRECATED_MSG(msg) __deprecated_msg(msg) +# else +# define ASDISPLAYNODE_DEPRECATED_MSG(msg) +# endif +#endif + +#ifndef AS_ENABLE_TIPS +#define AS_ENABLE_TIPS 0 +#endif + +/** + * The event backtraces take a static 2KB of memory + * and retain all objects present in all the registers + * of the stack frames. The memory consumption impact + * is too significant even to be enabled during general + * development. + */ +#ifndef AS_SAVE_EVENT_BACKTRACES +# define AS_SAVE_EVENT_BACKTRACES 0 +#endif + +#ifndef __has_feature // Optional. +#define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef __has_attribute // Optional. +#define __has_attribute(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_RETURNS_RETAINED +#if __has_feature(attribute_ns_returns_retained) +#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained)) +#else +#define NS_RETURNS_RETAINED +#endif +#endif + +#ifndef CF_RETURNS_RETAINED +#if __has_feature(attribute_cf_returns_retained) +#define CF_RETURNS_RETAINED __attribute__((cf_returns_retained)) +#else +#define CF_RETURNS_RETAINED +#endif +#endif + +#ifndef ASDISPLAYNODE_REQUIRES_SUPER +#if __has_attribute(objc_requires_super) +#define ASDISPLAYNODE_REQUIRES_SUPER __attribute__((objc_requires_super)) +#else +#define ASDISPLAYNODE_REQUIRES_SUPER +#endif +#endif + +#ifndef AS_UNAVAILABLE +#if __has_attribute(unavailable) +#define AS_UNAVAILABLE(message) __attribute__((unavailable(message))) +#else +#define AS_UNAVAILABLE(message) +#endif +#endif + +#ifndef AS_WARN_UNUSED_RESULT +#if __has_attribute(warn_unused_result) +#define AS_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +#define AS_WARN_UNUSED_RESULT +#endif +#endif + +#define ASOVERLOADABLE __attribute__((overloadable)) + + +#if __has_attribute(noescape) +#define AS_NOESCAPE __attribute__((noescape)) +#else +#define AS_NOESCAPE +#endif + +#if __has_attribute(objc_subclassing_restricted) +#define AS_SUBCLASSING_RESTRICTED __attribute__((objc_subclassing_restricted)) +#else +#define AS_SUBCLASSING_RESTRICTED +#endif + +#define AS_ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + +#define ASCreateOnce(expr) ({ \ + static dispatch_once_t onceToken; \ + static __typeof__(expr) staticVar; \ + dispatch_once(&onceToken, ^{ \ + staticVar = expr; \ + }); \ + staticVar; \ +}) + +/// Ensure that class is of certain kind +#define ASDynamicCast(x, c) ({ \ + id __val = x;\ + ((c *) ([__val isKindOfClass:[c class]] ? __val : nil));\ +}) + +/// Ensure that class is of certain kind, assuming it is subclass restricted +#define ASDynamicCastStrict(x, c) ({ \ + id __val = x;\ + ((c *) ([__val class] == [c class] ? __val : nil));\ +}) + +// Compare two primitives, assign if different. Returns whether the assignment happened. +#define ASCompareAssign(lvalue, newValue) ({ \ + BOOL result = (lvalue != newValue); \ + if (result) { lvalue = newValue; } \ + result; \ +}) + +#define ASCompareAssignObjects(lvalue, newValue) \ + ASCompareAssignCustom(lvalue, newValue, ASObjectIsEqual) + +// e.g. ASCompareAssignCustom(_myInsets, insets, UIEdgeInsetsEqualToEdgeInsets) +#define ASCompareAssignCustom(lvalue, newValue, isequal) ({ \ + BOOL result = !(isequal(lvalue, newValue)); \ + if (result) { lvalue = newValue; } \ + result; \ +}) + +#define ASCompareAssignCopy(lvalue, newValue) ({ \ + BOOL result = !ASObjectIsEqual(lvalue, newValue); \ + if (result) { lvalue = [newValue copyWithZone:NULL]; } \ + result; \ +}) + +/** + * Create a new set by mapping `collection` over `work`, ignoring nil. + */ +#define ASSetByFlatMapping(collection, decl, work) ({ \ + NSMutableSet *s = [[NSMutableSet alloc] init]; \ + for (decl in collection) {\ + id result = work; \ + if (result != nil) { \ + [s addObject:result]; \ + } \ + } \ + s; \ +}) + +/** + * Create a new ObjectPointerPersonality NSHashTable by mapping `collection` over `work`, ignoring nil. + * + * capacity: 0 is taken from +hashTableWithOptions. + */ +#define ASPointerTableByFlatMapping(collection, decl, work) ({ \ + NSHashTable *t = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:0]; \ + for (decl in collection) {\ + id result = work; \ + if (result != nil) { \ + [t addObject:result]; \ + } \ + } \ + t; \ +}) + +/** + * Create a new array by mapping `collection` over `work`, ignoring nil. + */ +#define ASArrayByFlatMapping(collectionArg, decl, work) ({ \ + id __collection = collectionArg; \ + NSArray *__result; \ + if (__collection) { \ + id __buf[[__collection count]]; \ + NSUInteger __i = 0; \ + for (decl in __collection) {\ + if ((__buf[__i] = work)) { \ + __i++; \ + } \ + } \ + __result = [NSArray arrayByTransferring:__buf count:__i]; \ + } \ + __result; \ +}) + +/** + * Capture-and-clear a strong reference without the intervening retain/release pair. + * + * E.g. const auto localVar = ASTransferStrong(_myIvar); + * Post-condition: localVar has the strong value from _myIvar and _myIvar is nil. + * No retain/release is emitted when the optimizer is on. + */ +#define ASTransferStrong(lvalue) ({ \ + CFTypeRef *__rawPtr = (CFTypeRef *)(void *)(&(lvalue)); \ + CFTypeRef __cfValue = *__rawPtr; \ + *__rawPtr = NULL; \ + __typeof(lvalue) __result = (__bridge_transfer __typeof(lvalue))__cfValue; \ + __result; \ +}) diff --git a/submodules/AsyncDisplayKit/Source/Base/ASDisplayNode+Ancestry.h b/submodules/AsyncDisplayKit/Source/Base/ASDisplayNode+Ancestry.h new file mode 100644 index 0000000000..2c61cee00c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASDisplayNode+Ancestry.h @@ -0,0 +1,55 @@ +// +// ASDisplayNode+Ancestry.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASDisplayNode (Ancestry) + +/** + * Returns an object to enumerate the supernode ancestry of this node, starting with its supernode. + * + * For instance, you could write: + * for (ASDisplayNode *node in self.supernodes) { + * if ([node.backgroundColor isEqual:[UIColor blueColor]]) { + * node.hidden = YES; + * } + * } + * + * Note: If this property is read on the main thread, the enumeration will attempt to go up + * the layer hierarchy if it finds a break in the display node hierarchy. + */ +@property (readonly) id supernodes; + +/** + * Same as `supernodes` but begins the enumeration with self. + */ +@property (readonly) id supernodesIncludingSelf; + +/** + * Searches the supernodes of this node for one matching the given class. + * + * @param supernodeClass The class of node you're looking for. + * @param includeSelf Whether to include self in the search. + * @return A node of the given class that is an ancestor of this node, or nil. + * + * @note See the documentation on `supernodes` for details about the upward traversal. + */ +- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf; + +/** + * e.g. "(, , )" + */ +@property (copy, readonly) NSString *ancestryDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Base/ASDisplayNode+Ancestry.mm b/submodules/AsyncDisplayKit/Source/Base/ASDisplayNode+Ancestry.mm new file mode 100644 index 0000000000..7003064c70 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASDisplayNode+Ancestry.mm @@ -0,0 +1,90 @@ +// +// ASDisplayNode+Ancestry.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASDisplayNode+Ancestry.h" +#import +#import + +AS_SUBCLASSING_RESTRICTED +@interface ASNodeAncestryEnumerator : NSEnumerator +@end + +@implementation ASNodeAncestryEnumerator { + ASDisplayNode *_lastNode; // This needs to be strong because enumeration will not retain the current batch of objects + BOOL _initialState; +} + +- (instancetype)initWithNode:(ASDisplayNode *)node +{ + if (self = [super init]) { + _initialState = YES; + _lastNode = node; + } + return self; +} + +- (id)nextObject +{ + if (_initialState) { + _initialState = NO; + return _lastNode; + } + + ASDisplayNode *nextNode = _lastNode.supernode; + if (nextNode == nil && ASDisplayNodeThreadIsMain()) { + CALayer *layer = _lastNode.nodeLoaded ? _lastNode.layer.superlayer : nil; + while (layer != nil) { + nextNode = ASLayerToDisplayNode(layer); + if (nextNode != nil) { + break; + } + layer = layer.superlayer; + } + } + _lastNode = nextNode; + return nextNode; +} + +@end + +@implementation ASDisplayNode (Ancestry) + +- (id)supernodes +{ + NSEnumerator *result = [[ASNodeAncestryEnumerator alloc] initWithNode:self]; + [result nextObject]; // discard first object (self) + return result; +} + +- (id)supernodesIncludingSelf +{ + return [[ASNodeAncestryEnumerator alloc] initWithNode:self]; +} + +- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf +{ + id chain = includeSelf ? self.supernodesIncludingSelf : self.supernodes; + for (ASDisplayNode *ancestor in chain) { + if ([ancestor isKindOfClass:supernodeClass]) { + return ancestor; + } + } + return nil; +} + +- (NSString *)ancestryDescription +{ + NSMutableArray *strings = [NSMutableArray array]; + for (ASDisplayNode *node in self.supernodes) { + [strings addObject:ASObjectDescriptionMakeTiny(node)]; + } + return strings.description; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Base/ASEqualityHelpers.h b/submodules/AsyncDisplayKit/Source/Base/ASEqualityHelpers.h new file mode 100644 index 0000000000..602459547b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASEqualityHelpers.h @@ -0,0 +1,21 @@ +// +// ASEqualityHelpers.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + @abstract Correctly equates two objects, including cases where both objects are nil. The latter is a case where `isEqual:` fails. + @param obj The first object in the comparison. Can be nil. + @param otherObj The second object in the comparison. Can be nil. + @result YES if the objects are equal, including cases where both object are nil. + */ +ASDISPLAYNODE_INLINE BOOL ASObjectIsEqual(id obj, id otherObj) +{ + return obj == otherObj || [obj isEqual:otherObj]; +} diff --git a/submodules/AsyncDisplayKit/Source/Base/ASLog.h b/submodules/AsyncDisplayKit/Source/Base/ASLog.h new file mode 100644 index 0000000000..ccab4b8e58 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASLog.h @@ -0,0 +1,163 @@ +// +// ASLog.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import + +#ifndef ASEnableVerboseLogging + #define ASEnableVerboseLogging 0 +#endif + +/** + * Disable all logging. + * + * You should only use this function if the default log level is + * annoying during development. By default, logging is run at + * the appropriate system log level (see the os_log_* functions), + * so you do not need to worry generally about the performance + * implications of log messages. + * + * For example, virtually all log messages generated by Texture + * are at the `debug` log level, which the system + * disables in production. + */ +AS_EXTERN void ASDisableLogging(void); + +/** + * Restore logging that has been runtime-disabled via ASDisableLogging(). + * + * Logging can be disabled at runtime using the ASDisableLogging() function. + * This command restores logging to the level provided in the build + * configuration. This can be used in conjunction with ASDisableLogging() + * to allow logging to be toggled off and back on at runtime. + */ +AS_EXTERN void ASEnableLogging(void); + +/// Log for general node events e.g. interfaceState, didLoad. +#define ASNodeLogEnabled 1 +AS_EXTERN os_log_t ASNodeLog(void); + +/// Log for layout-specific events e.g. calculateLayout. +#define ASLayoutLogEnabled 1 +AS_EXTERN os_log_t ASLayoutLog(void); + +/// Log for display-specific events e.g. display queue batches. +#define ASDisplayLogEnabled 1 +AS_EXTERN os_log_t ASDisplayLog(void); + +/// Log for collection events e.g. reloadData, performBatchUpdates. +#define ASCollectionLogEnabled 1 +AS_EXTERN os_log_t ASCollectionLog(void); + +/// Log for ASNetworkImageNode and ASMultiplexImageNode events. +#define ASImageLoadingLogEnabled 1 +AS_EXTERN os_log_t ASImageLoadingLog(void); + +/// Specialized log for our main thread deallocation trampoline. +#define ASMainThreadDeallocationLogEnabled 0 +AS_EXTERN os_log_t ASMainThreadDeallocationLog(void); + +/** + * The activity tracing system changed a lot between iOS 9 and 10. + * In iOS 10, the system was merged with logging and became much more powerful + * and adopted a new API. + * + * The legacy API is visible, but its functionality is extremely limited and the API is so different + * that we don't bother with it. For example, activities described by os_activity_start/end are not + * reflected in the log whereas activities described by the newer + * os_activity_scope are. So unfortunately we must use these iOS 10 + * APIs to get meaningful logging data. + */ +#if OS_LOG_TARGET_HAS_10_12_FEATURES + +#define OS_ACTIVITY_NULLABLE nullable +#define AS_ACTIVITY_CURRENT OS_ACTIVITY_CURRENT +#define as_activity_scope(activity) os_activity_scope(activity) +#define as_activity_apply(activity, block) os_activity_apply(activity, block) +#define as_activity_create(description, parent_activity, flags) os_activity_create(description, parent_activity, flags) +#define as_activity_scope_enter(activity, statePtr) os_activity_scope_enter(activity, statePtr) +#define as_activity_scope_leave(statePtr) os_activity_scope_leave(statePtr) +#define as_activity_get_identifier(activity, outParentID) os_activity_get_identifier(activity, outParentID) + +#else + +#define OS_ACTIVITY_NULLABLE +#define AS_ACTIVITY_CURRENT OS_ACTIVITY_NULL +#define as_activity_scope(activity) +#define as_activity_apply(activity, block) +#define as_activity_create(description, parent_activity, flags) OS_ACTIVITY_NULL +#define as_activity_scope_enter(activity, statePtr) +#define as_activity_scope_leave(statePtr) +#define as_activity_get_identifier(activity, outParentID) (os_activity_id_t)0 + +#endif // OS_LOG_TARGET_HAS_10_12_FEATURES + +// Create activities only when verbose enabled. Doesn't materially impact performance, but good if we're cluttering up +// activity scopes and reducing readability. +#if ASEnableVerboseLogging + #define as_activity_scope_verbose(activity) as_activity_scope(activity) +#else + #define as_activity_scope_verbose(activity) +#endif + +// Convenience for: as_activity_scope(as_activity_create(description, AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)) +#define as_activity_create_for_scope(description) \ + as_activity_scope(as_activity_create(description, AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)) + +/** + * The logging macros are not guarded by deployment-target checks like the activity macros are, but they are + * only available on iOS >= 9 at runtime, so just make them conditional. + */ + +#define as_log_create(subsystem, category) ({ \ +os_log_t __val; \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + __val = os_log_create(subsystem, category); \ +} else { \ + __val = (os_log_t)0; \ +} \ +__val; \ +}) + +#define as_log_debug(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_debug(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_info(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_info(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_error(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_error(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_fault(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_fault(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#if ASEnableVerboseLogging + #define as_log_verbose(log, format, ...) as_log_debug(log, format, ##__VA_ARGS__) +#else + #define as_log_verbose(log, format, ...) +#endif diff --git a/submodules/AsyncDisplayKit/Source/Base/ASLog.mm b/submodules/AsyncDisplayKit/Source/Base/ASLog.mm new file mode 100644 index 0000000000..270246454b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASLog.mm @@ -0,0 +1,48 @@ +// +// ASLog.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +static atomic_bool __ASLogEnabled = ATOMIC_VAR_INIT(YES); + +void ASDisableLogging() { + atomic_store(&__ASLogEnabled, NO); +} + +void ASEnableLogging() { + atomic_store(&__ASLogEnabled, YES); +} + +ASDISPLAYNODE_INLINE BOOL ASLoggingIsEnabled() { + return atomic_load(&__ASLogEnabled); +} + +os_log_t ASNodeLog() { + return (ASNodeLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Node")) : OS_LOG_DISABLED; +} + +os_log_t ASLayoutLog() { + return (ASLayoutLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Layout")) : OS_LOG_DISABLED; +} + +os_log_t ASCollectionLog() { + return (ASCollectionLogEnabled && ASLoggingIsEnabled()) ?ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Collection")) : OS_LOG_DISABLED; +} + +os_log_t ASDisplayLog() { + return (ASDisplayLogEnabled && ASLoggingIsEnabled()) ?ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Display")) : OS_LOG_DISABLED; +} + +os_log_t ASImageLoadingLog() { + return (ASImageLoadingLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "ImageLoading")) : OS_LOG_DISABLED; +} + +os_log_t ASMainThreadDeallocationLog() { + return (ASMainThreadDeallocationLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "MainDealloc")) : OS_LOG_DISABLED; +} diff --git a/submodules/AsyncDisplayKit/Source/Base/ASSignpost.h b/submodules/AsyncDisplayKit/Source/Base/ASSignpost.h new file mode 100644 index 0000000000..a841794417 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Base/ASSignpost.h @@ -0,0 +1,94 @@ +// +// ASSignpost.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +/// The signposts we use. Signposts are grouped by color. The SystemTrace.tracetemplate file +/// should be kept up-to-date with these values. +typedef NS_ENUM(uint32_t, ASSignpostName) { + // Collection/Table (Blue) + ASSignpostDataControllerBatch = 300, // Alloc/layout nodes before collection update. + ASSignpostRangeControllerUpdate, // Ranges update pass. + ASSignpostCollectionUpdate, // Entire update process, from -endUpdates to [super perform…] + + // Rendering (Green) + ASSignpostLayerDisplay = 325, // Client display callout. + ASSignpostRunLoopQueueBatch, // One batch of ASRunLoopQueue. + + // Layout (Purple) + ASSignpostCalculateLayout = 350, // Start of calculateLayoutThatFits to end. Max 1 per thread. + + // Misc (Orange) + ASSignpostDeallocQueueDrain = 375, // One chunk of dealloc queue work. arg0 is count. + ASSignpostCATransactionLayout, // The CA transaction commit layout phase. + ASSignpostCATransactionCommit // The CA transaction commit post-layout phase. +}; + +typedef NS_ENUM(uintptr_t, ASSignpostColor) { + ASSignpostColorBlue, + ASSignpostColorGreen, + ASSignpostColorPurple, + ASSignpostColorOrange, + ASSignpostColorRed, + ASSignpostColorDefault +}; + +static inline ASSignpostColor ASSignpostGetColor(ASSignpostName name, ASSignpostColor colorPref) { + if (colorPref == ASSignpostColorDefault) { + return (ASSignpostColor)((name / 25) % 4); + } else { + return colorPref; + } +} + +#if defined(PROFILE) && __has_include() + #define AS_KDEBUG_ENABLE 1 +#else + #define AS_KDEBUG_ENABLE 0 +#endif + +#if AS_KDEBUG_ENABLE + +#import + +// These definitions are required to build the backward-compatible kdebug trace +// on the iOS 10 SDK. The kdebug_trace function crashes if run on iOS 9 and earlier. +// It's valuable to support trace signposts on iOS 9, because A5 devices don't support iOS 10. +#ifndef DBG_MACH_CHUD +#define DBG_MACH_CHUD 0x0A +#define DBG_FUNC_NONE 0 +#define DBG_FUNC_START 1 +#define DBG_FUNC_END 2 +#define DBG_APPS 33 +#define SYS_kdebug_trace 180 +#define KDBG_CODE(Class, SubClass, code) (((Class & 0xff) << 24) | ((SubClass & 0xff) << 16) | ((code & 0x3fff) << 2)) +#define APPSDBG_CODE(SubClass,code) KDBG_CODE(DBG_APPS, SubClass, code) +#endif + +// Currently we'll reserve arg3. +#define ASSignpost(name, identifier, arg2, color) \ +AS_AT_LEAST_IOS10 ? kdebug_signpost(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)) \ +: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_NONE, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)); + +#define ASSignpostStartCustom(name, identifier, arg2) \ +AS_AT_LEAST_IOS10 ? kdebug_signpost_start(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, 0) \ +: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_START, (uintptr_t)identifier, (uintptr_t)arg2, 0, 0); +#define ASSignpostStart(name) ASSignpostStartCustom(name, self, 0) + +#define ASSignpostEndCustom(name, identifier, arg2, color) \ +AS_AT_LEAST_IOS10 ? kdebug_signpost_end(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)) \ +: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_END, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)); +#define ASSignpostEnd(name) ASSignpostEndCustom(name, self, 0, ASSignpostColorDefault) + +#else + +#define ASSignpost(name, identifier, arg2, color) +#define ASSignpostStartCustom(name, identifier, arg2) +#define ASSignpostStart(name) +#define ASSignpostEndCustom(name, identifier, arg2, color) +#define ASSignpostEnd(name) + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Debug.h b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Debug.h new file mode 100644 index 0000000000..2f0826289a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Debug.h @@ -0,0 +1,55 @@ +// +// AsyncDisplayKit+Debug.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASImageNode (Debugging) + +/** + * Enables an ASImageNode debug label that shows the ratio of pixels in the source image to those in + * the displayed bounds (including cropRect). This helps detect excessive image fetching / downscaling, + * as well as upscaling (such as providing a URL not suitable for a Retina device). For dev purposes only. + * Specify YES to show the label on all ASImageNodes with non-1.0x source-to-bounds pixel ratio. + */ +@property (class, nonatomic) BOOL shouldShowImageScalingOverlay; + +@end + +@interface ASControlNode (Debugging) + +/** + * Class method to enable a visualization overlay of the tappable area on the ASControlNode. For app debugging purposes only. + * NOTE: GESTURE RECOGNIZERS, (including tap gesture recognizers on a control node) WILL NOT BE VISUALIZED!!! + * Overlay = translucent GREEN color, + * edges that are clipped by the tappable area of any parent (their bounds + hitTestSlop) in the hierarchy = DARK GREEN BORDERED EDGE, + * edges that are clipped by clipToBounds = YES of any parent in the hierarchy = ORANGE BORDERED EDGE (may still receive touches beyond + * overlay rect, but can't be visualized). + * Specify YES to make this debug feature enabled when messaging the ASControlNode class. + */ +@property (class, nonatomic) BOOL enableHitTestDebug; + +@end + +#ifndef MINIMAL_ASDK +@interface ASDisplayNode (RangeDebugging) + +/** + * Enable a visualization overlay of the all table/collection tuning parameters. For dev purposes only. + * To use, set this in the AppDelegate --> ASDisplayNode.shouldShowRangeDebugOverlay = YES + */ +@property (class, nonatomic) BOOL shouldShowRangeDebugOverlay; + +@end +#endif + + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Debug.mm b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Debug.mm new file mode 100644 index 0000000000..f527098dcc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Debug.mm @@ -0,0 +1,758 @@ +// +// AsyncDisplayKit+Debug.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + + +#pragma mark - ASImageNode (Debugging) + +static BOOL __shouldShowImageScalingOverlay = NO; + +@implementation ASImageNode (Debugging) + ++ (void)setShouldShowImageScalingOverlay:(BOOL)show; +{ + __shouldShowImageScalingOverlay = show; +} + ++ (BOOL)shouldShowImageScalingOverlay +{ + return __shouldShowImageScalingOverlay; +} + +@end + +#pragma mark - ASControlNode (DebuggingInternal) + +static BOOL __enableHitTestDebug = NO; + +@interface ASControlNode (DebuggingInternal) + +- (ASImageNode *)debugHighlightOverlay; + +@end + +@implementation ASControlNode (Debugging) + ++ (void)setEnableHitTestDebug:(BOOL)enable +{ + __enableHitTestDebug = enable; +} + ++ (BOOL)enableHitTestDebug +{ + return __enableHitTestDebug; +} + +// layout method required ONLY when hitTestDebug is enabled +- (void)layout +{ + [super layout]; + + if ([ASControlNode enableHitTestDebug]) { + + // Construct hitTestDebug highlight overlay frame indicating tappable area of a node, which can be restricted by two things: + + // (1) Any parent's tapable area (its own bounds + hitTestSlop) may restrict the desired tappable area expansion using + // hitTestSlop of a child as UIKit event delivery (hitTest:) will not search sub-hierarchies if one of our parents does + // not return YES for pointInside:. To circumvent this restriction, a developer will need to set / adjust the hitTestSlop + // on the limiting parent. This is indicated in the overlay by a dark GREEN edge. This is an ACTUAL restriction. + + // (2) Any parent's .clipToBounds. If a parent is clipping, we cannot show the overlay outside that area + // (although it still respond to touch). To indicate that the overlay cannot accurately display the true tappable area, + // the overlay will have an ORANGE edge. This is a VISUALIZATION restriction. + + CGRect intersectRect = UIEdgeInsetsInsetRect(self.bounds, [self hitTestSlop]); + UIRectEdge clippedEdges = UIRectEdgeNone; + UIRectEdge clipsToBoundsClippedEdges = UIRectEdgeNone; + CALayer *layer = self.layer; + CALayer *intersectLayer = layer; + CALayer *intersectSuperlayer = layer.superlayer; + + // FIXED: Stop climbing hierarchy if UIScrollView is encountered (its offset bounds origin may make it seem like our events + // will be clipped when scrolling will actually reveal them (because this process will not re-run due to scrolling)) + while (intersectSuperlayer && ![intersectSuperlayer.delegate respondsToSelector:@selector(contentOffset)]) { + + // Get parent's tappable area + CGRect parentHitRect = intersectSuperlayer.bounds; + BOOL parentClipsToBounds = NO; + + // If parent is a node, tappable area may be expanded by hitTestSlop + ASDisplayNode *parentNode = ASLayerToDisplayNode(intersectSuperlayer); + if (parentNode) { + UIEdgeInsets parentSlop = [parentNode hitTestSlop]; + + // If parent has hitTestSlop, expand tappable area (if parent doesn't clipToBounds) + if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, parentSlop)) { + parentClipsToBounds = parentNode.clipsToBounds; + if (!parentClipsToBounds) { + parentHitRect = UIEdgeInsetsInsetRect(parentHitRect, [parentNode hitTestSlop]); + } + } + } + + // Convert our current rect to parent coordinates + CGRect intersectRectInParentCoordinates = [intersectSuperlayer convertRect:intersectRect fromLayer:intersectLayer]; + + // Intersect rect with the parent's tappable area rect + intersectRect = CGRectIntersection(parentHitRect, intersectRectInParentCoordinates); + if (!CGSizeEqualToSize(parentHitRect.size, intersectRectInParentCoordinates.size)) { + clippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates + parentRect:parentHitRect rectEdge:clippedEdges]; + if (parentClipsToBounds) { + clipsToBoundsClippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates + parentRect:parentHitRect rectEdge:clipsToBoundsClippedEdges]; + } + } + + // move up hierarchy + intersectLayer = intersectSuperlayer; + intersectSuperlayer = intersectLayer.superlayer; + } + + // produce final overlay image (or fill background if edges aren't restricted) + CGRect finalRect = [intersectLayer convertRect:intersectRect toLayer:layer]; + UIColor *fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.4]; + + ASImageNode *debugOverlay = [self debugHighlightOverlay]; + + // determine if edges are clipped and if so, highlight the restricted edges + if (clippedEdges == UIRectEdgeNone) { + debugOverlay.backgroundColor = fillColor; + } else { + const CGFloat borderWidth = 2.0; + UIColor *borderColor = [[UIColor orangeColor] colorWithAlphaComponent:0.8]; + UIColor *clipsBorderColor = [UIColor colorWithRed:30/255.0 green:90/255.0 blue:50/255.0 alpha:0.7]; + CGRect imgRect = CGRectMake(0, 0, 2.0 * borderWidth + 1.0, 2.0 * borderWidth + 1.0); + + ASGraphicsBeginImageContextWithOptions(imgRect.size, NO, 1); + + [fillColor setFill]; + UIRectFill(imgRect); + + [self drawEdgeIfClippedWithEdges:clippedEdges color:clipsBorderColor borderWidth:borderWidth imgRect:imgRect]; + [self drawEdgeIfClippedWithEdges:clipsToBoundsClippedEdges color:borderColor borderWidth:borderWidth imgRect:imgRect]; + + UIImage *debugHighlightImage = ASGraphicsGetImageAndEndCurrentContext(); + + UIEdgeInsets edgeInsets = UIEdgeInsetsMake(borderWidth, borderWidth, borderWidth, borderWidth); + debugOverlay.image = [debugHighlightImage resizableImageWithCapInsets:edgeInsets resizingMode:UIImageResizingModeStretch]; + debugOverlay.backgroundColor = nil; + } + + debugOverlay.frame = finalRect; + } +} + +- (UIRectEdge)setEdgesOfIntersectionForChildRect:(CGRect)childRect parentRect:(CGRect)parentRect rectEdge:(UIRectEdge)rectEdge +{ + // determine which edges of childRect are outside parentRect (and thus will be clipped) + if (childRect.origin.y < parentRect.origin.y) { + rectEdge |= UIRectEdgeTop; + } + if (childRect.origin.x < parentRect.origin.x) { + rectEdge |= UIRectEdgeLeft; + } + if (CGRectGetMaxY(childRect) > CGRectGetMaxY(parentRect)) { + rectEdge |= UIRectEdgeBottom; + } + if (CGRectGetMaxX(childRect) > CGRectGetMaxX(parentRect)) { + rectEdge |= UIRectEdgeRight; + } + + return rectEdge; +} + +- (void)drawEdgeIfClippedWithEdges:(UIRectEdge)rectEdge color:(UIColor *)color borderWidth:(CGFloat)borderWidth imgRect:(CGRect)imgRect +{ + [color setFill]; + + // highlight individual edges of overlay if edge is restricted by parentRect + // so that the developer is aware that increasing hitTestSlop will not result in an expanded tappable area + if (rectEdge & UIRectEdgeTop) { + UIRectFill(CGRectMake(0.0, 0.0, imgRect.size.width, borderWidth)); + } + if (rectEdge & UIRectEdgeLeft) { + UIRectFill(CGRectMake(0.0, 0.0, borderWidth, imgRect.size.height)); + } + if (rectEdge & UIRectEdgeBottom) { + UIRectFill(CGRectMake(0.0, imgRect.size.height - borderWidth, imgRect.size.width, borderWidth)); + } + if (rectEdge & UIRectEdgeRight) { + UIRectFill(CGRectMake(imgRect.size.width - borderWidth, 0.0, borderWidth, imgRect.size.height)); + } +} + +@end + +#pragma mark - ASRangeController (Debugging) + +#ifndef MINIMAL_ASDK + +@interface _ASRangeDebugOverlayView : UIView + ++ (instancetype)sharedInstance NS_RETURNS_RETAINED; + +- (void)addRangeController:(ASRangeController *)rangeController; + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters + interfaceState:(ASInterfaceState)interfaceState; + +@end + +@interface _ASRangeDebugBarView : UIView + +@property (nonatomic, weak) ASRangeController *rangeController; +@property (nonatomic) BOOL destroyOnLayout; +@property (nonatomic) NSString *debugString; + +- (instancetype)initWithRangeController:(ASRangeController *)rangeController; + +- (void)updateWithVisibleRatio:(CGFloat)visibleRatio + displayRatio:(CGFloat)displayRatio + leadingDisplayRatio:(CGFloat)leadingDisplayRatio + preloadRatio:(CGFloat)preloadRatio + leadingpreloadRatio:(CGFloat)leadingpreloadRatio + direction:(ASScrollDirection)direction; + +@end + +static BOOL __shouldShowRangeDebugOverlay = NO; + +@implementation ASDisplayNode (RangeDebugging) + ++ (void)setShouldShowRangeDebugOverlay:(BOOL)show +{ + __shouldShowRangeDebugOverlay = show; +} + ++ (BOOL)shouldShowRangeDebugOverlay +{ + return __shouldShowRangeDebugOverlay; +} + +@end + +@implementation ASRangeController (DebugInternal) + ++ (void)layoutDebugOverlayIfNeeded +{ + [[_ASRangeDebugOverlayView sharedInstance] setNeedsLayout]; +} + +- (void)addRangeControllerToRangeDebugOverlay +{ + [[_ASRangeDebugOverlayView sharedInstance] addRangeController:self]; +} + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters + interfaceState:(ASInterfaceState)interfaceState +{ + [[_ASRangeDebugOverlayView sharedInstance] updateRangeController:controller + withScrollableDirections:scrollableDirections + scrollDirection:direction + rangeMode:mode + displayTuningParameters:displayTuningParameters + preloadTuningParameters:preloadTuningParameters + interfaceState:interfaceState]; +} + +@end + + +#pragma mark _ASRangeDebugOverlayView + +@interface _ASRangeDebugOverlayView () +@end + +@implementation _ASRangeDebugOverlayView +{ + NSMutableArray *_rangeControllerViews; + NSInteger _newControllerCount; + NSInteger _removeControllerCount; + BOOL _animating; +} + ++ (UIWindow *)keyWindow +{ + // hack to work around app extensions not having UIApplication...not sure of a better way to do this? + return [[NSClassFromString(@"UIApplication") sharedApplication] keyWindow]; +} + ++ (_ASRangeDebugOverlayView *)sharedInstance NS_RETURNS_RETAINED +{ + static _ASRangeDebugOverlayView *__rangeDebugOverlay = nil; + + if (!__rangeDebugOverlay && ASDisplayNode.shouldShowRangeDebugOverlay) { + __rangeDebugOverlay = [[self alloc] initWithFrame:CGRectZero]; + [[self keyWindow] addSubview:__rangeDebugOverlay]; + } + + return __rangeDebugOverlay; +} + +#define OVERLAY_INSET 10 +#define OVERLAY_SCALE 3 +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + _rangeControllerViews = [[NSMutableArray alloc] init]; + self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + self.layer.zPosition = 1000; + self.clipsToBounds = YES; + + CGSize windowSize = [[[self class] keyWindow] bounds].size; + self.frame = CGRectMake(windowSize.width - (windowSize.width / OVERLAY_SCALE) - OVERLAY_INSET, windowSize.height - OVERLAY_INSET, + windowSize.width / OVERLAY_SCALE, 0.0); + + UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(rangeDebugOverlayWasPanned:)]; + [self addGestureRecognizer:panGR]; + } + + return self; +} + +#define BAR_THICKNESS 24 + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + [self layoutToFitAllBarsExcept:0]; + } completion:^(BOOL finished) { + + }]; +} + +- (void)layoutToFitAllBarsExcept:(NSInteger)barsToClip +{ + CGSize boundsSize = self.bounds.size; + CGFloat totalHeight = 0.0; + + CGRect barRect = CGRectMake(0, boundsSize.height - BAR_THICKNESS, self.bounds.size.width, BAR_THICKNESS); + NSMutableArray *displayedBars = [NSMutableArray array]; + + for (_ASRangeDebugBarView *barView in [_rangeControllerViews copy]) { + barView.frame = barRect; + + ASInterfaceState interfaceState = [barView.rangeController.dataSource interfaceStateForRangeController:barView.rangeController]; + + if (!(interfaceState & (ASInterfaceStateVisible))) { + if (barView.destroyOnLayout && barView.alpha == 0.0) { + [_rangeControllerViews removeObjectIdenticalTo:barView]; + [barView removeFromSuperview]; + } else { + barView.alpha = 0.0; + } + } else { + assert(!barView.destroyOnLayout); // In this case we should not have a visible interfaceState + barView.alpha = 1.0; + totalHeight += BAR_THICKNESS; + barRect.origin.y -= BAR_THICKNESS; + [displayedBars addObject:barView]; + } + } + + if (totalHeight > 0) { + totalHeight -= (BAR_THICKNESS * barsToClip); + } + + if (barsToClip == 0) { + CGRect overlayFrame = self.frame; + CGFloat heightChange = (overlayFrame.size.height - totalHeight); + + overlayFrame.origin.y += heightChange; + overlayFrame.size.height = totalHeight; + self.frame = overlayFrame; + + for (_ASRangeDebugBarView *barView in displayedBars) { + [self offsetYOrigin:-heightChange forView:barView]; + } + } +} + +- (void)setOrigin:(CGPoint)origin forView:(UIView *)view +{ + CGRect newFrame = view.frame; + newFrame.origin = origin; + view.frame = newFrame; +} + +- (void)offsetYOrigin:(CGFloat)offset forView:(UIView *)view +{ + CGRect newFrame = view.frame; + newFrame.origin = CGPointMake(newFrame.origin.x, newFrame.origin.y + offset); + view.frame = newFrame; +} + +- (void)addRangeController:(ASRangeController *)rangeController +{ + for (_ASRangeDebugBarView *rangeView in _rangeControllerViews) { + if (rangeView.rangeController == rangeController) { + return; + } + } + _ASRangeDebugBarView *rangeView = [[_ASRangeDebugBarView alloc] initWithRangeController:rangeController]; + [_rangeControllerViews addObject:rangeView]; + [self addSubview:rangeView]; + + if (!_animating) { + [self layoutToFitAllBarsExcept:1]; + } + + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + _animating = YES; + [self layoutToFitAllBarsExcept:0]; + } completion:^(BOOL finished) { + _animating = NO; + }]; +} + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)scrollDirection + rangeMode:(ASLayoutRangeMode)rangeMode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters + interfaceState:(ASInterfaceState)interfaceState; +{ + _ASRangeDebugBarView *viewToUpdate = [self barViewForRangeController:controller]; + + CGRect boundsRect = self.bounds; + CGRect visibleRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, ASRangeTuningParametersZero, scrollableDirections, scrollDirection); + CGRect displayRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, displayTuningParameters, scrollableDirections, scrollDirection); + CGRect preloadRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, preloadTuningParameters, scrollableDirections, scrollDirection); + + // figure out which is biggest and assume that is full bounds + BOOL displayRangeLargerThanPreload = NO; + CGFloat visibleRatio = 0; + CGFloat displayRatio = 0; + CGFloat preloadRatio = 0; + CGFloat leadingDisplayTuningRatio = 0; + CGFloat leadingPreloadTuningRatio = 0; + + if (!((displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls) == 0)) { + leadingDisplayTuningRatio = displayTuningParameters.leadingBufferScreenfuls / (displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls); + } + if (!((preloadTuningParameters.leadingBufferScreenfuls + preloadTuningParameters.trailingBufferScreenfuls) == 0)) { + leadingPreloadTuningRatio = preloadTuningParameters.leadingBufferScreenfuls / (preloadTuningParameters.leadingBufferScreenfuls + preloadTuningParameters.trailingBufferScreenfuls); + } + + if (ASScrollDirectionContainsVerticalDirection(scrollDirection)) { + + if (displayRect.size.height >= preloadRect.size.height) { + displayRangeLargerThanPreload = YES; + } else { + displayRangeLargerThanPreload = NO; + } + + if (displayRangeLargerThanPreload) { + visibleRatio = visibleRect.size.height / displayRect.size.height; + displayRatio = 1.0; + preloadRatio = preloadRect.size.height / displayRect.size.height; + } else { + visibleRatio = visibleRect.size.height / preloadRect.size.height; + displayRatio = displayRect.size.height / preloadRect.size.height; + preloadRatio = 1.0; + } + + } else { + + if (displayRect.size.width >= preloadRect.size.width) { + displayRangeLargerThanPreload = YES; + } else { + displayRangeLargerThanPreload = NO; + } + + if (displayRangeLargerThanPreload) { + visibleRatio = visibleRect.size.width / displayRect.size.width; + displayRatio = 1.0; + preloadRatio = preloadRect.size.width / displayRect.size.width; + } else { + visibleRatio = visibleRect.size.width / preloadRect.size.width; + displayRatio = displayRect.size.width / preloadRect.size.width; + preloadRatio = 1.0; + } + } + + [viewToUpdate updateWithVisibleRatio:visibleRatio + displayRatio:displayRatio + leadingDisplayRatio:leadingDisplayTuningRatio + preloadRatio:preloadRatio + leadingpreloadRatio:leadingPreloadTuningRatio + direction:scrollDirection]; + + [self setNeedsLayout]; +} + +- (_ASRangeDebugBarView *)barViewForRangeController:(ASRangeController *)controller +{ + _ASRangeDebugBarView *rangeControllerBarView = nil; + + for (_ASRangeDebugBarView *rangeView in [[_rangeControllerViews reverseObjectEnumerator] allObjects]) { + // remove barView if its rangeController has been deleted + if (!rangeView.rangeController) { + rangeView.destroyOnLayout = YES; + [self setNeedsLayout]; + } + ASInterfaceState interfaceState = [rangeView.rangeController.dataSource interfaceStateForRangeController:rangeView.rangeController]; + if (!(interfaceState & (ASInterfaceStateVisible | ASInterfaceStateDisplay))) { + [self setNeedsLayout]; + } + + if ([rangeView.rangeController isEqual:controller]) { + rangeControllerBarView = rangeView; + } + } + + return rangeControllerBarView; +} + +#define MIN_VISIBLE_INSET 40 +- (void)rangeDebugOverlayWasPanned:(UIPanGestureRecognizer *)recognizer +{ + CGPoint translation = [recognizer translationInView:recognizer.view]; + CGFloat newCenterX = recognizer.view.center.x + translation.x; + CGFloat newCenterY = recognizer.view.center.y + translation.y; + CGSize boundsSize = recognizer.view.bounds.size; + CGSize superBoundsSize = recognizer.view.superview.bounds.size; + CGFloat minAllowableX = -boundsSize.width / 2.0 + MIN_VISIBLE_INSET; + CGFloat maxAllowableX = superBoundsSize.width + boundsSize.width / 2.0 - MIN_VISIBLE_INSET; + + if (newCenterX > maxAllowableX) { + newCenterX = maxAllowableX; + } else if (newCenterX < minAllowableX) { + newCenterX = minAllowableX; + } + + CGFloat minAllowableY = -boundsSize.height / 2.0 + MIN_VISIBLE_INSET; + CGFloat maxAllowableY = superBoundsSize.height + boundsSize.height / 2.0 - MIN_VISIBLE_INSET; + + if (newCenterY > maxAllowableY) { + newCenterY = maxAllowableY; + } else if (newCenterY < minAllowableY) { + newCenterY = minAllowableY; + } + + recognizer.view.center = CGPointMake(newCenterX, newCenterY); + [recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view]; +} + +@end + +#pragma mark _ASRangeDebugBarView + +@implementation _ASRangeDebugBarView +{ + ASTextNode *_debugText; + ASTextNode *_leftDebugText; + ASTextNode *_rightDebugText; + ASImageNode *_visibleRect; + ASImageNode *_displayRect; + ASImageNode *_preloadRect; + CGFloat _visibleRatio; + CGFloat _displayRatio; + CGFloat _preloadRatio; + CGFloat _leadingDisplayRatio; + CGFloat _leadingpreloadRatio; + ASScrollDirection _scrollDirection; + BOOL _firstLayoutOfRects; +} + +- (instancetype)initWithRangeController:(ASRangeController *)rangeController +{ + self = [super initWithFrame:CGRectZero]; + if (self) { + _firstLayoutOfRects = YES; + _rangeController = rangeController; + _debugText = [self createDebugTextNode]; + _leftDebugText = [self createDebugTextNode]; + _rightDebugText = [self createDebugTextNode]; + _preloadRect = [self createRangeNodeWithColor:[UIColor orangeColor]]; + _displayRect = [self createRangeNodeWithColor:[UIColor yellowColor]]; + _visibleRect = [self createRangeNodeWithColor:[UIColor greenColor]]; + } + + return self; +} + +#define HORIZONTAL_INSET 10 +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGSize boundsSize = self.bounds.size; + CGFloat subCellHeight = 9.0; + [self setBarDebugLabelsWithSize:subCellHeight]; + [self setBarSubviewOrder]; + + CGRect rect = CGRectIntegral(CGRectMake(0, 0, boundsSize.width, floorf(boundsSize.height / 2.0))); + rect.size = [_debugText layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))].size; + rect.origin.x = (boundsSize.width - rect.size.width) / 2.0; + _debugText.frame = rect; + rect.origin.y += rect.size.height; + + rect.origin.x = 0; + rect.size = CGSizeMake(HORIZONTAL_INSET, boundsSize.height / 2.0); + _leftDebugText.frame = rect; + + rect.origin.x = boundsSize.width - HORIZONTAL_INSET; + _rightDebugText.frame = rect; + + CGFloat visibleDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _visibleRatio; + CGFloat displayDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _displayRatio; + CGFloat preloadDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _preloadRatio; + CGFloat visiblePoint = 0; + CGFloat displayPoint = 0; + CGFloat preloadPoint = 0; + + BOOL displayLargerThanPreload = (_displayRatio == 1.0) ? YES : NO; + + if (ASScrollDirectionContainsLeft(_scrollDirection) || ASScrollDirectionContainsUp(_scrollDirection)) { + + if (displayLargerThanPreload) { + visiblePoint = (displayDimension - visibleDimension) * _leadingDisplayRatio; + preloadPoint = visiblePoint - (preloadDimension - visibleDimension) * _leadingpreloadRatio; + } else { + visiblePoint = (preloadDimension - visibleDimension) * _leadingpreloadRatio; + displayPoint = visiblePoint - (displayDimension - visibleDimension) * _leadingDisplayRatio; + } + } else if (ASScrollDirectionContainsRight(_scrollDirection) || ASScrollDirectionContainsDown(_scrollDirection)) { + + if (displayLargerThanPreload) { + visiblePoint = (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio); + preloadPoint = visiblePoint - (preloadDimension - visibleDimension) * (1 - _leadingpreloadRatio); + } else { + visiblePoint = (preloadDimension - visibleDimension) * (1 - _leadingpreloadRatio); + displayPoint = visiblePoint - (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio); + } + } + + BOOL animate = !_firstLayoutOfRects; + [UIView animateWithDuration:animate ? 0.3 : 0.0 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{ + _visibleRect.frame = CGRectMake(HORIZONTAL_INSET + visiblePoint, rect.origin.y, visibleDimension, subCellHeight); + _displayRect.frame = CGRectMake(HORIZONTAL_INSET + displayPoint, rect.origin.y, displayDimension, subCellHeight); + _preloadRect.frame = CGRectMake(HORIZONTAL_INSET + preloadPoint, rect.origin.y, preloadDimension, subCellHeight); + } completion:^(BOOL finished) {}]; + + if (!animate) { + _visibleRect.alpha = _displayRect.alpha = _preloadRect.alpha = 0; + [UIView animateWithDuration:0.3 animations:^{ + _visibleRect.alpha = _displayRect.alpha = _preloadRect.alpha = 1; + }]; + } + + _firstLayoutOfRects = NO; +} + +- (void)updateWithVisibleRatio:(CGFloat)visibleRatio + displayRatio:(CGFloat)displayRatio + leadingDisplayRatio:(CGFloat)leadingDisplayRatio + preloadRatio:(CGFloat)preloadRatio + leadingpreloadRatio:(CGFloat)leadingpreloadRatio + direction:(ASScrollDirection)scrollDirection +{ + _visibleRatio = visibleRatio; + _displayRatio = displayRatio; + _leadingDisplayRatio = leadingDisplayRatio; + _preloadRatio = preloadRatio; + _leadingpreloadRatio = leadingpreloadRatio; + _scrollDirection = scrollDirection; + + [self setNeedsLayout]; +} + +- (void)setBarSubviewOrder +{ + if (_preloadRatio == 1.0) { + [self sendSubviewToBack:_preloadRect.view]; + } else { + [self sendSubviewToBack:_displayRect.view]; + } + + [self bringSubviewToFront:_visibleRect.view]; +} + +- (void)setBarDebugLabelsWithSize:(CGFloat)size +{ + if (!_debugString) { + _debugString = [[_rangeController dataSource] nameForRangeControllerDataSource]; + } + if (_debugString) { + _debugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:_debugString withSize:size]; + } + + if (ASScrollDirectionContainsVerticalDirection(_scrollDirection)) { + _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▲" withSize:size]; + _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▼" withSize:size]; + } else if (ASScrollDirectionContainsHorizontalDirection(_scrollDirection)) { + _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"◀︎" withSize:size]; + _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▶︎" withSize:size]; + } + + _leftDebugText.hidden = (_scrollDirection != ASScrollDirectionLeft && _scrollDirection != ASScrollDirectionUp); + _rightDebugText.hidden = (_scrollDirection != ASScrollDirectionRight && _scrollDirection != ASScrollDirectionDown); +} + +- (ASTextNode *)createDebugTextNode +{ + ASTextNode *label = [[ASTextNode alloc] init]; + [self addSubnode:label]; + return label; +} + +#define RANGE_BAR_CORNER_RADIUS 3 +#define RANGE_BAR_BORDER_WIDTH 1 +- (ASImageNode *)createRangeNodeWithColor:(UIColor *)color +{ + ASImageNode *rangeBarImageNode = [[ASImageNode alloc] init]; + rangeBarImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:RANGE_BAR_CORNER_RADIUS + cornerColor:[UIColor clearColor] + fillColor:[color colorWithAlphaComponent:0.5] + borderColor:[[UIColor blackColor] colorWithAlphaComponent:0.9] + borderWidth:RANGE_BAR_BORDER_WIDTH + roundedCorners:UIRectCornerAllCorners + scale:[[UIScreen mainScreen] scale]]; + [self addSubnode:rangeBarImageNode]; + + return rangeBarImageNode; +} + ++ (NSAttributedString *)whiteAttributedStringFromString:(NSString *)string withSize:(CGFloat)size NS_RETURNS_RETAINED +{ + NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor whiteColor], + NSFontAttributeName : [UIFont systemFontOfSize:size]}; + return [[NSAttributedString alloc] initWithString:string attributes:attributes]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Tips.h b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Tips.h new file mode 100644 index 0000000000..6232746a57 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Tips.h @@ -0,0 +1,43 @@ +// +// AsyncDisplayKit+Tips.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^ASTipDisplayBlock)(ASDisplayNode *node, NSString *message); + +/** + * The methods added to ASDisplayNode to control the tips system. + * + * To enable tips, define AS_ENABLE_TIPS=1 (e.g. modify ASBaseDefines.h). + */ +@interface ASDisplayNode (Tips) + +/** + * Whether this class should have tips active. Default YES. + * + * NOTE: This property is for _disabling_ tips on a per-class basis, + * if they become annoying or have false-positives. The tips system + * is completely disabled unless you define AS_ENABLE_TIPS=1. + */ +@property (class) BOOL enableTips; + +/** + * A block to be run on the main thread to show text when a tip is tapped. + * + * If nil, the default, the message is just logged to the console with the + * ancestry of the node. + */ +@property (class, nonatomic, null_resettable) ASTipDisplayBlock tipDisplayBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Tips.mm b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Tips.mm new file mode 100644 index 0000000000..f1a3ec8d72 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Debug/AsyncDisplayKit+Tips.mm @@ -0,0 +1,48 @@ +// +// AsyncDisplayKit+Tips.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASDisplayNode (Tips) + +static char ASDisplayNodeEnableTipsKey; +static ASTipDisplayBlock _Nullable __tipDisplayBlock; + +/** + * Use associated objects with NSNumbers. This is a debug property - simplicity is king. + */ ++ (void)setEnableTips:(BOOL)enableTips +{ + objc_setAssociatedObject(self, &ASDisplayNodeEnableTipsKey, @(enableTips), OBJC_ASSOCIATION_COPY); +} + ++ (BOOL)enableTips +{ + NSNumber *result = objc_getAssociatedObject(self, &ASDisplayNodeEnableTipsKey); + if (result == nil) { + return YES; + } + return result.boolValue; +} + + ++ (void)setTipDisplayBlock:(ASTipDisplayBlock)tipDisplayBlock +{ + __tipDisplayBlock = tipDisplayBlock; +} + ++ (ASTipDisplayBlock)tipDisplayBlock +{ + return __tipDisplayBlock ?: ^(ASDisplayNode *node, NSString *string) { + NSLog(@"%@. Node ancestry: %@", string, node.ancestryDescription); + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASAbstractLayoutController.h b/submodules/AsyncDisplayKit/Source/Details/ASAbstractLayoutController.h new file mode 100644 index 0000000000..6de7801fb6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASAbstractLayoutController.h @@ -0,0 +1,33 @@ +// +// ASAbstractLayoutController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDirection scrollDirection, ASRangeTuningParameters rangeTuningParameters); + +AS_EXTERN ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferVertical(ASScrollDirection scrollDirection, ASRangeTuningParameters rangeTuningParameters); + +AS_EXTERN CGRect CGRectExpandToRangeWithScrollableDirections(CGRect rect, ASRangeTuningParameters tuningParameters, ASScrollDirection scrollableDirections, ASScrollDirection scrollDirection); + +@interface ASAbstractLayoutController : NSObject + +@end + +@interface ASAbstractLayoutController (Unavailable) + +- (NSHashTable *)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType __unavailable; + +- (void)allIndexPathsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode displaySet:(NSHashTable * _Nullable * _Nullable)displaySet preloadSet:(NSHashTable * _Nullable * _Nullable)preloadSet __unavailable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASAbstractLayoutController.mm b/submodules/AsyncDisplayKit/Source/Details/ASAbstractLayoutController.mm new file mode 100644 index 0000000000..9d6b61f689 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASAbstractLayoutController.mm @@ -0,0 +1,185 @@ +// +// ASAbstractLayoutController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +ASRangeTuningParameters const ASRangeTuningParametersZero = {}; + +BOOL ASRangeTuningParametersEqualToRangeTuningParameters(ASRangeTuningParameters lhs, ASRangeTuningParameters rhs) +{ + return lhs.leadingBufferScreenfuls == rhs.leadingBufferScreenfuls && lhs.trailingBufferScreenfuls == rhs.trailingBufferScreenfuls; +} + +ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDirection scrollDirection, + ASRangeTuningParameters rangeTuningParameters) +{ + ASDirectionalScreenfulBuffer horizontalBuffer = {0, 0}; + BOOL movingRight = ASScrollDirectionContainsRight(scrollDirection); + + horizontalBuffer.positiveDirection = movingRight ? rangeTuningParameters.leadingBufferScreenfuls + : rangeTuningParameters.trailingBufferScreenfuls; + horizontalBuffer.negativeDirection = movingRight ? rangeTuningParameters.trailingBufferScreenfuls + : rangeTuningParameters.leadingBufferScreenfuls; + return horizontalBuffer; +} + +ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferVertical(ASScrollDirection scrollDirection, + ASRangeTuningParameters rangeTuningParameters) +{ + ASDirectionalScreenfulBuffer verticalBuffer = {0, 0}; + BOOL movingDown = ASScrollDirectionContainsDown(scrollDirection); + + verticalBuffer.positiveDirection = movingDown ? rangeTuningParameters.leadingBufferScreenfuls + : rangeTuningParameters.trailingBufferScreenfuls; + verticalBuffer.negativeDirection = movingDown ? rangeTuningParameters.trailingBufferScreenfuls + : rangeTuningParameters.leadingBufferScreenfuls; + return verticalBuffer; +} + +CGRect CGRectExpandHorizontally(CGRect rect, ASDirectionalScreenfulBuffer buffer) +{ + CGFloat negativeDirectionWidth = buffer.negativeDirection * rect.size.width; + CGFloat positiveDirectionWidth = buffer.positiveDirection * rect.size.width; + rect.size.width = negativeDirectionWidth + rect.size.width + positiveDirectionWidth; + rect.origin.x -= negativeDirectionWidth; + return rect; +} + +CGRect CGRectExpandVertically(CGRect rect, ASDirectionalScreenfulBuffer buffer) +{ + CGFloat negativeDirectionHeight = buffer.negativeDirection * rect.size.height; + CGFloat positiveDirectionHeight = buffer.positiveDirection * rect.size.height; + rect.size.height = negativeDirectionHeight + rect.size.height + positiveDirectionHeight; + rect.origin.y -= negativeDirectionHeight; + return rect; +} + +CGRect CGRectExpandToRangeWithScrollableDirections(CGRect rect, ASRangeTuningParameters tuningParameters, + ASScrollDirection scrollableDirections, ASScrollDirection scrollDirection) +{ + // Can scroll horizontally - expand the range appropriately + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { + ASDirectionalScreenfulBuffer horizontalBuffer = ASDirectionalScreenfulBufferHorizontal(scrollDirection, tuningParameters); + rect = CGRectExpandHorizontally(rect, horizontalBuffer); + } + + // Can scroll vertically - expand the range appropriately + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { + ASDirectionalScreenfulBuffer verticalBuffer = ASDirectionalScreenfulBufferVertical(scrollDirection, tuningParameters); + rect = CGRectExpandVertically(rect, verticalBuffer); + } + + return rect; +} + +@interface ASAbstractLayoutController () { + std::vector> _tuningParameters; +} +@end + +@implementation ASAbstractLayoutController + ++ (std::vector>)defaultTuningParameters +{ + auto tuningParameters = std::vector> (ASLayoutRangeModeCount, std::vector (ASLayoutRangeTypeCount)); + + tuningParameters[ASLayoutRangeModeFull][ASLayoutRangeTypeDisplay] = { + .leadingBufferScreenfuls = 1.0, + .trailingBufferScreenfuls = 0.5 + }; + + tuningParameters[ASLayoutRangeModeFull][ASLayoutRangeTypePreload] = { + .leadingBufferScreenfuls = 2.5, + .trailingBufferScreenfuls = 1.5 + }; + + tuningParameters[ASLayoutRangeModeMinimum][ASLayoutRangeTypeDisplay] = { + .leadingBufferScreenfuls = 0.25, + .trailingBufferScreenfuls = 0.25 + }; + tuningParameters[ASLayoutRangeModeMinimum][ASLayoutRangeTypePreload] = { + .leadingBufferScreenfuls = 0.5, + .trailingBufferScreenfuls = 0.25 + }; + + tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypeDisplay] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypePreload] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + + // The Low Memory range mode has special handling. Because a zero range still includes the visible area / bounds, + // in order to implement the behavior of releasing all graphics memory (backing stores), ASRangeController must check + // for this range mode and use an empty set for displayIndexPaths rather than querying the ASLayoutController for the indexPaths. + tuningParameters[ASLayoutRangeModeLowMemory][ASLayoutRangeTypeDisplay] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + tuningParameters[ASLayoutRangeModeLowMemory][ASLayoutRangeTypePreload] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + return tuningParameters; +} + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + ASDisplayNodeAssert(self.class != [ASAbstractLayoutController class], @"Should never create instances of abstract class ASAbstractLayoutController."); + + _tuningParameters = [[self class] defaultTuningParameters]; + + return self; +} + +#pragma mark - Tuning Parameters + +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType +{ + return [self tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType +{ + return [self setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + ASDisplayNodeAssert(rangeMode < _tuningParameters.size() && rangeType < _tuningParameters[rangeMode].size(), @"Requesting a range that is OOB for the configured tuning parameters"); + return _tuningParameters[rangeMode][rangeType]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + ASDisplayNodeAssert(rangeMode < _tuningParameters.size() && rangeType < _tuningParameters[rangeMode].size(), @"Setting a range that is OOB for the configured tuning parameters"); + _tuningParameters[rangeMode][rangeType] = tuningParameters; +} + +#pragma mark - Abstract Index Path Range Support + +- (NSHashTable *)elementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType map:(ASElementMap *)map +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (void)allElementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode displaySet:(NSHashTable *__autoreleasing _Nullable *)displaySet preloadSet:(NSHashTable *__autoreleasing _Nullable *)preloadSet map:(ASElementMap *)map +{ + ASDisplayNodeAssertNotSupported(); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASBasicImageDownloader.h b/submodules/AsyncDisplayKit/Source/Details/ASBasicImageDownloader.h new file mode 100644 index 0000000000..d5f8169745 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASBasicImageDownloader.h @@ -0,0 +1,40 @@ +// +// ASBasicImageDownloader.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * @abstract Simple NSURLSession-based image downloader. + */ +@interface ASBasicImageDownloader : NSObject + +/** + * A shared image downloader which can be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes. + * The userInfo provided by this downloader is `nil`. + * + * This is a very basic image downloader. It does not support caching, progressive downloading and likely + * isn't something you should use in production. If you'd like something production ready, see @c ASPINRemoteImageDownloader + * + * @note It is strongly recommended you include PINRemoteImage and use @c ASPINRemoteImageDownloader instead. + */ +@property (class, readonly) ASBasicImageDownloader *sharedImageDownloader; ++ (ASBasicImageDownloader *)sharedImageDownloader NS_RETURNS_RETAINED; + ++ (instancetype)new __attribute__((unavailable("+[ASBasicImageDownloader sharedImageDownloader] must be used."))); +- (instancetype)init __attribute__((unavailable("+[ASBasicImageDownloader sharedImageDownloader] must be used."))); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASBasicImageDownloader.mm b/submodules/AsyncDisplayKit/Source/Details/ASBasicImageDownloader.mm new file mode 100644 index 0000000000..17c035a41c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASBasicImageDownloader.mm @@ -0,0 +1,355 @@ +// +// ASBasicImageDownloader.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import + +#import +#import + +#import +#import +#import + +using AS::MutexLocker; + +#pragma mark - +/** + * Collection of properties associated with a download request. + */ + +NSString * const kASBasicImageDownloaderContextCallbackQueue = @"kASBasicImageDownloaderContextCallbackQueue"; +NSString * const kASBasicImageDownloaderContextProgressBlock = @"kASBasicImageDownloaderContextProgressBlock"; +NSString * const kASBasicImageDownloaderContextCompletionBlock = @"kASBasicImageDownloaderContextCompletionBlock"; + +static inline float NSURLSessionTaskPriorityWithImageDownloaderPriority(ASImageDownloaderPriority priority) { + switch (priority) { + case ASImageDownloaderPriorityPreload: + return NSURLSessionTaskPriorityLow; + + case ASImageDownloaderPriorityImminent: + return NSURLSessionTaskPriorityDefault; + + case ASImageDownloaderPriorityVisible: + return NSURLSessionTaskPriorityHigh; + } +} + +@interface ASBasicImageDownloaderContext () +{ + BOOL _invalid; + AS::RecursiveMutex __instanceLock__; +} + +@property (nonatomic) NSMutableArray *callbackDatas; + +@end + +@implementation ASBasicImageDownloaderContext + +static NSMutableDictionary *currentRequests = nil; + ++ (AS::Mutex *)currentRequestLock +{ + static dispatch_once_t onceToken; + static AS::Mutex *currentRequestsLock; + dispatch_once(&onceToken, ^{ + currentRequestsLock = new AS::Mutex(); + }); + return currentRequestsLock; +} + ++ (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL +{ + MutexLocker l(*self.currentRequestLock); + if (!currentRequests) { + currentRequests = [[NSMutableDictionary alloc] init]; + } + ASBasicImageDownloaderContext *context = currentRequests[URL]; + if (!context) { + context = [[ASBasicImageDownloaderContext alloc] initWithURL:URL]; + currentRequests[URL] = context; + } + return context; +} + ++ (void)cancelContextWithURL:(NSURL *)URL +{ + MutexLocker l(*self.currentRequestLock); + if (currentRequests) { + [currentRequests removeObjectForKey:URL]; + } +} + +- (instancetype)initWithURL:(NSURL *)URL +{ + if (self = [super init]) { + _URL = URL; + _callbackDatas = [NSMutableArray array]; + } + return self; +} + +- (void)cancel +{ + MutexLocker l(__instanceLock__); + + NSURLSessionTask *sessionTask = self.sessionTask; + if (sessionTask) { + [sessionTask cancel]; + self.sessionTask = nil; + } + + _invalid = YES; + [self.class cancelContextWithURL:self.URL]; +} + +- (BOOL)isCancelled +{ + MutexLocker l(__instanceLock__); + return _invalid; +} + +- (void)addCallbackData:(NSDictionary *)callbackData +{ + MutexLocker l(__instanceLock__); + [self.callbackDatas addObject:callbackData]; +} + +- (void)performProgressBlocks:(CGFloat)progress +{ + MutexLocker l(__instanceLock__); + for (NSDictionary *callbackData in self.callbackDatas) { + ASImageDownloaderProgress progressBlock = callbackData[kASBasicImageDownloaderContextProgressBlock]; + dispatch_queue_t callbackQueue = callbackData[kASBasicImageDownloaderContextCallbackQueue]; + + if (progressBlock) { + dispatch_async(callbackQueue, ^{ + progressBlock(progress); + }); + } + } +} + +- (void)completeWithImage:(UIImage *)image error:(NSError *)error +{ + MutexLocker l(__instanceLock__); + for (NSDictionary *callbackData in self.callbackDatas) { + ASImageDownloaderCompletion completionBlock = callbackData[kASBasicImageDownloaderContextCompletionBlock]; + dispatch_queue_t callbackQueue = callbackData[kASBasicImageDownloaderContextCallbackQueue]; + + if (completionBlock) { + dispatch_async(callbackQueue, ^{ + completionBlock(image, error, nil, nil); + }); + } + } + + self.sessionTask = nil; + [self.callbackDatas removeAllObjects]; +} + +- (NSURLSessionTask *)createSessionTaskIfNecessaryWithBlock:(NSURLSessionTask *(^)())creationBlock { + { + MutexLocker l(__instanceLock__); + + if (self.isCancelled) { + return nil; + } + + if (self.sessionTask && (self.sessionTask.state == NSURLSessionTaskStateRunning)) { + return nil; + } + } + + NSURLSessionTask *newTask = creationBlock(); + + { + MutexLocker l(__instanceLock__); + + if (self.isCancelled) { + return nil; + } + + if (self.sessionTask && (self.sessionTask.state == NSURLSessionTaskStateRunning)) { + return nil; + } + + self.sessionTask = newTask; + + return self.sessionTask; + } +} + +@end + + +#pragma mark - +/** + * NSURLSessionDownloadTask lacks a `userInfo` property, so add this association ourselves. + */ +@interface NSURLRequest (ASBasicImageDownloader) +@property (nonatomic) ASBasicImageDownloaderContext *asyncdisplaykit_context; +@end + +@implementation NSURLRequest (ASBasicImageDownloader) + +static const void *ContextKey() { + return @selector(asyncdisplaykit_context); +} + +- (void)setAsyncdisplaykit_context:(ASBasicImageDownloaderContext *)asyncdisplaykit_context +{ + objc_setAssociatedObject(self, ContextKey(), asyncdisplaykit_context, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} +- (ASBasicImageDownloader *)asyncdisplaykit_context +{ + return objc_getAssociatedObject(self, ContextKey()); +} +@end + + +#pragma mark - +@interface ASBasicImageDownloader () +{ + NSOperationQueue *_sessionDelegateQueue; + NSURLSession *_session; +} + +@end + +@implementation ASBasicImageDownloader + ++ (ASBasicImageDownloader *)sharedImageDownloader +{ + static ASBasicImageDownloader *sharedImageDownloader = nil; + static dispatch_once_t once = 0; + dispatch_once(&once, ^{ + sharedImageDownloader = [[ASBasicImageDownloader alloc] _init]; + }); + return sharedImageDownloader; +} + +#pragma mark Lifecycle. + +- (instancetype)_init +{ + if (!(self = [super init])) + return nil; + + _sessionDelegateQueue = [[NSOperationQueue alloc] init]; + _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:self + delegateQueue:_sessionDelegateQueue]; + + return self; +} + + +#pragma mark ASImageDownloaderProtocol. + +- (nullable id)downloadImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion +{ + return [self downloadImageWithURL:URL + priority:ASImageDownloaderPriorityImminent // maps to default priority + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; +} + +- (nullable id)downloadImageWithURL:(NSURL *)URL + priority:(ASImageDownloaderPriority)priority + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion +{ + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:URL]; + + // NSURLSessionDownloadTask will do file I/O to create a temp directory. If called on the main thread this will + // cause significant performance issues. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // associate metadata with it + const auto callbackData = [[NSMutableDictionary alloc] init]; + callbackData[kASBasicImageDownloaderContextCallbackQueue] = callbackQueue ? : dispatch_get_main_queue(); + + if (downloadProgress) { + callbackData[kASBasicImageDownloaderContextProgressBlock] = [downloadProgress copy]; + } + + if (completion) { + callbackData[kASBasicImageDownloaderContextCompletionBlock] = [completion copy]; + } + + [context addCallbackData:[[NSDictionary alloc] initWithDictionary:callbackData]]; + + // Create new task if necessary + NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)[context createSessionTaskIfNecessaryWithBlock:^(){return [_session downloadTaskWithURL:URL];}]; + + if (task) { + task.priority = NSURLSessionTaskPriorityWithImageDownloaderPriority(priority); + task.originalRequest.asyncdisplaykit_context = context; + + // start downloading + [task resume]; + } + }); + + return context; +} + +- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier +{ + ASDisplayNodeAssert([downloadIdentifier isKindOfClass:ASBasicImageDownloaderContext.class], @"unexpected downloadIdentifier"); + ASBasicImageDownloaderContext *context = (ASBasicImageDownloaderContext *)downloadIdentifier; + + [context cancel]; +} + + +#pragma mark NSURLSessionDownloadDelegate. + +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask + didWriteData:(int64_t)bytesWritten + totalBytesWritten:(int64_t)totalBytesWritten + totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite +{ + ASBasicImageDownloaderContext *context = downloadTask.originalRequest.asyncdisplaykit_context; + [context performProgressBlocks:(CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite]; +} + +// invoked if the download succeeded with no error +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask + didFinishDownloadingToURL:(NSURL *)location +{ + ASBasicImageDownloaderContext *context = downloadTask.originalRequest.asyncdisplaykit_context; + if ([context isCancelled]) { + return; + } + + if (context) { + UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]]; + [context completeWithImage:image error:nil]; + } +} + +// invoked unconditionally +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionDownloadTask *)task + didCompleteWithError:(NSError *)error +{ + ASBasicImageDownloaderContext *context = task.originalRequest.asyncdisplaykit_context; + if (context && error) { + [context completeWithImage:nil error:error]; + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASBatchContext.h b/submodules/AsyncDisplayKit/Source/Details/ASBatchContext.h new file mode 100644 index 0000000000..bbe92b8c34 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASBatchContext.h @@ -0,0 +1,69 @@ +// +// ASBatchContext.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * @abstract A context object to notify when batch fetches are finished or cancelled. + */ +@interface ASBatchContext : NSObject + +/** + * Retrieve the state of the current batch process. + * + * @return A boolean reflecting if the owner of the context object is fetching another batch. + */ +- (BOOL)isFetching; + +/** + * Let the context object know that a batch fetch was completed. + * + * @param didComplete A boolean that states whether or not the batch fetch completed. + * + * @discussion Only by passing YES will the owner of the context know to attempt another batch update when necessary. + * For instance, when a table has reached the end of its data, a batch fetch will be attempted unless the context + * object thinks that it is still fetching. + */ +- (void)completeBatchFetching:(BOOL)didComplete; + +/** + * Ask the context object if the batch fetching process was cancelled by the context owner. + * + * @discussion If an error occurs in the context owner, the batch fetching may become out of sync and need to be + * cancelled. For best practices, pass the return value of -batchWasCancelled to -completeBatchFetch:. + * + * @return A boolean reflecting if the context object owner had to cancel the batch process. + */ +- (BOOL)batchFetchingWasCancelled; + +/** + * Notify the context object that something has interrupted the batch fetching process. + * + * @discussion Call this method only when something has corrupted the batch fetching process. Calling this method should + * be left to the owner of the batch process unless there is a specific purpose. + */ +- (void)cancelBatchFetching; + +/** + * Notify the context object that fetching has started. + * + * @discussion Call this method only when you are beginning a fetch process. This should really only be called by the + * context object's owner. Calling this method should be paired with -completeBatchFetching:. + */ +- (void)beginBatchFetching; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASBatchContext.mm b/submodules/AsyncDisplayKit/Source/Details/ASBatchContext.mm new file mode 100644 index 0000000000..ee4ec173a5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASBatchContext.mm @@ -0,0 +1,64 @@ +// +// ASBatchContext.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +#import +#import + +typedef NS_ENUM(NSInteger, ASBatchContextState) { + ASBatchContextStateFetching, + ASBatchContextStateCancelled, + ASBatchContextStateCompleted +}; + +@implementation ASBatchContext { + atomic_int _state; +} + +- (instancetype)init +{ + if (self = [super init]) { + _state = ATOMIC_VAR_INIT(ASBatchContextStateCompleted); + } + return self; +} + +- (BOOL)isFetching +{ + return atomic_load(&_state) == ASBatchContextStateFetching; +} + +- (BOOL)batchFetchingWasCancelled +{ + return atomic_load(&_state) == ASBatchContextStateCancelled; +} + +- (void)beginBatchFetching +{ + atomic_store(&_state, ASBatchContextStateFetching); +} + +- (void)completeBatchFetching:(BOOL)didComplete +{ + if (didComplete) { + as_log_debug(ASCollectionLog(), "Completed batch fetch with context %@", self); + atomic_store(&_state, ASBatchContextStateCompleted); + } +} + +- (void)cancelBatchFetching +{ + atomic_store(&_state, ASBatchContextStateCancelled); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASBatchFetchingDelegate.h b/submodules/AsyncDisplayKit/Source/Details/ASBatchFetchingDelegate.h new file mode 100644 index 0000000000..3b35115026 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASBatchFetchingDelegate.h @@ -0,0 +1,23 @@ +// +// ASBatchFetchingDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@protocol ASBatchFetchingDelegate + +/** + * @abstract Determine if batch fetching should begin based on the remaining time. + * If the delegate doesn't have enough information to confidently decide, it can take the given hint. + * + * @param remainingTime The amount of time left for user to reach the end of the scroll view's content. + * + * @param hint A hint for the delegate to fallback to. + */ +- (BOOL)shouldFetchBatchWithRemainingTime:(NSTimeInterval)remainingTime hint:(BOOL)hint; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionElement.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionElement.h new file mode 100644 index 0000000000..ea8fc697c4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionElement.h @@ -0,0 +1,51 @@ +// +// ASCollectionElement.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +@class ASDisplayNode; +@protocol ASRangeManagingNode; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionElement : NSObject + +@property (nullable, nonatomic, copy, readonly) NSString *supplementaryElementKind; +@property (nonatomic) ASSizeRange constrainedSize; +@property (nonatomic, weak, readonly) id owningNode; +@property (nonatomic) ASPrimitiveTraitCollection traitCollection; +@property (nullable, nonatomic, readonly) id nodeModel; + +- (instancetype)initWithNodeModel:(nullable id)nodeModel + nodeBlock:(ASCellNodeBlock)nodeBlock + supplementaryElementKind:(nullable NSString *)supplementaryElementKind + constrainedSize:(ASSizeRange)constrainedSize + owningNode:(id)owningNode + traitCollection:(ASPrimitiveTraitCollection)traitCollection; + +/** + * @return The node, running the node block if necessary. The node block will be discarded + * after the first time it is run. + */ +@property (readonly) ASCellNode *node; + +/** + * @return The node, if the node block has been run already. + */ +@property (nullable, readonly) ASCellNode *nodeIfAllocated; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionElement.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionElement.mm new file mode 100644 index 0000000000..5b7b33862b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionElement.mm @@ -0,0 +1,91 @@ +// +// ASCollectionElement.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import +#import + +@interface ASCollectionElement () + +/// Required node block used to allocate a cell node. Nil after the first execution. +@property (nonatomic) ASCellNodeBlock nodeBlock; + +@end + +@implementation ASCollectionElement { + std::mutex _lock; + ASCellNode *_node; +} + +- (instancetype)initWithNodeModel:(id)nodeModel + nodeBlock:(ASCellNodeBlock)nodeBlock + supplementaryElementKind:(NSString *)supplementaryElementKind + constrainedSize:(ASSizeRange)constrainedSize + owningNode:(id)owningNode + traitCollection:(ASPrimitiveTraitCollection)traitCollection +{ + NSAssert(nodeBlock != nil, @"Node block must not be nil"); + self = [super init]; + if (self) { + _nodeModel = nodeModel; + _nodeBlock = nodeBlock; + _supplementaryElementKind = [supplementaryElementKind copy]; + _constrainedSize = constrainedSize; + _owningNode = owningNode; + _traitCollection = traitCollection; + } + return self; +} + +- (ASCellNode *)node +{ + std::lock_guard l(_lock); + if (_nodeBlock != nil) { + ASCellNode *node = _nodeBlock(); + _nodeBlock = nil; + if (node == nil) { + ASDisplayNodeFailAssert(@"Node block returned nil node!"); + node = [[ASCellNode alloc] init]; + } + node.owningNode = _owningNode; + node.collectionElement = self; + ASTraitCollectionPropagateDown(node, _traitCollection); + node.nodeModel = _nodeModel; + _node = node; + } + return _node; +} + +- (ASCellNode *)nodeIfAllocated +{ + std::lock_guard l(_lock); + return _node; +} + +- (void)setTraitCollection:(ASPrimitiveTraitCollection)traitCollection +{ + ASCellNode *nodeIfNeedsPropagation; + + { + std::lock_guard l(_lock); + if (! ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(_traitCollection, traitCollection)) { + _traitCollection = traitCollection; + nodeIfNeedsPropagation = _node; + } + } + + if (nodeIfNeedsPropagation != nil) { + ASTraitCollectionPropagateDown(nodeIfNeedsPropagation, traitCollection); + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionFlowLayoutDelegate.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionFlowLayoutDelegate.h new file mode 100644 index 0000000000..ba6e5b48ed --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionFlowLayoutDelegate.h @@ -0,0 +1,32 @@ +// +// ASCollectionFlowLayoutDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED + +/** + * A thread-safe, high performant layout delegate that arranges items into a flow layout. + * It uses a concurrent and multi-line ASStackLayoutSpec under the hood. Thus, per-child flex properties (i.e alignSelf, + * flexShrink, flexGrow, etc - see @ASStackLayoutElement) can be set directly on cell nodes to be used + * to calculate the final collection layout. + */ +@interface ASCollectionFlowLayoutDelegate : NSObject + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionFlowLayoutDelegate.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionFlowLayoutDelegate.mm new file mode 100644 index 0000000000..37d311d24e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionFlowLayoutDelegate.mm @@ -0,0 +1,82 @@ +// +// ASCollectionFlowLayoutDelegate.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +@implementation ASCollectionFlowLayoutDelegate { + ASScrollDirection _scrollableDirections; +} + +- (instancetype)init +{ + return [self initWithScrollableDirections:ASScrollDirectionVerticalDirections]; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections +{ + self = [super init]; + if (self) { + _scrollableDirections = scrollableDirections; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertMainThread(); + return _scrollableDirections; +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + return nil; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + NSArray *children = ASArrayByFlatMapping(elements.itemElements, ASCollectionElement *element, element.node); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + children:children]; + stackSpec.concurrent = YES; + + ASSizeRange sizeRange = ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, context.scrollableDirections); + ASLayout *layout = [stackSpec layoutThatFits:sizeRange]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nullable(ASLayout * _Nonnull sublayout) { + ASCellNode *node = ASDynamicCast(sublayout.layoutElement, ASCellNode); + return node ? node.collectionElement : nil; + }]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.h new file mode 100644 index 0000000000..c6d1df2cb7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -0,0 +1,110 @@ +// +// ASCollectionGalleryLayoutDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +@class ASElementMap; +@class ASCollectionGalleryLayoutDelegate; + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASCollectionGalleryLayoutPropertiesProviding + +/** + * Returns the fixed size of each and every element. + * + * @discussion This method will only be called on main thread. + * + * @param delegate The calling object. + * + * @param elements All elements to be sized. + * + * @return The elements' size + */ +- (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements; + +@optional + +/** + * Returns the minumum spacing to use between lines of items. + * + * @discussion This method will only be called on main thread. + * + * @discussion For a vertically scrolling layout, this value represents the minimum spacing between rows. + * For a horizontally scrolling one, it represents the minimum spacing between columns. + * It is not applied between the first line and the header, or between the last line and the footer. + * This is the same behavior as UICollectionViewFlowLayout's minimumLineSpacing. + * + * @param delegate The calling object. + * + * @param elements All elements in the layout. + * + * @return The interitem spacing + */ +- (CGFloat)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate minimumLineSpacingForElements:(ASElementMap *)elements; + +/** + * Returns the minumum spacing to use between items in the same row or column, depending on the scroll directions. + * + * @discussion This method will only be called on main thread. + * + * @discussion For a vertically scrolling layout, this value represents the minimum spacing between items in the same row. + * For a horizontally scrolling one, it represents the minimum spacing between items in the same column. + * It is considered while fitting items into lines, but the actual final spacing between some items might be larger. + * This is the same behavior as UICollectionViewFlowLayout's minimumInteritemSpacing. + * + * @param delegate The calling object. + * + * @param elements All elements in the layout. + * + * @return The interitem spacing + */ +- (CGFloat)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate minimumInteritemSpacingForElements:(ASElementMap *)elements; + +/** + * Returns the margins of each section. + * + * @discussion This method will only be called on main thread. + * + * @param delegate The calling object. + * + * @param elements All elements in the layout. + * + * @return The margins used to layout content in a section + */ +- (UIEdgeInsets)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sectionInsetForElements:(ASElementMap *)elements; + +@end + +/** + * A thread-safe layout delegate that arranges items with the same size into a flow layout. + * + * @note Supplemenraty elements are not supported. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionGalleryLayoutDelegate : NSObject + +@property (nonatomic, weak) id propertiesProvider; + +/** + * Designated initializer. + * + * @param scrollableDirections The scrollable directions of this layout. Must be either vertical or horizontal directions. + */ +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.m b/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.m new file mode 100644 index 0000000000..99e65f8343 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -0,0 +1,97 @@ +// +// ASCollectionGalleryLayoutDelegate.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - ASCollectionGalleryLayoutDelegate + +@implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; + CGSize _itemSize; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections +{ + self = [super init]; + if (self) { + _scrollableDirections = scrollableDirections; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertMainThread(); + return _scrollableDirections; +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + if (_sizeProvider == nil) { + return nil; + } + + return [NSValue valueWithCGSize:[_sizeProvider sizeForElements:elements]]; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + CGSize pageSize = context.viewportSize; + ASScrollDirection scrollableDirections = context.scrollableDirections; + + CGSize itemSize = context.additionalInfo ? ((NSValue *)context.additionalInfo).CGSizeValue : CGSizeZero; + if (CGSizeEqualToSize(CGSizeZero, itemSize)) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + children:children]; + stackSpec.concurrent = YES; + ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { + return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; + }]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.mm new file mode 100644 index 0000000000..94d1f0de36 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -0,0 +1,141 @@ +// +// ASCollectionGalleryLayoutDelegate.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - ASCollectionGalleryLayoutDelegate + +@implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; + + struct { + unsigned int minimumLineSpacingForElements:1; + unsigned int minimumInteritemSpacingForElements:1; + unsigned int sectionInsetForElements:1; + } _propertiesProviderFlags; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections +{ + self = [super init]; + if (self) { + // Scrollable directions must be either vertical or horizontal, but not both + ASDisplayNodeAssertTrue(ASScrollDirectionContainsVerticalDirection(scrollableDirections) + || ASScrollDirectionContainsHorizontalDirection(scrollableDirections)); + ASDisplayNodeAssertFalse(ASScrollDirectionContainsVerticalDirection(scrollableDirections) + && ASScrollDirectionContainsHorizontalDirection(scrollableDirections)); + _scrollableDirections = scrollableDirections; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertMainThread(); + return _scrollableDirections; +} + +- (void)setPropertiesProvider:(id)propertiesProvider +{ + ASDisplayNodeAssertMainThread(); + if (propertiesProvider == nil) { + _propertiesProvider = nil; + _propertiesProviderFlags = {}; + } else { + _propertiesProvider = propertiesProvider; + _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:minimumLineSpacingForElements:)]; + _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:minimumInteritemSpacingForElements:)]; + _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:sectionInsetForElements:)]; + } +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + id propertiesProvider = _propertiesProvider; + if (propertiesProvider == nil) { + return nil; + } + + CGSize itemSize = [propertiesProvider galleryLayoutDelegate:self sizeForElements:elements]; + UIEdgeInsets sectionInset = _propertiesProviderFlags.sectionInsetForElements ? [propertiesProvider galleryLayoutDelegate:self sectionInsetForElements:elements] : UIEdgeInsetsZero; + CGFloat lineSpacing = _propertiesProviderFlags.minimumLineSpacingForElements ? [propertiesProvider galleryLayoutDelegate:self minimumLineSpacingForElements:elements] : 0.0; + CGFloat interitemSpacing = _propertiesProviderFlags.minimumInteritemSpacingForElements ? [propertiesProvider galleryLayoutDelegate:self minimumInteritemSpacingForElements:elements] : 0.0; + return [[_ASCollectionGalleryLayoutInfo alloc] initWithItemSize:itemSize + minimumLineSpacing:lineSpacing + minimumInteritemSpacing:interitemSpacing + sectionInset:sectionInset]; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + CGSize pageSize = context.viewportSize; + ASScrollDirection scrollableDirections = context.scrollableDirections; + + _ASCollectionGalleryLayoutInfo *info = ASDynamicCast(context.additionalInfo, _ASCollectionGalleryLayoutInfo); + CGSize itemSize = info.itemSize; + if (info == nil || CGSizeEqualToSize(CGSizeZero, itemSize)) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + NSArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + ASStackLayoutDirection stackDirection = ASScrollDirectionContainsVerticalDirection(scrollableDirections) + ? ASStackLayoutDirectionHorizontal + : ASStackLayoutDirectionVertical; + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:stackDirection + spacing:info.minimumInteritemSpacing + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + lineSpacing:info.minimumLineSpacing + children:children]; + stackSpec.concurrent = YES; + + ASLayoutSpec *finalSpec = stackSpec; + UIEdgeInsets sectionInset = info.sectionInset; + if (UIEdgeInsetsEqualToEdgeInsets(sectionInset, UIEdgeInsetsZero) == NO) { + finalSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:sectionInset child:stackSpec]; + } + + ASLayout *layout = [finalSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nullable(ASLayout * _Nonnull sublayout) { + _ASGalleryLayoutItem *item = ASDynamicCast(sublayout.layoutElement, _ASGalleryLayoutItem); + return item ? item.collectionElement : nil; + }]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionInternal.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionInternal.h new file mode 100644 index 0000000000..925b19f970 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionInternal.h @@ -0,0 +1,68 @@ +// +// ASCollectionInternal.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASCollectionViewLayoutFacilitatorProtocol; +@class ASCollectionNode; +@class ASDataController; +@class ASRangeController; + +@interface ASCollectionView () +- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(nullable id)layoutFacilitator owningNode:(nullable ASCollectionNode *)owningNode eventLog:(nullable ASEventLog *)eventLog; + +@property (nonatomic, weak) ASCollectionNode *collectionNode; +@property (nonatomic, readonly) ASDataController *dataController; +@property (nonatomic, readonly) ASRangeController *rangeController; + +/** + * The change set that we're currently building, if any. + */ +@property (nonatomic, nullable, readonly) _ASHierarchyChangeSet *changeSet; + +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) ASCellLayoutMode cellLayoutMode; + +/** + * Attempt to get the view-layer index path for the item with the given index path. + * + * @param indexPath The index path of the item. + * @param wait If the item hasn't reached the view yet, this attempts to wait for updates to commit. + */ +- (nullable NSIndexPath *)convertIndexPathFromCollectionNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait; + +/** + * Attempt to get the node index path given the view-layer index path. + * + * @param indexPath The index path of the row. + */ +- (nullable NSIndexPath *)convertIndexPathToCollectionNode:(NSIndexPath *)indexPath; + +/** + * Attempt to get the node index paths given the view-layer index paths. + * + * @param indexPaths An array of index paths in the view space + */ +- (nullable NSArray *)convertIndexPathsToCollectionNode:(nullable NSArray *)indexPaths; + +- (void)beginUpdates; + +- (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutContext.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutContext.h new file mode 100644 index 0000000000..f5e875f561 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutContext.h @@ -0,0 +1,35 @@ +// +// ASCollectionLayoutContext.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASElementMap; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionLayoutContext : NSObject + +@property (nonatomic, readonly) CGSize viewportSize; +@property (nonatomic, readonly) CGPoint initialContentOffset; +@property (nonatomic, readonly) ASScrollDirection scrollableDirections; +@property (nonatomic, weak, readonly) ASElementMap *elements; +@property (nullable, nonatomic, readonly) id additionalInfo; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutContext.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutContext.mm new file mode 100644 index 0000000000..5f82fd76fa --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutContext.mm @@ -0,0 +1,107 @@ +// +// ASCollectionLayoutContext.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +#import +#import +#import +#import +#import +#import + +@implementation ASCollectionLayoutContext { + Class _layoutDelegateClass; + __weak ASCollectionLayoutCache *_layoutCache; +} + +- (instancetype)initWithViewportSize:(CGSize)viewportSize + initialContentOffset:(CGPoint)initialContentOffset + scrollableDirections:(ASScrollDirection)scrollableDirections + elements:(ASElementMap *)elements + layoutDelegateClass:(Class)layoutDelegateClass + layoutCache:(ASCollectionLayoutCache *)layoutCache + additionalInfo:(id)additionalInfo +{ + self = [super init]; + if (self) { + _viewportSize = viewportSize; + _initialContentOffset = initialContentOffset; + _scrollableDirections = scrollableDirections; + _elements = elements; + _layoutDelegateClass = layoutDelegateClass; + _layoutCache = layoutCache; + _additionalInfo = additionalInfo; + } + return self; +} + +- (Class)layoutDelegateClass +{ + return _layoutDelegateClass; +} + +- (ASCollectionLayoutCache *)layoutCache +{ + return _layoutCache; +} + +// NOTE: Some properties, like initialContentOffset and layoutCache are ignored in -isEqualToContext: and -hash. +// That is because contexts can be equal regardless of the content offsets or layout caches. +- (BOOL)isEqualToContext:(ASCollectionLayoutContext *)context +{ + if (context == nil) { + return NO; + } + + // NOTE: ASObjectIsEqual returns YES when both objects are nil. + // So don't use ASObjectIsEqual on _elements. + // It is a weak property and 2 layouts generated from different sets of elements + // should never be considered the same even if they are nil now. + return CGSizeEqualToSize(_viewportSize, context.viewportSize) + && _scrollableDirections == context.scrollableDirections + && [_elements isEqual:context.elements] + && _layoutDelegateClass == context.layoutDelegateClass + && ASObjectIsEqual(_additionalInfo, context.additionalInfo); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[ASCollectionLayoutContext class]]) { + return NO; + } + return [self isEqualToContext:other]; +} + +- (NSUInteger)hash +{ + struct { + CGSize viewportSize; + ASScrollDirection scrollableDirections; + NSUInteger elementsHash; + NSUInteger layoutDelegateClassHash; + NSUInteger additionalInfoHash; + } data = { + _viewportSize, + _scrollableDirections, + _elements.hash, + _layoutDelegateClass.hash, + [_additionalInfo hash] + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutDelegate.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutDelegate.h new file mode 100644 index 0000000000..d92a8c0ab7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutDelegate.h @@ -0,0 +1,63 @@ +// +// ASCollectionLayoutDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASElementMap, ASCollectionLayoutContext, ASCollectionLayoutState; + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASCollectionLayoutDelegate + +/** + * @abstract Returns the scrollable directions of the coming layout (@see @c -calculateLayoutWithContext:). + * It will be available in the context parameter in +calculateLayoutWithContext: + * + * @return The scrollable directions. + * + * @discusstion This method will be called on main thread. + */ +- (ASScrollDirection)scrollableDirections; + +/** + * @abstract Returns any additional information needed for a coming layout pass (@see @c -calculateLayoutWithContext:) with the given elements. + * + * @param elements The elements to be laid out later. + * + * @discussion The returned object must support equality and hashing (i.e `-isEqual:` and `-hash` must be properly implemented). + * It should contain all the information needed for the layout pass to perform. It will be available in the context parameter in +calculateLayoutWithContext: + * + * This method will be called on main thread. + */ +- (nullable id)additionalInfoForLayoutWithElements:(ASElementMap *)elements; + +/** + * @abstract Prepares and returns a new layout for given context. + * + * @param context A context that contains all elements to be laid out and any additional information needed. + * + * @return The new layout calculated for the given context. + * + * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. + * As a result, clients must solely rely on the given context and should not reach out to other objects for information not available in the context. + * + * This method can be called on background theads. It must be thread-safe and should not change any internal state of this delegate. + * It must block the calling thread but can dispatch to other theads to reduce total blocking time. + */ ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutState.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutState.h new file mode 100644 index 0000000000..461f4b1296 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutState.h @@ -0,0 +1,115 @@ +// +// ASCollectionLayoutState.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASCollectionLayoutContext, ASLayout, ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +typedef ASCollectionElement * _Nullable (^ASCollectionLayoutStateGetElementBlock)(ASLayout *); + +@interface NSMapTable (ASCollectionLayoutConvenience) + ++ (NSMapTable *)elementToLayoutAttributesTable; + +@end + +AS_SUBCLASSING_RESTRICTED + +/// An immutable state of the collection layout +@interface ASCollectionLayoutState : NSObject + +/// The context used to calculate this object +@property (readonly) ASCollectionLayoutContext *context; + +/// The final content size of the collection's layout +@property (readonly) CGSize contentSize; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Designated initializer. + * + * @param context The context used to calculate this object + * + * @param contentSize The content size of the collection's layout + * + * @param table A map between elements to their layout attributes. It must contain all elements. + * It should have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. + */ +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; + +/** + * Convenience initializer. Returns an object with zero content size and an empty table. + * + * @param context The context used to calculate this object + */ +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context; + +/** + * Convenience initializer. + * + * @param context The context used to calculate this object + * + * @param layout The layout describes size and position of all elements. + * + * @param getElementBlock A block that can retrieve the collection element from a sublayout of the root layout. + */ +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + getElementBlock:(ASCollectionLayoutStateGetElementBlock)getElementBlock; + +/** + * Returns all layout attributes present in this object. + */ +- (NSArray *)allLayoutAttributes; + +/** + * Returns layout attributes of elements in the specified rect. + * + * @param rect The rect containing the target elements. + */ +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect; + +/** + * Returns layout attributes of the element at the specified index path. + * + * @param indexPath The index path of the item. + */ +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Returns layout attributes of the specified supplementary element. + * + * @param kind A string that identifies the type of the supplementary element. + * + * @param indexPath The index path of the element. + */ +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind + atIndexPath:(NSIndexPath *)indexPath; + +/** + * Returns layout attributes of the specified element. + * + * @element The element. + */ +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutState.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutState.mm new file mode 100644 index 0000000000..2c09202c1b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionLayoutState.mm @@ -0,0 +1,259 @@ +// +// ASCollectionLayoutState.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import + +@implementation NSMapTable (ASCollectionLayoutConvenience) + ++ (NSMapTable *)elementToLayoutAttributesTable +{ + return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; +} + +@end + +@implementation ASCollectionLayoutState { + AS::Mutex __instanceLock__; + CGSize _contentSize; + ASCollectionLayoutContext *_context; + NSMapTable *_elementToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_pageToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_unmeasuredPageToLayoutAttributesTable; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context +{ + return [self initWithContext:context + contentSize:CGSizeZero +elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + getElementBlock:(ASCollectionLayoutStateGetElementBlock)getElementBlock +{ + ASElementMap *elements = context.elements; + NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; + + // Traverse the given layout tree in breadth first fashion. Generate layout attributes for all included elements along the way. + struct Context { + ASLayout *layout; + CGPoint absolutePosition; + }; + + std::queue queue; + queue.push({layout, CGPointZero}); + + while (!queue.empty()) { + Context context = queue.front(); + queue.pop(); + + ASLayout *layout = context.layout; + const CGPoint absolutePosition = context.absolutePosition; + + ASCollectionElement *element = getElementBlock(layout); + if (element != nil) { + NSIndexPath *indexPath = [elements indexPathForElement:element]; + NSString *supplementaryElementKind = element.supplementaryElementKind; + + UICollectionViewLayoutAttributes *attrs; + if (supplementaryElementKind == nil) { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; + } else { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; + } + + CGRect frame = layout.frame; + frame.origin = absolutePosition; + attrs.frame = frame; + [table setObject:attrs forKey:element]; + } + + // Add all sublayouts to process in next step + for (ASLayout *sublayout in layout.sublayouts) { + queue.push({sublayout, absolutePosition + sublayout.position}); + } + } + + return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + elementToLayoutAttributesTable:(NSMapTable *)table +{ + self = [super init]; + if (self) { + _context = context; + _contentSize = contentSize; + _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure clients can't mutate it after this point. + CGSize pageSize = context.viewportSize; + _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:table.objectEnumerator contentSize:contentSize pageSize:pageSize]; + _unmeasuredPageToLayoutAttributesTable = [ASCollectionLayoutState _unmeasuredLayoutAttributesTableFromTable:table contentSize:contentSize pageSize:pageSize]; + } + return self; +} + +- (ASCollectionLayoutContext *)context +{ + return _context; +} + +- (CGSize)contentSize +{ + return _contentSize; +} + +- (NSArray *)allLayoutAttributes +{ + return [_elementToLayoutAttributesTable.objectEnumerator allObjects]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_context.elements elementForItemAtIndexPath:indexPath]; + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)elementKind + atIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element +{ + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +{ + CGSize pageSize = _context.viewportSize; + NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); + if (pages.count == 0) { + return @[]; + } + + // Use a set here because some items may span multiple pages + const auto result = [[NSMutableSet alloc] init]; + for (id pagePtr in pages) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; + if (allAttrs.count > 0) { + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + + if (CGRectContainsRect(rect, pageRect)) { + [result addObjectsFromArray:allAttrs]; + } else { + for (UICollectionViewLayoutAttributes *attrs in allAttrs) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + [result addObject:attrs]; + } + } + } + } + } + + return [result allObjects]; +} + +- (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect +{ + CGSize pageSize = _context.viewportSize; + CGSize contentSize = _contentSize; + + AS::MutexLocker l(__instanceLock__); + if (_unmeasuredPageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { + return nil; + } + + // Step 1: Determine all the pages that intersect the specified rect + NSPointerArray *pagesInRect = ASPageCoordinatesForPagesThatIntersectRect(rect, contentSize, pageSize); + if (pagesInRect.count == 0) { + return nil; + } + + // Step 2: Filter out attributes in these pages that intersect the specified rect. + ASPageToLayoutAttributesTable *result = nil; + for (id pagePtr in pagesInRect) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; + if (attrsInPage.count == 0) { + continue; + } + + NSMutableArray *intersectingAttrsInPage = nil; + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + if (CGRectContainsRect(rect, pageRect)) { + // This page fits well within the specified rect. Simply return all of its attributes. + intersectingAttrsInPage = attrsInPage; + } else { + // The page intersects the specified rect. Some attributes in this page are returned, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + if (intersectingAttrsInPage == nil) { + intersectingAttrsInPage = [[NSMutableArray alloc] init]; + } + [intersectingAttrsInPage addObject:attrs]; + } + } + } + + if (intersectingAttrsInPage.count > 0) { + if (attrsInPage.count == intersectingAttrsInPage.count) { + [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; + } else { + [attrsInPage removeObjectsInArray:intersectingAttrsInPage]; + } + if (result == nil) { + result = [ASPageTable pageTableForStrongObjectPointers]; + } + [result setObject:intersectingAttrsInPage forPage:page]; + } + } + + return result; +} + +#pragma mark - Private methods + ++ (ASPageToLayoutAttributesTable *)_unmeasuredLayoutAttributesTableFromTable:(NSMapTable *)table + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize +{ + NSMutableArray *unmeasuredAttrs = [[NSMutableArray alloc] init]; + for (ASCollectionElement *element in table) { + UICollectionViewLayoutAttributes *attrs = [table objectForKey:element]; + if (element.nodeIfAllocated == nil || CGSizeEqualToSize(element.nodeIfAllocated.calculatedSize, attrs.frame.size) == NO) { + [unmeasuredAttrs addObject:attrs]; + } + } + + return [ASPageTable pageTableWithLayoutAttributes:unmeasuredAttrs contentSize:contentSize pageSize:pageSize]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutController.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutController.h new file mode 100644 index 0000000000..9441486b54 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutController.h @@ -0,0 +1,27 @@ +// +// ASCollectionViewLayoutController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionView; + +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionViewLayoutController : ASAbstractLayoutController + +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutController.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutController.mm new file mode 100644 index 0000000000..1d7f1ca013 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutController.mm @@ -0,0 +1,130 @@ +// +// ASCollectionViewLayoutController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import +#import +#import + +struct ASRangeGeometry { + CGRect rangeBounds; + CGRect updateBounds; +}; +typedef struct ASRangeGeometry ASRangeGeometry; + +#pragma mark - +#pragma mark ASCollectionViewLayoutController + +@interface ASCollectionViewLayoutController () +{ + @package + ASCollectionView * __weak _collectionView; + UICollectionViewLayout * __strong _collectionViewLayout; +} +@end + +@implementation ASCollectionViewLayoutController + +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView +{ + if (!(self = [super init])) { + return nil; + } + + _collectionView = collectionView; + _collectionViewLayout = [collectionView collectionViewLayout]; + return self; +} + +- (NSHashTable *)elementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType map:(ASElementMap *)map +{ + ASRangeTuningParameters tuningParameters = [self tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + CGRect rangeBounds = [self rangeBoundsWithScrollDirection:scrollDirection rangeTuningParameters:tuningParameters]; + return [self elementsWithinRangeBounds:rangeBounds map:map]; +} + +- (void)allElementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode displaySet:(NSHashTable *__autoreleasing _Nullable *)displaySet preloadSet:(NSHashTable *__autoreleasing _Nullable *)preloadSet map:(ASElementMap *)map +{ + if (displaySet == NULL || preloadSet == NULL) { + return; + } + + ASRangeTuningParameters displayParams = [self tuningParametersForRangeMode:rangeMode rangeType:ASLayoutRangeTypeDisplay]; + ASRangeTuningParameters preloadParams = [self tuningParametersForRangeMode:rangeMode rangeType:ASLayoutRangeTypePreload]; + CGRect displayBounds = [self rangeBoundsWithScrollDirection:scrollDirection rangeTuningParameters:displayParams]; + CGRect preloadBounds = [self rangeBoundsWithScrollDirection:scrollDirection rangeTuningParameters:preloadParams]; + + CGRect unionBounds = CGRectUnion(displayBounds, preloadBounds); + NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:unionBounds]; + NSInteger count = layoutAttributes.count; + + __auto_type display = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:count]; + __auto_type preload = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:count]; + + for (UICollectionViewLayoutAttributes *la in layoutAttributes) { + // Manually filter out elements that don't intersect the range bounds. + // See comment in elementsForItemsWithinRangeBounds: + // This is re-implemented here so that the iteration over layoutAttributes can be done once to check both ranges. + CGRect frame = la.frame; + BOOL intersectsDisplay = CGRectIntersectsRect(displayBounds, frame); + BOOL intersectsPreload = CGRectIntersectsRect(preloadBounds, frame); + if (intersectsDisplay == NO && intersectsPreload == NO && CATransform3DIsIdentity(la.transform3D) == YES) { + // Questionable why the element would be included here, but it doesn't belong. + continue; + } + + // Avoid excessive retains and releases, as well as property calls. We know the element is kept alive by map. + __unsafe_unretained ASCollectionElement *e = [map elementForLayoutAttributes:la]; + if (e != nil && intersectsDisplay) { + [display addObject:e]; + } + if (e != nil && intersectsPreload) { + [preload addObject:e]; + } + } + + *displaySet = display; + *preloadSet = preload; + return; +} + +- (NSHashTable *)elementsWithinRangeBounds:(CGRect)rangeBounds map:(ASElementMap *)map +{ + NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; + NSHashTable *elementSet = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:layoutAttributes.count]; + + for (UICollectionViewLayoutAttributes *la in layoutAttributes) { + // Manually filter out elements that don't intersect the range bounds. + // If a layout returns elements outside the requested rect this can be a huge problem. + // For instance in a paging flow, you may only want to preload 3 pages (one center, one on each side) + // but if flow layout includes the 4th page (which it does! as of iOS 9&10), you will preload a 4th + // page as well. + if (CATransform3DIsIdentity(la.transform3D) && CGRectIntersectsRect(la.frame, rangeBounds) == NO) { + continue; + } + [elementSet addObject:[map elementForLayoutAttributes:la]]; + } + + return elementSet; +} + +- (CGRect)rangeBoundsWithScrollDirection:(ASScrollDirection)scrollDirection + rangeTuningParameters:(ASRangeTuningParameters)tuningParameters +{ + CGRect rect = _collectionView.bounds; + + return CGRectExpandToRangeWithScrollableDirections(rect, tuningParameters, [_collectionView scrollableDirections], scrollDirection); +} + +@end +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutInspector.h b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutInspector.h new file mode 100644 index 0000000000..be812b68a3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutInspector.h @@ -0,0 +1,88 @@ +// +// ASCollectionViewLayoutInspector.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASCollectionView; +@protocol ASCollectionDataSource; +@protocol ASCollectionDelegate; + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionView); + +@protocol ASCollectionViewLayoutInspecting + +/** + * Asks the inspector to provide a constrained size range for the given collection view node. + */ +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Return the directions in which your collection view can scroll + */ +- (ASScrollDirection)scrollableDirections; + +@optional + +/** + * Asks the inspector to provide a constrained size range for the given supplementary node. + */ +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +/** + * Allow the inspector to respond to delegate changes. + * + * @discussion A great time to update perform selector caches! + */ +- (void)didChangeCollectionViewDelegate:(nullable id)delegate; + +/** + * Allow the inspector to respond to dataSource changes. + * + * @discussion A great time to update perform selector caches! + */ +- (void)didChangeCollectionViewDataSource:(nullable id)dataSource; + +#pragma mark Deprecated Methods + +/** + * Asks the inspector for the number of supplementary sections in the collection view for the given kind. + * + * @deprecated This method will not be called, and it is only deprecated as a reminder to remove it. + * Supplementary elements must exist in the same sections as regular collection view items i.e. -numberOfSectionsInCollectionView: + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's method instead."); + +@end + +/** + * A layout inspector for non-flow layouts that returns a constrained size to let the cells layout itself as + * far as possible based on the scrollable direction of the collection view. + * It doesn't support supplementary nodes and therefore doesn't implement delegate methods + * that are related to supplementary node's management. + * + * @warning This class is not meant to be subclassed and will be restricted in the future. + */ +@interface ASCollectionViewLayoutInspector : NSObject +@end + + +NS_ASSUME_NONNULL_END + +#endif \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutInspector.mm b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutInspector.mm new file mode 100644 index 0000000000..f02fe1c171 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASCollectionViewLayoutInspector.mm @@ -0,0 +1,78 @@ +// +// ASCollectionViewLayoutInspector.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import + +#pragma mark - Helper Functions + +// Returns a constrained size to let the cells layout itself as far as possible based on the scrollable direction +// of the collection view +ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionView) { + CGSize maxSize = collectionView.bounds.size; + UIEdgeInsets contentInset = collectionView.contentInset; + if (ASScrollDirectionContainsHorizontalDirection(collectionView.scrollableDirections)) { + maxSize.width = CGFLOAT_MAX; + maxSize.height -= (contentInset.top + contentInset.bottom); + } else { + maxSize.width -= (contentInset.left + contentInset.right); + maxSize.height = CGFLOAT_MAX; + } + return ASSizeRangeMake(CGSizeZero, maxSize); +} + +#pragma mark - ASCollectionViewLayoutInspector + +@implementation ASCollectionViewLayoutInspector { + struct { + unsigned int implementsConstrainedSizeForNodeAtIndexPathDeprecated:1; + unsigned int implementsConstrainedSizeForNodeAtIndexPath:1; + } _delegateFlags; +} + +#pragma mark ASCollectionViewLayoutInspecting + +- (void)didChangeCollectionViewDelegate:(id)delegate +{ + if (delegate == nil) { + memset(&_delegateFlags, 0, sizeof(_delegateFlags)); + } else { + _delegateFlags.implementsConstrainedSizeForNodeAtIndexPathDeprecated = [delegate respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)]; + _delegateFlags.implementsConstrainedSizeForNodeAtIndexPath = [delegate respondsToSelector:@selector(collectionNode:constrainedSizeForItemAtIndexPath:)]; + } +} + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + if (_delegateFlags.implementsConstrainedSizeForNodeAtIndexPath) { + return [collectionView.asyncDelegate collectionNode:collectionView.collectionNode constrainedSizeForItemAtIndexPath:indexPath]; + } else if (_delegateFlags.implementsConstrainedSizeForNodeAtIndexPathDeprecated) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [collectionView.asyncDelegate collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } else { + // With 2.0 `collectionView:constrainedSizeForNodeAtIndexPath:` was moved to the delegate. Assert if not implemented on the delegate but on the data source + ASDisplayNodeAssert([collectionView.asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] == NO, @"collectionView:constrainedSizeForNodeAtIndexPath: was moved from the ASCollectionDataSource to the ASCollectionDelegate."); + } + + return NodeConstrainedSizeForScrollDirection(collectionView); +} + +- (ASScrollDirection)scrollableDirections +{ + return ASScrollDirectionNone; +} + +@end + +#endif \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Source/Details/ASDataController.h b/submodules/AsyncDisplayKit/Source/Details/ASDataController.h new file mode 100644 index 0000000000..5a03f24e23 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASDataController.h @@ -0,0 +1,291 @@ +// +// ASDataController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#pragma once + +#import +#import +#import +#import +#ifdef __cplusplus +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +#if ASEVENTLOG_ENABLE +#define ASDataControllerLogEvent(dataController, ...) [dataController.eventLog logEventWithBacktrace:(AS_SAVE_EVENT_BACKTRACES ? [NSThread callStackSymbols] : nil) format:__VA_ARGS__] +#else +#define ASDataControllerLogEvent(dataController, ...) +#endif + +@class ASCellNode; +@class ASCollectionElement; +@class ASCollectionLayoutContext; +@class ASCollectionLayoutState; +@class ASDataController; +@class ASElementMap; +@class ASLayout; +@class _ASHierarchyChangeSet; +@protocol ASRangeManagingNode; +@protocol ASTraitEnvironment; +@protocol ASSectionContext; + +typedef NSUInteger ASDataControllerAnimationOptions; + +AS_EXTERN NSString * const ASDataControllerRowNodeKind; +AS_EXTERN NSString * const ASCollectionInvalidUpdateException; + +/** + Data source for data controller + It will be invoked in the same thread as the api call of ASDataController. + */ + +@protocol ASDataControllerSource + +/** + Fetch the ASCellNode block for specific index path. This block should return the ASCellNode for the specified index path. + */ +- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout; + +/** + Fetch the number of rows in specific section. + */ +- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section; + +/** + Fetch the number of sections. + */ +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController; + +/** + Returns if the collection element size matches a given size. + @precondition The element is present in the data controller's visible map. + */ +- (BOOL)dataController:(ASDataController *)dataController presentedSizeForElement:(ASCollectionElement *)element matchesSize:(CGSize)size; + +- (nullable id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Called just after dispatching ASCellNode allocation and layout to the concurrent background queue. + * In some cases, for example on the first content load for a screen, it may be desirable to call + * -waitUntilAllUpdatesAreProcessed at this point. + * + * Returning YES will cause the ASDataController to wait on the background queue, and this ensures + * that any new / changed cells are in the hierarchy by the very next CATransaction / frame draw. + */ +- (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet; +- (BOOL)dataController:(ASDataController *)dataController shouldEagerlyLayoutNode:(ASCellNode *)node; +- (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataController; + +@optional + +/** + The constrained size range for layout. Called only if collection layout delegate is not provided. + */ +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; + +- (NSArray *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections; + +- (NSUInteger)dataController:(ASDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout; + +/** + The constrained size range for layout. Called only if no data controller layout delegate is provided. + */ +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +- (nullable id)dataController:(ASDataController *)dataController contextForSection:(NSInteger)section; + +@end + +/** + Delegate for notify the data updating of data controller. + These methods will be invoked from main thread right now, but it may be moved to background thread in the future. + */ +@protocol ASDataControllerDelegate + +/** + * Called for change set updates. + * + * @param changeSet The change set that includes all updates + * + * @param updates The block that performs relevant data updates. + * + * @discussion The updates block must always be executed or the data controller will get into a bad state. + * It should be called at the time the backing view is ready to process the updates, + * i.e inside the updates block of `-[UICollectionView performBatchUpdates:completion:] or after calling `-[UITableView beginUpdates]`. + */ +- (void)dataController:(ASDataController *)dataController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates; + +@end + +@protocol ASDataControllerLayoutDelegate + +/** + * @abstract Returns a layout context needed for a coming layout pass with the given elements. + * The context should contain the elements and any additional information needed. + * + * @discussion This method will be called on main thread. + */ +- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements; + +/** + * @abstract Prepares and returns a new layout for given context. + * + * @param context A context that was previously returned by `-layoutContextWithElements:`. + * + * @return The new layout calculated for the given context. + * + * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. + * As a result, clients must solely rely on the given context and should not reach out to other objects for information not available in the context. + * + * This method will be called on background theads. It must be thread-safe and should not change any internal state of the conforming object. + * It must block the calling thread but can dispatch to other theads to reduce total blocking time. + */ ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; + +@end + +/** + * Controller to layout data in background, and managed data updating. + * + * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the data + * will be updated asynchronously. The dataSource must be updated to reflect the changes before these methods has been called. + * For each data updating, the corresponding methods in delegate will be called. + */ +@interface ASDataController : NSObject + +- (instancetype)initWithDataSource:(id)dataSource node:(nullable id)node eventLog:(nullable ASEventLog *)eventLog NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * The node that owns this data controller, if any. + * + * NOTE: Soon we will drop support for using ASTableView/ASCollectionView without the node, so this will be non-null. + */ +@property (nullable, nonatomic, weak, readonly) id node; + +/** + * The map that is currently displayed. The "UIKit index space." + * + * This property will only be changed on the main thread. + */ +@property (copy, readonly) ASElementMap *visibleMap; + +/** + * The latest map fetched from the data source. May be more recent than @c visibleMap. + * + * This property will only be changed on the main thread. + */ +@property (copy, readonly) ASElementMap *pendingMap; + +/** + Data source for fetching data info. + */ +@property (nonatomic, weak, readonly) id dataSource; + +/** + An object that will be included in the backtrace of any update validation exceptions that occur. + */ +@property (nonatomic, weak) id validationErrorSource; + +/** + Delegate to notify when data is updated. + */ +@property (nonatomic, weak) id delegate; + +/** + * Delegate for preparing layouts. Main thead only. + */ +@property (nonatomic, weak) id layoutDelegate; + +#ifdef __cplusplus +/** + * Returns the most recently gathered item counts from the data source. If the counts + * have been invalidated, this synchronously queries the data source and saves the result. + * + * This must be called on the main thread. + */ +- (std::vector)itemCountsFromDataSource; +#endif + +/** + * Returns YES if reloadData has been called at least once. Before this point it is + * important to ignore/suppress some operations. For example, inserting a section + * before the initial data load should have no effect. + * + * This must be called on the main thread. + */ +@property (nonatomic, readonly) BOOL initialReloadDataHasBeenCalled; + +#if ASEVENTLOG_ENABLE +/* + * @abstract The primitive event tracing object. You shouldn't directly use it to log event. Use the ASDataControllerLogEvent macro instead. + */ +@property (nonatomic, readonly) ASEventLog *eventLog; +#endif + +/** @name Data Updating */ + +- (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet; + +/** + * Re-measures all loaded nodes in the backing store. + * + * @discussion Used to respond to a change in size of the containing view + * (e.g. ASTableView or ASCollectionView after an orientation change). + * + * The invalidationBlock is called after flushing the ASMainSerialQueue, which ensures that any in-progress + * layout calculations have been applied. The block will not be called if data hasn't been loaded. + */ +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)(void))invalidationBlock; + +/** + * Re-measures given nodes in the backing store. + * + * @discussion Used to respond to setNeedsLayout calls in ASCellNode + */ +- (void)relayoutNodes:(id)nodes nodesSizeChanged:(NSMutableArray *)nodesSizesChanged; + +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(void (^)(void))completion; +- (void)waitUntilAllUpdatesAreProcessed; + +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; +- (void)onDidFinishSynchronizing:(void (^)(void))completion; + +/** + * Notifies the data controller object that its environment has changed. The object will request its environment delegate for new information + * and propagate the information to all visible elements, including ones that are being prepared in background. + * + * @discussion If called before the initial @c reloadData, this method will do nothing and the trait collection of the initial load will be requested from the environment delegate. + * + * @discussion This method can be called on any threads. + */ +- (void)environmentDidChange; + +/** + * Reset visibleMap and pendingMap when asyncDataSource and asyncDelegate of collection view become nil. + */ +- (void)clearData; + +@end + +NS_ASSUME_NONNULL_END +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASDataController.mm b/submodules/AsyncDisplayKit/Source/Details/ASDataController.mm new file mode 100644 index 0000000000..a842e76c44 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASDataController.mm @@ -0,0 +1,958 @@ +// +// ASDataController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#include + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +#define ASSERT_ON_EDITING_QUEUE ASDisplayNodeAssertNotNil(dispatch_get_specific(&kASDataControllerEditingQueueKey), @"%@ must be called on the editing transaction queue.", NSStringFromSelector(_cmd)) + +const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditingQueueKey"; +const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext"; + +NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; +NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; + +typedef dispatch_block_t ASDataControllerCompletionBlock; + +typedef void (^ASDataControllerSynchronizationBlock)(); + +@interface ASDataController () { + id _layoutDelegate; + + NSInteger _nextSectionID; + + BOOL _itemCountsFromDataSourceAreValid; // Main thread only. + std::vector _itemCountsFromDataSource; // Main thread only. + + ASMainSerialQueue *_mainSerialQueue; + + dispatch_queue_t _editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. + dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting. + std::atomic _editingTransactionGroupCount; + + BOOL _initialReloadDataHasBeenCalled; + + BOOL _synchronized; + NSMutableSet *_onDidFinishSynchronizingBlocks; + + struct { + unsigned int supplementaryNodeKindsInSections:1; + unsigned int supplementaryNodesOfKindInSection:1; + unsigned int supplementaryNodeBlockOfKindAtIndexPath:1; + unsigned int constrainedSizeForNodeAtIndexPath:1; + unsigned int constrainedSizeForSupplementaryNodeOfKindAtIndexPath:1; + unsigned int contextForSection:1; + } _dataSourceFlags; +} + +@property (copy) ASElementMap *pendingMap; +@property (copy) ASElementMap *visibleMap; +@end + +@implementation ASDataController + +#pragma mark - Lifecycle + +- (instancetype)initWithDataSource:(id)dataSource node:(nullable id)node eventLog:(ASEventLog *)eventLog +{ + if (!(self = [super init])) { + return nil; + } + + _node = node; + _dataSource = dataSource; + + _dataSourceFlags.supplementaryNodeKindsInSections = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeKindsInSections:)]; + _dataSourceFlags.supplementaryNodesOfKindInSection = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodesOfKind:inSection:)]; + _dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:shouldAsyncLayout:)]; + _dataSourceFlags.constrainedSizeForNodeAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForNodeAtIndexPath:)]; + _dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; + _dataSourceFlags.contextForSection = [_dataSource respondsToSelector:@selector(dataController:contextForSection:)]; + +#if ASEVENTLOG_ENABLE + _eventLog = eventLog; +#endif + + self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; + + _nextSectionID = 0; + + _mainSerialQueue = [[ASMainSerialQueue alloc] init]; + + _synchronized = YES; + _onDidFinishSynchronizingBlocks = [[NSMutableSet alloc] init]; + + const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding]; + _editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(_editingTransactionQueue, &kASDataControllerEditingQueueKey, &kASDataControllerEditingQueueContext, NULL); + _editingTransactionGroup = dispatch_group_create(); + + return self; +} + +- (id)layoutDelegate +{ + ASDisplayNodeAssertMainThread(); + return _layoutDelegate; +} + +- (void)setLayoutDelegate:(id)layoutDelegate +{ + ASDisplayNodeAssertMainThread(); + if (layoutDelegate != _layoutDelegate) { + _layoutDelegate = layoutDelegate; + } +} + +#pragma mark - Cell Layout + +- (void)_allocateNodesFromElements:(NSArray *)elements +{ + ASSERT_ON_EDITING_QUEUE; + + NSUInteger nodeCount = elements.count; + __weak id weakDataSource = _dataSource; + if (nodeCount == 0 || weakDataSource == nil) { + return; + } + + ASSignpostStart(ASSignpostDataControllerBatch); + + { + as_activity_create_for_scope("Data controller batch"); + + dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0); + NSUInteger threadCount = 0; + if ([_dataSource dataControllerShouldSerializeNodeCreation:self]) { + threadCount = 1; + } + ASDispatchApply(nodeCount, queue, threadCount, ^(size_t i) { + __strong id strongDataSource = weakDataSource; + if (strongDataSource == nil) { + return; + } + + unowned ASCollectionElement *element = elements[i]; + + NSMutableDictionary *dict = [[NSThread currentThread] threadDictionary]; + dict[ASThreadDictMaxConstraintSizeKey] = + [NSValue valueWithCGSize:element.constrainedSize.max]; + unowned ASCellNode *node = element.node; + [dict removeObjectForKey:ASThreadDictMaxConstraintSizeKey]; + + // Layout the node if the size range is valid. + ASSizeRange sizeRange = element.constrainedSize; + if (ASSizeRangeHasSignificantArea(sizeRange)) { + [self _layoutNode:node withConstrainedSize:sizeRange]; + } + }); + } + + ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (weakDataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); +} + +/** + * Measure and layout the given node with the constrained size range. + */ +- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize +{ + if (![_dataSource dataController:self shouldEagerlyLayoutNode:node]) { + return; + } + + ASDisplayNodeAssert(ASSizeRangeHasSignificantArea(constrainedSize), @"Attempt to layout cell node with invalid size range %@", NSStringFromASSizeRange(constrainedSize)); + + CGRect frame = CGRectZero; + frame.size = [node layoutThatFits:constrainedSize].size; + node.frame = frame; +} + +#pragma mark - Data Source Access (Calling _dataSource) + +- (NSArray *)_allIndexPathsForItemsOfKind:(NSString *)kind inSections:(NSIndexSet *)sections +{ + ASDisplayNodeAssertMainThread(); + + if (sections.count == 0 || _dataSource == nil) { + return @[]; + } + + const auto indexPaths = [[NSMutableArray alloc] init]; + if ([kind isEqualToString:ASDataControllerRowNodeKind]) { + std::vector counts = [self itemCountsFromDataSource]; + [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger sectionIndex = range.location; sectionIndex < NSMaxRange(range); sectionIndex++) { + NSUInteger itemCount = counts[sectionIndex]; + for (NSUInteger i = 0; i < itemCount; i++) { + [indexPaths addObject:[NSIndexPath indexPathForItem:i inSection:sectionIndex]]; + } + } + }]; + } else if (_dataSourceFlags.supplementaryNodesOfKindInSection) { + id dataSource = _dataSource; + [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger sectionIndex = range.location; sectionIndex < NSMaxRange(range); sectionIndex++) { + NSUInteger itemCount = [dataSource dataController:self supplementaryNodesOfKind:kind inSection:sectionIndex]; + for (NSUInteger i = 0; i < itemCount; i++) { + [indexPaths addObject:[NSIndexPath indexPathForItem:i inSection:sectionIndex]]; + } + } + }]; + } + + return indexPaths; +} + +/** + * Agressively repopulates supplementary nodes of all kinds for sections that contains some given index paths. + * + * @param map The element map into which to apply the change. + * @param indexPaths The index paths belongs to sections whose supplementary nodes need to be repopulated. + * @param changeSet The changeset that triggered this repopulation. + * @param traitCollection The trait collection needed to initialize elements + * @param indexPathsAreNew YES if index paths are "after the update," NO otherwise. + * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source + */ +- (void)_repopulateSupplementaryNodesIntoMap:(ASMutableElementMap *)map + forSectionsContainingIndexPaths:(NSArray *)indexPaths + changeSet:(_ASHierarchyChangeSet *)changeSet + traitCollection:(ASPrimitiveTraitCollection)traitCollection + indexPathsAreNew:(BOOL)indexPathsAreNew + shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges + previousMap:(ASElementMap *)previousMap +{ + ASDisplayNodeAssertMainThread(); + + if (indexPaths.count == 0) { + return; + } + + // Remove all old supplementaries from these sections + NSIndexSet *oldSections = [NSIndexSet as_sectionsFromIndexPaths:indexPaths]; + + // Add in new ones with the new kinds. + NSIndexSet *newSections; + if (indexPathsAreNew) { + newSections = oldSections; + } else { + newSections = [oldSections as_indexesByMapping:^NSUInteger(NSUInteger oldSection) { + return [changeSet newSectionForOldSection:oldSection]; + }]; + } + + for (NSString *kind in [self supplementaryKindsInSections:newSections]) { + [self _insertElementsIntoMap:map kind:kind forSections:newSections traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + } +} + +/** + * Update supplementary nodes of all kinds for sections. + * + * @param map The element map into which to apply the change. + * @param traitCollection The trait collection needed to initialize elements + * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source + */ +- (void)_updateSupplementaryNodesIntoMap:(ASMutableElementMap *)map + traitCollection:(ASPrimitiveTraitCollection)traitCollection + shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges + previousMap:(ASElementMap *)previousMap +{ + ASDisplayNodeAssertMainThread(); + if (self.layoutDelegate != nil) { + // TODO: https://github.com/TextureGroup/Texture/issues/948 + return; + } + NSUInteger sectionCount = [self itemCountsFromDataSource].size(); + if (sectionCount > 0) { + NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; + ASSizeRange newSizeRange = ASSizeRangeZero; + for (NSString *kind in [self supplementaryKindsInSections:sectionIndexes]) { + NSArray *indexPaths = [self _allIndexPathsForItemsOfKind:kind inSections:sectionIndexes]; + NSMutableArray *indexPathsToDeleteForKind = [[NSMutableArray alloc] init]; + NSMutableArray *indexPathsToInsertForKind = [[NSMutableArray alloc] init]; + // If supplementary node does exist and size is now zero, remove it. + // If supplementary node doesn't exist and size is now non-zero, insert one. + for (NSIndexPath *indexPath in indexPaths) { + ASCollectionElement *previousElement = [previousMap supplementaryElementOfKind:kind atIndexPath:indexPath]; + newSizeRange = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + BOOL sizeRangeIsZero = ASSizeRangeEqualToSizeRange(ASSizeRangeZero, newSizeRange); + if (previousElement != nil && sizeRangeIsZero) { + [indexPathsToDeleteForKind addObject:indexPath]; + } else if (previousElement == nil && !sizeRangeIsZero) { + [indexPathsToInsertForKind addObject:indexPath]; + } + } + + [map removeSupplementaryElementsAtIndexPaths:indexPathsToDeleteForKind kind:kind]; + [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPathsToInsertForKind traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:nil previousMap:previousMap]; + } + } +} + +/** + * Inserts new elements of a certain kind for some sections + * + * @param kind The kind of the elements, e.g ASDataControllerRowNodeKind + * @param sections The sections that should be populated by new elements + * @param traitCollection The trait collection needed to initialize elements + * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source + */ +- (void)_insertElementsIntoMap:(ASMutableElementMap *)map + kind:(NSString *)kind + forSections:(NSIndexSet *)sections + traitCollection:(ASPrimitiveTraitCollection)traitCollection + shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges + changeSet:(_ASHierarchyChangeSet *)changeSet + previousMap:(ASElementMap *)previousMap +{ + ASDisplayNodeAssertMainThread(); + + if (sections.count == 0 || _dataSource == nil) { + return; + } + + NSArray *indexPaths = [self _allIndexPathsForItemsOfKind:kind inSections:sections]; + [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; +} + +/** + * Inserts new elements of a certain kind at some index paths + * + * @param map The map to insert the elements into. + * @param kind The kind of the elements, e.g ASDataControllerRowNodeKind + * @param indexPaths The index paths at which new elements should be populated + * @param traitCollection The trait collection needed to initialize elements + * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source + */ +- (void)_insertElementsIntoMap:(ASMutableElementMap *)map + kind:(NSString *)kind + atIndexPaths:(NSArray *)indexPaths + traitCollection:(ASPrimitiveTraitCollection)traitCollection + shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges + changeSet:(_ASHierarchyChangeSet *)changeSet + previousMap:(ASElementMap *)previousMap +{ + ASDisplayNodeAssertMainThread(); + + if (indexPaths.count == 0 || _dataSource == nil) { + return; + } + + BOOL isRowKind = [kind isEqualToString:ASDataControllerRowNodeKind]; + if (!isRowKind && !_dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath) { + // Populating supplementary elements but data source doesn't support. + return; + } + + LOG(@"Populating elements of kind: %@, for index paths: %@", kind, indexPaths); + id dataSource = self.dataSource; + id node = self.node; + BOOL shouldAsyncLayout = YES; + for (NSIndexPath *indexPath in indexPaths) { + ASCellNodeBlock nodeBlock; + id nodeModel; + if (isRowKind) { + nodeModel = [dataSource dataController:self nodeModelForItemAtIndexPath:indexPath]; + + // Get the prior element and attempt to update the existing cell node. + if (nodeModel != nil && !changeSet.includesReloadData) { + NSIndexPath *oldIndexPath = [changeSet oldIndexPathForNewIndexPath:indexPath]; + if (oldIndexPath != nil) { + ASCollectionElement *oldElement = [previousMap elementForItemAtIndexPath:oldIndexPath]; + ASCellNode *oldNode = oldElement.node; + if ([oldNode canUpdateToNodeModel:nodeModel]) { + // Just wrap the node in a block. The collection element will -setNodeModel: + nodeBlock = ^{ + return oldNode; + }; + } + } + } + if (nodeBlock == nil) { + nodeBlock = [dataSource dataController:self nodeBlockAtIndexPath:indexPath shouldAsyncLayout:&shouldAsyncLayout]; + } + } else { + nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath shouldAsyncLayout:&shouldAsyncLayout]; + } + + ASSizeRange constrainedSize = ASSizeRangeUnconstrained; + if (shouldFetchSizeRanges) { + constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + } + + ASCollectionElement *element = [[ASCollectionElement alloc] initWithNodeModel:nodeModel + nodeBlock:nodeBlock + supplementaryElementKind:isRowKind ? nil : kind + constrainedSize:constrainedSize + owningNode:node + traitCollection:traitCollection]; + [map insertElement:element atIndexPath:indexPath]; + changeSet.countForAsyncLayout += (shouldAsyncLayout ? 1 : 0); + } +} + +- (void)invalidateDataSourceItemCounts +{ + ASDisplayNodeAssertMainThread(); + _itemCountsFromDataSourceAreValid = NO; +} + +- (std::vector)itemCountsFromDataSource +{ + ASDisplayNodeAssertMainThread(); + if (NO == _itemCountsFromDataSourceAreValid) { + id source = self.dataSource; + NSInteger sectionCount = [source numberOfSectionsInDataController:self]; + std::vector newCounts; + newCounts.reserve(sectionCount); + for (NSInteger i = 0; i < sectionCount; i++) { + newCounts.push_back([source dataController:self rowsInSection:i]); + } + _itemCountsFromDataSource = newCounts; + _itemCountsFromDataSourceAreValid = YES; + } + return _itemCountsFromDataSource; +} + +- (NSArray *)supplementaryKindsInSections:(NSIndexSet *)sections +{ + if (_dataSourceFlags.supplementaryNodeKindsInSections) { + return [_dataSource dataController:self supplementaryNodeKindsInSections:sections]; + } + + return @[]; +} + +/** + * Returns constrained size for the node of the given kind and at the given index path. + * NOTE: index path must be in the data-source index space. + */ +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + + id dataSource = _dataSource; + if (dataSource == nil || indexPath == nil) { + return ASSizeRangeZero; + } + + if ([kind isEqualToString:ASDataControllerRowNodeKind]) { + ASDisplayNodeAssert(_dataSourceFlags.constrainedSizeForNodeAtIndexPath, @"-dataController:constrainedSizeForNodeAtIndexPath: must also be implemented"); + return [dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; + } + + if (_dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath){ + return [dataSource dataController:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; + } + + ASDisplayNodeAssert(NO, @"Unknown constrained size for node of kind %@ by data source %@", kind, dataSource); + return ASSizeRangeZero; +} + +#pragma mark - Batching (External API) + +- (void)waitUntilAllUpdatesAreProcessed +{ + // Schedule block in main serial queue to wait until all operations are finished that are + // where scheduled while waiting for the _editingTransactionQueue to finish + [self _scheduleBlockOnMainSerialQueue:^{ }]; +} + +- (BOOL)isProcessingUpdates +{ + ASDisplayNodeAssertMainThread(); + return _mainSerialQueue.numberOfScheduledBlocks > 0 || _editingTransactionGroupCount > 0; +} + +- (void)onDidFinishProcessingUpdates:(void (^)())completion +{ + ASDisplayNodeAssertMainThread(); + if (!completion) { + return; + } + if ([self isProcessingUpdates] == NO) { + ASPerformBlockOnMainThread(completion); + } else { + dispatch_async(_editingTransactionQueue, ^{ + // Retry the block. If we're done processing updates, it'll run immediately, otherwise + // wait again for updates to quiesce completely. + // Don't use _mainSerialQueue so that we don't affect -isProcessingUpdates. + dispatch_async(dispatch_get_main_queue(), ^{ + [self onDidFinishProcessingUpdates:completion]; + }); + }); + } +} + +- (BOOL)isSynchronized { + return _synchronized; +} + +- (void)onDidFinishSynchronizing:(void (^)())completion { + ASDisplayNodeAssertMainThread(); + if (!completion) { + return; + } + if ([self isSynchronized]) { + ASPerformBlockOnMainThread(completion); + } else { + // Hang on to the completion block so that it gets called the next time view is synchronized to data. + [_onDidFinishSynchronizingBlocks addObject:[completion copy]]; + } +} + +- (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet +{ + ASDisplayNodeAssertMainThread(); + + _synchronized = NO; + + [changeSet addCompletionHandler:^(BOOL finished) { + _synchronized = YES; + [self onDidFinishProcessingUpdates:^{ + if (_synchronized) { + for (ASDataControllerSynchronizationBlock block in _onDidFinishSynchronizingBlocks) { + block(); + } + [_onDidFinishSynchronizingBlocks removeAllObjects]; + } + }]; + }]; + + if (changeSet.includesReloadData) { + if (_initialReloadDataHasBeenCalled) { + as_log_debug(ASCollectionLog(), "reloadData %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView))); + } else { + as_log_debug(ASCollectionLog(), "Initial reloadData %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView))); + _initialReloadDataHasBeenCalled = YES; + } + } else { + as_log_debug(ASCollectionLog(), "performBatchUpdates %@ %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView)), changeSet); + } + + NSTimeInterval transactionQueueFlushDuration = 0.0f; + { + AS::ScopeTimer t(transactionQueueFlushDuration); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + } + + // If the initial reloadData has not been called, just bail because we don't have our old data source counts. + // See ASUICollectionViewTests.testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable + // for the issue that UICollectionView has that we're choosing to workaround. + if (!_initialReloadDataHasBeenCalled) { + as_log_debug(ASCollectionLog(), "%@ Skipped update because load hasn't happened.", ASObjectDescriptionMakeTiny(_dataSource)); + [changeSet executeCompletionHandlerWithFinished:YES]; + return; + } + + [self invalidateDataSourceItemCounts]; + + // Log events +#if ASEVENTLOG_ENABLE + ASDataControllerLogEvent(self, @"updateWithChangeSet waited on previous update for %fms. changeSet: %@", + transactionQueueFlushDuration * 1000.0f, changeSet); + NSTimeInterval changeSetStartTime = CACurrentMediaTime(); + NSString *changeSetDescription = ASObjectDescriptionMakeTiny(changeSet); + [changeSet addCompletionHandler:^(BOOL finished) { + ASDataControllerLogEvent(self, @"finishedUpdate in %fms: %@", + (CACurrentMediaTime() - changeSetStartTime) * 1000.0f, changeSetDescription); + }]; +#endif + + // Attempt to mark the update completed. This is when update validation will occur inside the changeset. + // If an invalid update exception is thrown, we catch it and inject our "validationErrorSource" object, + // which is the table/collection node's data source, into the exception reason to help debugging. + @try { + [changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; + } @catch (NSException *e) { + id responsibleDataSource = self.validationErrorSource; + if (e.name == ASCollectionInvalidUpdateException && responsibleDataSource != nil) { + [NSException raise:ASCollectionInvalidUpdateException format:@"%@: %@", [responsibleDataSource class], e.reason]; + } else { + @throw e; + } + } + + BOOL canDelegate = (self.layoutDelegate != nil); + ASElementMap *newMap; + ASCollectionLayoutContext *layoutContext; + { + as_activity_scope(as_activity_create("Latch new data for collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + + // Step 1: Populate a new map that reflects the data source's state and use it as pendingMap + ASElementMap *previousMap = self.pendingMap; + if (changeSet.isEmpty) { + // If the change set is empty, nothing has changed so we can just reuse the previous map + newMap = previousMap; + } else { + // Mutable copy of current data. + ASMutableElementMap *mutableMap = [previousMap mutableCopy]; + + // Step 1.1: Update the mutable copies to match the data source's state + [self _updateSectionsInMap:mutableMap changeSet:changeSet]; + ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection]; + [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap]; + + // Step 1.2: Clone the new data + newMap = [mutableMap copy]; + } + self.pendingMap = newMap; + + // Step 2: Ask layout delegate for contexts + if (canDelegate) { + layoutContext = [self.layoutDelegate layoutContextWithElements:newMap]; + } + } + + as_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription); + + Class layoutDelegateClass = [self.layoutDelegate class]; + ++_editingTransactionGroupCount; + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + __block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10 + as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope); + + // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements + if (canDelegate) { + [layoutDelegateClass calculateLayoutWithContext:layoutContext]; + } else { + const auto elementsToProcess = [[NSMutableArray alloc] init]; + for (ASCollectionElement *element in newMap) { + ASCellNode *nodeIfAllocated = element.nodeIfAllocated; + if (nodeIfAllocated.shouldUseUIKitCell) { + // If the node exists and we know it is a passthrough cell, we know it will never have a .calculatedLayout. + continue; + } else if (nodeIfAllocated.calculatedLayout == nil) { + // If the node hasn't been allocated, or it doesn't have a valid layout, let's process it. + [elementsToProcess addObject:element]; + } + } + [self _allocateNodesFromElements:elementsToProcess]; + } + + // Step 4: Inform the delegate on main thread + [_mainSerialQueue performBlockOnMainThread:^{ + as_activity_scope_leave(&preparationScope); + [_delegate dataController:self updateWithChangeSet:changeSet updates:^{ + // Step 5: Deploy the new data as "completed" + // + // Note that since the backing collection view might be busy responding to user events (e.g scrolling), + // it will not consume the batch update blocks immediately. + // As a result, in a short intermidate time, the view will still be relying on the old data source state. + // Thus, we can't just swap the new map immediately before step 4, but until this update block is executed. + // (https://github.com/TextureGroup/Texture/issues/378) + self.visibleMap = newMap; + }]; + }]; + --_editingTransactionGroupCount; + }); + + // We've now dispatched node allocation and layout to a concurrent background queue. + // In some cases, it's advantageous to prevent the main thread from returning, to ensure the next + // frame displayed to the user has the view updates in place. Doing this does slightly reduce + // total latency, by donating the main thread's priority to the background threads. As such, the + // two cases where it makes sense to block: + // 1. There is very little work to be performed in the background (UIKit passthrough) + // 2. There is a higher priority on display latency than smoothness, e.g. app startup. + if ([_dataSource dataController:self shouldSynchronouslyProcessChangeSet:changeSet]) { + [self waitUntilAllUpdatesAreProcessed]; + } +} + +/** + * Update sections based on the given change set. + */ +- (void)_updateSectionsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet +{ + ASDisplayNodeAssertMainThread(); + + if (changeSet.includesReloadData) { + [map removeAllSections]; + + NSUInteger sectionCount = [self itemCountsFromDataSource].size(); + NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; + [self _insertSectionsIntoMap:map indexes:sectionIndexes]; + // Return immediately because reloadData can't be used in conjuntion with other updates. + return; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + [map removeSectionsAtIndexes:change.indexSet]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [self _insertSectionsIntoMap:map indexes:change.indexSet]; + } +} + +- (void)_insertSectionsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes +{ + ASDisplayNodeAssertMainThread(); + + [sectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + id context; + if (_dataSourceFlags.contextForSection) { + context = [_dataSource dataController:self contextForSection:idx]; + } + ASSection *section = [[ASSection alloc] initWithSectionID:_nextSectionID context:context]; + [map insertSection:section atIndex:idx]; + _nextSectionID++; + }]; +} + +/** + * Update elements based on the given change set. + */ +- (void)_updateElementsInMap:(ASMutableElementMap *)map + changeSet:(_ASHierarchyChangeSet *)changeSet + traitCollection:(ASPrimitiveTraitCollection)traitCollection + shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges + previousMap:(ASElementMap *)previousMap +{ + ASDisplayNodeAssertMainThread(); + + if (changeSet.includesReloadData) { + [map removeAllElements]; + + NSUInteger sectionCount = [self itemCountsFromDataSource].size(); + if (sectionCount > 0) { + NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; + [self _insertElementsIntoMap:map sections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + } + // Return immediately because reloadData can't be used in conjuntion with other updates. + return; + } + + // Migrate old supplementary nodes to their new index paths. + [map migrateSupplementaryElementsWithSectionMapping:changeSet.sectionMapping]; + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { + [map removeItemsAtIndexPaths:change.indexPaths]; + // Aggressively repopulate supplementary nodes (#1773 & #1629) + [self _repopulateSupplementaryNodesIntoMap:map forSectionsContainingIndexPaths:change.indexPaths + changeSet:changeSet + traitCollection:traitCollection + indexPathsAreNew:NO + shouldFetchSizeRanges:shouldFetchSizeRanges + previousMap:previousMap]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + NSIndexSet *sectionIndexes = change.indexSet; + [map removeSectionsOfItems:sectionIndexes]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [self _insertElementsIntoMap:map sections:change.indexSet traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { + [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind atIndexPaths:change.indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + // Aggressively reload supplementary nodes (#1773 & #1629) + [self _repopulateSupplementaryNodesIntoMap:map forSectionsContainingIndexPaths:change.indexPaths + changeSet:changeSet + traitCollection:traitCollection + indexPathsAreNew:YES + shouldFetchSizeRanges:shouldFetchSizeRanges + previousMap:previousMap]; + } +} + +- (void)_insertElementsIntoMap:(ASMutableElementMap *)map + sections:(NSIndexSet *)sectionIndexes + traitCollection:(ASPrimitiveTraitCollection)traitCollection + shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges + changeSet:(_ASHierarchyChangeSet *)changeSet + previousMap:(ASElementMap *)previousMap +{ + ASDisplayNodeAssertMainThread(); + + if (sectionIndexes.count == 0 || _dataSource == nil) { + return; + } + + // Items + [map insertEmptySectionsOfItemsAtIndexes:sectionIndexes]; + [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + + // Supplementaries + for (NSString *kind in [self supplementaryKindsInSections:sectionIndexes]) { + // Step 2: Populate new elements for all sections + [self _insertElementsIntoMap:map kind:kind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + } +} + +#pragma mark - Relayout + +- (void)relayoutNodes:(id)nodes nodesSizeChanged:(NSMutableArray *)nodesSizesChanged +{ + NSParameterAssert(nodes); + NSParameterAssert(nodesSizesChanged); + + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } + + id dataSource = self.dataSource; + const auto visibleMap = self.visibleMap; + const auto pendingMap = self.pendingMap; + for (ASCellNode *node in nodes) { + const auto element = node.collectionElement; + NSIndexPath *indexPathInPendingMap = [pendingMap indexPathForElement:element]; + // Ensure the element is present in both maps or skip it. If it's not in the visible map, + // then we can't check the presented size. If it's not in the pending map, we can't get the constrained size. + // This will only happen if the element has been deleted, so the specifics of this behavior aren't important. + if (indexPathInPendingMap == nil || [visibleMap indexPathForElement:element] == nil) { + continue; + } + + NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; + [self _layoutNode:node withConstrainedSize:constrainedSize]; + + BOOL matchesSize = [dataSource dataController:self presentedSizeForElement:element matchesSize:node.frame.size]; + if (! matchesSize) { + [nodesSizesChanged addObject:node]; + } + } +} + +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock +{ + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } + + // Can't relayout right away because _visibleMap may not be up-to-date, + // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _visibleMap + LOG(@"Edit Command - relayoutRows"); + [self _scheduleBlockOnMainSerialQueue:^{ + // Because -invalidateLayout doesn't trigger any operations by itself, and we answer queries from UICollectionView using layoutThatFits:, + // we invalidate the layout before we have updated all of the cells. Any cells that the collection needs the size of immediately will get + // -layoutThatFits: with a new constraint, on the main thread, and synchronously calculate them. Meanwhile, relayoutAllNodes will update + // the layout of any remaining nodes on background threads (and fast-return for any nodes that the UICV got to first). + if (invalidationBlock) { + invalidationBlock(); + } + [self _relayoutAllNodes]; + }]; +} + +- (void)_relayoutAllNodes +{ + ASDisplayNodeAssertMainThread(); + // Aggressively repopulate all supplemtary elements + // Assuming this method is run on the main serial queue, _pending and _visible maps are synced and can be manipulated directly. + ASDisplayNodeAssert(_visibleMap == _pendingMap, @"Expected visible and pending maps to be synchronized: %@", self); + + ASMutableElementMap *newMap = [_pendingMap mutableCopy]; + [self _updateSupplementaryNodesIntoMap:newMap + traitCollection:[self.node primitiveTraitCollection] + shouldFetchSizeRanges:YES + previousMap:_pendingMap]; + _pendingMap = [newMap copy]; + _visibleMap = _pendingMap; + + for (ASCollectionElement *element in _visibleMap) { + // Ignore this element if it is no longer in the latest data. It is still recognized in the UIKit world but will be deleted soon. + NSIndexPath *indexPathInPendingMap = [_pendingMap indexPathForElement:element]; + if (indexPathInPendingMap == nil) { + continue; + } + + NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; + ASSizeRange newConstrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; + + if (ASSizeRangeHasSignificantArea(newConstrainedSize)) { + element.constrainedSize = newConstrainedSize; + + // Node may not be allocated yet (e.g node virtualization or same size optimization) + // Call context.nodeIfAllocated here to avoid premature node allocation and layout + ASCellNode *node = element.nodeIfAllocated; + if (node) { + [self _layoutNode:node withConstrainedSize:newConstrainedSize]; + } + } + } +} + +# pragma mark - ASPrimitiveTraitCollection + +- (void)environmentDidChange +{ + ASPerformBlockOnMainThread(^{ + if (!_initialReloadDataHasBeenCalled) { + return; + } + + // Can't update the trait collection right away because _visibleMap may not be up-to-date, + // i.e there might be some elements that were allocated using the old trait collection but haven't been added to _visibleMap + [self _scheduleBlockOnMainSerialQueue:^{ + ASPrimitiveTraitCollection newTraitCollection = [self.node primitiveTraitCollection]; + for (ASCollectionElement *element in _visibleMap) { + element.traitCollection = newTraitCollection; + } + }]; + }); +} + +- (void)clearData +{ + ASDisplayNodeAssertMainThread(); + if (_initialReloadDataHasBeenCalled) { + [self waitUntilAllUpdatesAreProcessed]; + self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; + } +} + +# pragma mark - Helper methods + +- (void)_scheduleBlockOnMainSerialQueue:(dispatch_block_t)block +{ + ASDisplayNodeAssertMainThread(); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + [_mainSerialQueue performBlockOnMainThread:block]; +} + +@end + +#endif \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Source/Details/ASDelegateProxy.h b/submodules/AsyncDisplayKit/Source/Details/ASDelegateProxy.h new file mode 100644 index 0000000000..7ed406b6db --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASDelegateProxy.h @@ -0,0 +1,56 @@ +// +// ASDelegateProxy.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASDelegateProxy; +@protocol ASDelegateProxyInterceptor +@required +// Called if the target object is discovered to be nil if it had been non-nil at init time. +// This happens if the object is deallocated, because the proxy must maintain a weak reference to avoid cycles. +// Though the target object may become nil, the interceptor must not; it is assumed the interceptor owns the proxy. +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy; +@end + +/** + * Stand-in for delegates like UITableView or UICollectionView's delegate / dataSource. + * Any selectors flagged by "interceptsSelector" are routed to the interceptor object and are not delivered to the target. + * Everything else leaves AsyncDisplayKit safely and arrives at the original target object. + */ + +@interface ASDelegateProxy : NSProxy + +- (instancetype)initWithTarget:(id)target interceptor:(id )interceptor; + +// This method must be overridden by a subclass. +- (BOOL)interceptsSelector:(SEL)selector; + +@end + +/** + * ASTableView intercepts and/or overrides a few of UITableView's critical data source and delegate methods. + * + * Any selector included in this function *MUST* be implemented by ASTableView. + */ + +@interface ASTableViewProxy : ASDelegateProxy +@end + +/** + * ASCollectionView intercepts and/or overrides a few of UICollectionView's critical data source and delegate methods. + * + * Any selector included in this function *MUST* be implemented by ASCollectionView. + */ + +@interface ASCollectionViewProxy : ASDelegateProxy +@end + +@interface ASPagerNodeProxy : ASDelegateProxy +@end + diff --git a/submodules/AsyncDisplayKit/Source/Details/ASDelegateProxy.mm b/submodules/AsyncDisplayKit/Source/Details/ASDelegateProxy.mm new file mode 100644 index 0000000000..a5368024d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASDelegateProxy.mm @@ -0,0 +1,273 @@ +// +// ASDelegateProxy.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import + +// UIKit performs a class check for UIDataSourceModelAssociation protocol conformance rather than an instance check, so +// the implementation of conformsToProtocol: below never gets called. We need to declare the two as conforming to the protocol here, then +// we need to implement dummy methods to get rid of a compiler warning about not conforming to the protocol. +@interface ASTableViewProxy () +@end + +@interface ASCollectionViewProxy () +@end + +@interface ASDelegateProxy (UIDataSourceModelAssociationPrivate) +- (nullable NSString *)_modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view; +- (nullable NSIndexPath *)_indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view; +@end + +@implementation ASTableViewProxy + +- (BOOL)interceptsSelector:(SEL)selector +{ + return ( + // handled by ASTableView node<->cell machinery + selector == @selector(tableView:cellForRowAtIndexPath:) || + selector == @selector(tableView:heightForRowAtIndexPath:) || + + // Selection, highlighting, menu + selector == @selector(tableView:willSelectRowAtIndexPath:) || + selector == @selector(tableView:didSelectRowAtIndexPath:) || + selector == @selector(tableView:willDeselectRowAtIndexPath:) || + selector == @selector(tableView:didDeselectRowAtIndexPath:) || + selector == @selector(tableView:shouldHighlightRowAtIndexPath:) || + selector == @selector(tableView:didHighlightRowAtIndexPath:) || + selector == @selector(tableView:didUnhighlightRowAtIndexPath:) || + selector == @selector(tableView:shouldShowMenuForRowAtIndexPath:) || + selector == @selector(tableView:canPerformAction:forRowAtIndexPath:withSender:) || + selector == @selector(tableView:performAction:forRowAtIndexPath:withSender:) || + + // handled by ASRangeController + selector == @selector(numberOfSectionsInTableView:) || + selector == @selector(tableView:numberOfRowsInSection:) || + + // reordering support + selector == @selector(tableView:canMoveRowAtIndexPath:) || + selector == @selector(tableView:moveRowAtIndexPath:toIndexPath:) || + + // used for ASCellNode visibility + selector == @selector(scrollViewDidScroll:) || + + // used for ASCellNode user interaction + selector == @selector(scrollViewWillBeginDragging:) || + selector == @selector(scrollViewDidEndDragging:willDecelerate:) || + + // used for ASRangeController visibility updates + selector == @selector(tableView:willDisplayCell:forRowAtIndexPath:) || + selector == @selector(tableView:didEndDisplayingCell:forRowAtIndexPath:) || + + // used for batch fetching API + selector == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) || + selector == @selector(scrollViewDidEndDecelerating:) || + + // UIDataSourceModelAssociation + selector == @selector(modelIdentifierForElementAtIndexPath:inView:) || + selector == @selector(indexPathForElementWithModelIdentifier:inView:) + ); +} + +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + return [self _modelIdentifierForElementAtIndexPath:indexPath inView:view]; +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + return [self _indexPathForElementWithModelIdentifier:identifier inView:view]; +} + +@end + +@implementation ASCollectionViewProxy + +- (BOOL)interceptsSelector:(SEL)selector +{ + return ( + // handled by ASCollectionView node<->cell machinery + selector == @selector(collectionView:cellForItemAtIndexPath:) || + selector == @selector(collectionView:layout:sizeForItemAtIndexPath:) || + selector == @selector(collectionView:layout:insetForSectionAtIndex:) || + selector == @selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:) || + selector == @selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:) || + selector == @selector(collectionView:layout:referenceSizeForHeaderInSection:) || + selector == @selector(collectionView:layout:referenceSizeForFooterInSection:) || + selector == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || + + // Selection, highlighting, menu + selector == @selector(collectionView:shouldSelectItemAtIndexPath:) || + selector == @selector(collectionView:didSelectItemAtIndexPath:) || + selector == @selector(collectionView:shouldDeselectItemAtIndexPath:) || + selector == @selector(collectionView:didDeselectItemAtIndexPath:) || + selector == @selector(collectionView:shouldHighlightItemAtIndexPath:) || + selector == @selector(collectionView:didHighlightItemAtIndexPath:) || + selector == @selector(collectionView:didUnhighlightItemAtIndexPath:) || + selector == @selector(collectionView:shouldShowMenuForItemAtIndexPath:) || + selector == @selector(collectionView:canPerformAction:forItemAtIndexPath:withSender:) || + selector == @selector(collectionView:performAction:forItemAtIndexPath:withSender:) || + + // Item counts + selector == @selector(numberOfSectionsInCollectionView:) || + selector == @selector(collectionView:numberOfItemsInSection:) || + + // Element appearance callbacks + selector == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || + selector == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || + selector == @selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:) || + selector == @selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:) || + + // used for batch fetching API + selector == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) || + selector == @selector(scrollViewDidEndDecelerating:) || + + // used for ASCellNode visibility + selector == @selector(scrollViewDidScroll:) || + + // used for ASCellNode user interaction + selector == @selector(scrollViewWillBeginDragging:) || + selector == @selector(scrollViewDidEndDragging:willDecelerate:) || + + // intercepted due to not being supported by ASCollectionView (prevent bugs caused by usage) + selector == @selector(collectionView:canMoveItemAtIndexPath:) || + selector == @selector(collectionView:moveItemAtIndexPath:toIndexPath:) || + + // UIDataSourceModelAssociation + selector == @selector(modelIdentifierForElementAtIndexPath:inView:) || + selector == @selector(indexPathForElementWithModelIdentifier:inView:) + ); +} + +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + return [self _modelIdentifierForElementAtIndexPath:indexPath inView:view]; +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + return [self _indexPathForElementWithModelIdentifier:identifier inView:view]; +} + +@end + +@implementation ASPagerNodeProxy + +- (BOOL)interceptsSelector:(SEL)selector +{ + return ( + // handled by ASPagerDataSource node<->cell machinery + selector == @selector(collectionNode:nodeForItemAtIndexPath:) || + selector == @selector(collectionNode:nodeBlockForItemAtIndexPath:) || + selector == @selector(collectionNode:numberOfItemsInSection:) || + selector == @selector(collectionNode:constrainedSizeForItemAtIndexPath:) + ); +} + +@end + +@implementation ASDelegateProxy { + id __weak _interceptor; + id __weak _target; +} + +- (instancetype)initWithTarget:(id)target interceptor:(id )interceptor +{ + ASDisplayNodeAssert(interceptor, @"interceptor must not be nil"); + + _target = target ? : [NSNull null]; + _interceptor = interceptor; + + return self; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol +{ + id target = _target; + if (target) { + return [target conformsToProtocol:aProtocol]; + } else { + return [super conformsToProtocol:aProtocol]; + } +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + if ([self interceptsSelector:aSelector]) { + return [_interceptor respondsToSelector:aSelector]; + } else { + // Also return NO if _target has become nil due to zeroing weak reference (or placeholder initialization). + return [_target respondsToSelector:aSelector]; + } +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + if ([self interceptsSelector:aSelector]) { + return _interceptor; + } else { + id target = _target; + if (target) { + return [target respondsToSelector:aSelector] ? target : nil; + } else { + // The _interceptor needs to be nilled out in this scenario. For that a strong reference needs to be created + // to be able to nil out the _interceptor but still let it know that the proxy target has deallocated + // We have to hold a strong reference to the interceptor as we have to nil it out and call the proxyTargetHasDeallocated + // The reason that the interceptor needs to be nilled out is that there maybe a change of a infinite loop, for example + // if a method will be called in the proxyTargetHasDeallocated: that again would trigger a whole new forwarding cycle + id interceptor = _interceptor; + _interceptor = nil; + [interceptor proxyTargetHasDeallocated:self]; + + return nil; + } + } +} + +- (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."); + return NO; +} + +- (nullable NSString *)_modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + return [(id)_interceptor modelIdentifierForElementAtIndexPath:indexPath inView:view]; +} + +- (nullable NSIndexPath *)_indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + return [(id)_interceptor indexPathForElementWithModelIdentifier:identifier inView:view]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASElementMap.h b/submodules/AsyncDisplayKit/Source/Details/ASElementMap.h new file mode 100644 index 0000000000..1ea0ef58d4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASElementMap.h @@ -0,0 +1,137 @@ +// +// ASElementMap.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionElement, ASSection, UICollectionViewLayoutAttributes; +@protocol ASSectionContext; + +/** + * An immutable representation of the state of a collection view's data. + * All items and supplementary elements are represented by ASCollectionElement. + * Fast enumeration is in terms of ASCollectionElement. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASElementMap : NSObject + +/** + * The total number of elements in this map. + */ +@property (readonly) NSUInteger count; + +/** + * The number of sections (of items) in this map. + */ +@property (readonly) NSInteger numberOfSections; + +/** + * The kinds of supplementary elements present in this map. O(1) + */ +@property (copy, readonly) NSArray *supplementaryElementKinds; + +/** + * Returns number of items in the given section. O(1) + */ +- (NSInteger)numberOfItemsInSection:(NSInteger)section; + +/** + * Returns the context object for the given section, if any. O(1) + */ +- (nullable id)contextForSection:(NSInteger)section; + +/** + * All the index paths for all the items in this map. O(N) + * + * This property may be removed in the future, since it doesn't account for supplementary nodes. + */ +@property (copy, readonly) NSArray *itemIndexPaths; + +/** + * All the item elements in this map, in ascending order. O(N) + */ +@property (copy, readonly) NSArray *itemElements; + +/** + * Returns the index path that corresponds to the same element in @c map at the given @c indexPath. + * O(1) for items, fast O(N) for sections. + * + * Note you can pass "section index paths" of length 1 and get a corresponding section index path. + */ +- (nullable NSIndexPath *)convertIndexPath:(NSIndexPath *)indexPath fromMap:(ASElementMap *)map; + +/** + * Returns the section index into the receiver that corresponds to the same element in @c map at @c sectionIndex. Fast O(N). + * + * Returns @c NSNotFound if the section does not exist in the receiver. + */ +- (NSInteger)convertSection:(NSInteger)sectionIndex fromMap:(ASElementMap *)map; + +/** + * Returns the index path for the given element. O(1) + */ +- (nullable NSIndexPath *)indexPathForElement:(ASCollectionElement *)element; + +/** + * Returns the index path for the given element, if it represents a cell. O(1) + */ +- (nullable NSIndexPath *)indexPathForElementIfCell:(ASCollectionElement *)element; + +/** + * Returns the item-element at the given index path. O(1) + */ +- (nullable ASCollectionElement *)elementForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Returns the element for the supplementary element of the given kind at the given index path. O(1) + */ +- (nullable ASCollectionElement *)supplementaryElementOfKind:(NSString *)supplementaryElementKind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Returns the element that corresponds to the given layout attributes, if any. + * + * NOTE: This method only regards the category, kind, and index path of the attributes object. Elements do not + * have any concept of size/position. + */ +- (nullable ASCollectionElement *)elementForLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes; + +/** + * A very terse description e.g. { itemCounts = [ ] } + */ +@property (readonly) NSString *smallDescription; + +#pragma mark - Initialization -- Only Useful to ASDataController + + +// SectionIndex -> ItemIndex -> Element +typedef NSArray *> ASCollectionElementTwoDimensionalArray; + +// ElementKind -> IndexPath -> Element +typedef NSDictionary *> ASSupplementaryElementDictionary; + +/** + * Create a new element map for this dataset. You probably don't need to use this – ASDataController is the only one who creates these. + * + * @param sections The array of ASSection objects. + * @param items A 2D array of ASCollectionElements, for each item. + * @param supplementaryElements A dictionary of gathered supplementary elements. + */ +- (instancetype)initWithSections:(NSArray *)sections + items:(ASCollectionElementTwoDimensionalArray *)items + supplementaryElements:(ASSupplementaryElementDictionary *)supplementaryElements; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASElementMap.mm b/submodules/AsyncDisplayKit/Source/Details/ASElementMap.mm new file mode 100644 index 0000000000..6df89ff6cd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASElementMap.mm @@ -0,0 +1,280 @@ +// +// ASElementMap.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import +#import +#import +#import +#import + +@interface ASElementMap () + +@property (nonatomic, readonly) NSArray *sections; + +// Element -> IndexPath +@property (nonatomic, readonly) NSMapTable *elementToIndexPathMap; + +// The items, in a 2D array +@property (nonatomic, readonly) ASCollectionElementTwoDimensionalArray *sectionsOfItems; + +@property (nonatomic, readonly) ASSupplementaryElementDictionary *supplementaryElements; + +@end + +@implementation ASElementMap + +- (instancetype)init +{ + return [self initWithSections:@[] items:@[] supplementaryElements:@{}]; +} + +- (instancetype)initWithSections:(NSArray *)sections items:(ASCollectionElementTwoDimensionalArray *)items supplementaryElements:(ASSupplementaryElementDictionary *)supplementaryElements +{ + NSCParameterAssert(items.count == sections.count); + + if (self = [super init]) { + _sections = [sections copy]; + _sectionsOfItems = [[NSArray alloc] initWithArray:items copyItems:YES]; + _supplementaryElements = [[NSDictionary alloc] initWithDictionary:supplementaryElements copyItems:YES]; + + // Setup our index path map + _elementToIndexPathMap = [NSMapTable mapTableWithKeyOptions:(NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableCopyIn]; + NSInteger s = 0; + for (NSArray *section in _sectionsOfItems) { + NSInteger i = 0; + for (ASCollectionElement *element in section) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:s]; + [_elementToIndexPathMap setObject:indexPath forKey:element]; + i++; + } + s++; + } + for (NSDictionary *supplementariesForKind in [_supplementaryElements objectEnumerator]) { + [supplementariesForKind enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *_Nonnull indexPath, ASCollectionElement * _Nonnull element, BOOL * _Nonnull stop) { + [_elementToIndexPathMap setObject:indexPath forKey:element]; + }]; + } + } + return self; +} + +- (NSUInteger)count +{ + return _elementToIndexPathMap.count; +} + +- (NSArray *)itemIndexPaths +{ + return ASIndexPathsForTwoDimensionalArray(_sectionsOfItems); +} + +- (NSArray *)itemElements +{ + return ASElementsInTwoDimensionalArray(_sectionsOfItems); +} + +- (NSInteger)numberOfSections +{ + return _sectionsOfItems.count; +} + +- (NSArray *)supplementaryElementKinds +{ + return _supplementaryElements.allKeys; +} + +- (NSInteger)numberOfItemsInSection:(NSInteger)section +{ + if (![self sectionIndexIsValid:section assert:YES]) { + return 0; + } + + return _sectionsOfItems[section].count; +} + +- (id)contextForSection:(NSInteger)section +{ + if (![self sectionIndexIsValid:section assert:NO]) { + return nil; + } + + return _sections[section].context; +} + +- (nullable NSIndexPath *)indexPathForElement:(ASCollectionElement *)element +{ + return element ? [_elementToIndexPathMap objectForKey:element] : nil; +} + +- (nullable NSIndexPath *)indexPathForElementIfCell:(ASCollectionElement *)element +{ + if (element.supplementaryElementKind == nil) { + return [self indexPathForElement:element]; + } else { + return nil; + } +} + +- (nullable ASCollectionElement *)elementForItemAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger section, item; + if (![self itemIndexPathIsValid:indexPath assert:NO item:&item section:§ion]) { + return nil; + } + + return _sectionsOfItems[section][item]; +} + +- (nullable ASCollectionElement *)supplementaryElementOfKind:(NSString *)supplementaryElementKind atIndexPath:(NSIndexPath *)indexPath +{ + return _supplementaryElements[supplementaryElementKind][indexPath]; +} + +- (ASCollectionElement *)elementForLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + switch (layoutAttributes.representedElementCategory) { + case UICollectionElementCategoryCell: + // Cell + return [self elementForItemAtIndexPath:layoutAttributes.indexPath]; + case UICollectionElementCategorySupplementaryView: + // Supplementary element. + return [self supplementaryElementOfKind:layoutAttributes.representedElementKind atIndexPath:layoutAttributes.indexPath]; + case UICollectionElementCategoryDecorationView: + // No support for decoration views. + return nil; + } +} + +- (NSIndexPath *)convertIndexPath:(NSIndexPath *)indexPath fromMap:(ASElementMap *)map +{ + if (indexPath.item == NSNotFound) { + // Section index path + NSInteger result = [self convertSection:indexPath.section fromMap:map]; + return (result != NSNotFound ? [NSIndexPath indexPathWithIndex:result] : nil); + } else { + // Item index path + ASCollectionElement *element = [map elementForItemAtIndexPath:indexPath]; + return [self indexPathForElement:element]; + } +} + +- (NSInteger)convertSection:(NSInteger)sectionIndex fromMap:(ASElementMap *)map +{ + if (![map sectionIndexIsValid:sectionIndex assert:YES]) { + return NSNotFound; + } + + ASSection *section = map.sections[sectionIndex]; + return [_sections indexOfObjectIdenticalTo:section]; +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + return self; +} + +// NSMutableCopying conformance is declared in ASMutableElementMap.h, so that most consumers of ASElementMap don't bother with it. +#pragma mark - NSMutableCopying + +- (id)mutableCopyWithZone:(NSZone *)zone +{ + return [[ASMutableElementMap alloc] initWithSections:_sections items:_sectionsOfItems supplementaryElements:_supplementaryElements]; +} + +#pragma mark - NSFastEnumeration + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id _Nullable __unsafe_unretained [])buffer count:(NSUInteger)len +{ + return [_elementToIndexPathMap countByEnumeratingWithState:state objects:buffer count:len]; +} + +- (NSString *)smallDescription +{ + NSMutableArray *sectionDescriptions = [NSMutableArray array]; + + NSUInteger i = 0; + for (NSArray *section in _sectionsOfItems) { + [sectionDescriptions addObject:[NSString stringWithFormat:@"", i, section.count]]; + i++; + } + return ASObjectDescriptionMakeWithoutObject(@[ @{ @"itemCounts": sectionDescriptions }]); +} + +#pragma mark - ASDescriptionProvider + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"items" : _sectionsOfItems }]; + [result addObject:@{ @"supplementaryElements" : _supplementaryElements }]; + return result; +} + +#pragma mark - Internal + +/** + * Fails assert + return NO if section is out of bounds. + */ +- (BOOL)sectionIndexIsValid:(NSInteger)section assert:(BOOL)assert +{ + NSInteger sectionCount = _sectionsOfItems.count; + if (section >= sectionCount || section < 0) { + if (assert) { + ASDisplayNodeFailAssert(@"Invalid section index %ld when there are only %ld sections!", (long)section, (long)sectionCount); + } + return NO; + } else { + return YES; + } +} + +/** + * If indexPath is nil, just returns NO. + * If indexPath is invalid, fails assertion and returns NO. + * Otherwise returns YES and sets the item & section. + */ +- (BOOL)itemIndexPathIsValid:(NSIndexPath *)indexPath assert:(BOOL)assert item:(out NSInteger *)outItem section:(out NSInteger *)outSection +{ + if (indexPath == nil) { + return NO; + } + + NSInteger section = indexPath.section; + if (![self sectionIndexIsValid:section assert:assert]) { + return NO; + } + + NSInteger itemCount = _sectionsOfItems[section].count; + NSInteger item = indexPath.item; + if (item >= itemCount || item < 0) { + if (assert) { + ASDisplayNodeFailAssert(@"Invalid item index %ld in section %ld which only has %ld items!", (long)item, (long)section, (long)itemCount); + } + return NO; + } + *outItem = item; + *outSection = section; + return YES; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASEventLog.h b/submodules/AsyncDisplayKit/Source/Details/ASEventLog.h new file mode 100644 index 0000000000..9a147f259d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASEventLog.h @@ -0,0 +1,39 @@ +// +// ASEventLog.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#ifndef ASEVENTLOG_CAPACITY +#define ASEVENTLOG_CAPACITY 5 +#endif + +#ifndef ASEVENTLOG_ENABLE +#define ASEVENTLOG_ENABLE 0 +#endif + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASEventLog : NSObject + +/** + * Create a new event log. + * + * @param anObject The object whose events we are logging. This object is not retained. + */ +- (instancetype)initWithObject:(id)anObject; + +- (void)logEventWithBacktrace:(nullable NSArray *)backtrace format:(NSString *)format, ... NS_FORMAT_FUNCTION(2, 3); + +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASEventLog.mm b/submodules/AsyncDisplayKit/Source/Details/ASEventLog.mm new file mode 100644 index 0000000000..25ecc29968 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASEventLog.mm @@ -0,0 +1,121 @@ +// +// ASEventLog.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@implementation ASEventLog { + AS::RecursiveMutex __instanceLock__; + + // The index of the most recent log entry. -1 until first entry. + NSInteger _eventLogHead; + + // A description of the object we're logging for. This is immutable. + NSString *_objectDescription; +} + +/** + * Even just when debugging, all these events can take up considerable memory. + * Store them in a shared NSCache to limit the total consumption. + */ ++ (NSCache *> *)contentsCache +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[NSCache alloc] init]; + }); + return cache; +} + +- (instancetype)initWithObject:(id)anObject +{ + if ((self = [super init])) { + _objectDescription = ASObjectDescriptionMakeTiny(anObject); + _eventLogHead = -1; + } + return self; +} + +- (instancetype)init +{ + // This method is marked unavailable so the compiler won't let them call it. + ASDisplayNodeFailAssert(@"Failed to call initWithObject:"); + return nil; +} + +- (void)logEventWithBacktrace:(NSArray *)backtrace format:(NSString *)format, ... +{ + va_list args; + va_start(args, format); + ASTraceEvent *event = [[ASTraceEvent alloc] initWithBacktrace:backtrace + format:format + arguments:args]; + va_end(args); + + AS::MutexLocker l(__instanceLock__); + NSCache *cache = [ASEventLog contentsCache]; + NSMutableArray *events = [cache objectForKey:self]; + if (events == nil) { + events = [NSMutableArray arrayWithObject:event]; + [cache setObject:events forKey:self]; + _eventLogHead = 0; + return; + } + + // Increment the head index. + _eventLogHead = (_eventLogHead + 1) % ASEVENTLOG_CAPACITY; + if (_eventLogHead < events.count) { + [events replaceObjectAtIndex:_eventLogHead withObject:event]; + } else { + [events insertObject:event atIndex:_eventLogHead]; + } +} + +- (NSArray *)events +{ + NSMutableArray *events = [[ASEventLog contentsCache] objectForKey:self]; + if (events == nil) { + return nil; + } + + AS::MutexLocker l(__instanceLock__); + NSUInteger tail = (_eventLogHead + 1); + NSUInteger count = events.count; + + NSMutableArray *result = [NSMutableArray array]; + + // Start from `tail` and go through array, wrapping around when we exceed end index. + for (NSUInteger actualIndex = 0; actualIndex < ASEVENTLOG_CAPACITY; actualIndex++) { + NSInteger ringIndex = (tail + actualIndex) % ASEVENTLOG_CAPACITY; + if (ringIndex < count) { + [result addObject:events[ringIndex]]; + } + } + return result; +} + +- (NSString *)description +{ + /** + * This description intentionally doesn't follow the standard description format. + * Since this is a log, it's important for the description to look a certain way, and + * the formal description style doesn't allow for newlines and has a ton of punctuation. + */ + NSArray *events = [self events]; + if (events == nil) { + return [NSString stringWithFormat:@"Event log for %@ was purged to conserve memory.", _objectDescription]; + } else { + return [NSString stringWithFormat:@"Event log for %@. Events: %@", _objectDescription, events]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASGraphicsContext.h b/submodules/AsyncDisplayKit/Source/Details/ASGraphicsContext.h new file mode 100644 index 0000000000..d22d3806b0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASGraphicsContext.h @@ -0,0 +1,50 @@ +// +// ASGraphicsContext.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@class UIImage; + +/** + * Functions for creating one-shot graphics contexts that do not have to copy + * their contents when an image is generated from them. This is efficient + * for our use, since we do not reuse graphics contexts. + * + * The API mirrors the UIGraphics API, with the exception that forming an image + * ends the context as well. + * + * Note: You must not mix-and-match between ASGraphics* and UIGraphics* functions + * within the same drawing operation. + */ + +NS_ASSUME_NONNULL_BEGIN + +/** + * Creates a one-shot context. + * + * Behavior is the same as UIGraphicsBeginImageContextWithOptions. + */ +AS_EXTERN void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale); + +/** + * Generates and image and ends the current one-shot context. + * + * Behavior is the same as UIGraphicsGetImageFromCurrentImageContext followed by UIGraphicsEndImageContext. + */ +AS_EXTERN UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED; + +/** + * Call this if you want to end the current context without making an image. + * + * Behavior is the same as UIGraphicsEndImageContext. + */ +AS_EXTERN void ASGraphicsEndImageContext(void); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASGraphicsContext.mm b/submodules/AsyncDisplayKit/Source/Details/ASGraphicsContext.mm new file mode 100644 index 0000000000..b950613d0d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASGraphicsContext.mm @@ -0,0 +1,167 @@ +// +// ASGraphicsContext.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import + +/** + * Our version of the private CGBitmapGetAlignedBytesPerRow function. + * + * In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32 + * in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that + * the bytes-per-row for a 1x1 context from the system is 32. + */ +static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) { + // Add 31 then zero out low 5 bits. + return (baseValue + 31) & ~0x1F; +} + +/** + * A key used to associate CGContextRef -> NSMutableData, nonatomic retain. + * + * That way the data will be released when the context dies. If they pull an image, + * we will retain the data object (in a CGDataProvider) before releasing the context. + */ +static UInt8 __contextDataAssociationKey; + +#pragma mark - Graphics Contexts + +void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIGraphicsBeginImageContextWithOptions(size, opaque, scale); + return; + } + + // We use "reference contexts" to get device-specific options that UIKit + // uses. + static dispatch_once_t onceToken; + static CGContextRef refCtxOpaque; + static CGContextRef refCtxTransparent; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1); + refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext()); + ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?"); + UIGraphicsEndImageContext(); + + // Make transparent ref context. + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1); + refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext()); + UIGraphicsEndImageContext(); + }); + + // These options are taken from UIGraphicsBeginImageContext. + CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent; + CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx); + + if (scale == 0) { + scale = ASScreenScale(); + } + size_t intWidth = (size_t)ceil(size.width * scale); + size_t intHeight = (size_t)ceil(size.height * scale); + size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx); + size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8; + bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow); + size_t bufferSize = bytesPerRow * intHeight; + CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx); + + // We create our own buffer, and wrap the context around that. This way we can prevent + // the copy that usually gets made when you form a CGImage from the context. + ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize]; + + CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo); + + // Transfer ownership of the data to the context. So that if the context + // is destroyed before we create an image from it, the data will be released. + objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Set the CTM to account for iOS orientation & specified scale. + // If only we could use CGContextSetBaseCTM. It doesn't + // seem like there are any consequences for our use case + // but we'll be on the look out. The internet hinted that it + // affects shadowing but I tested and shadowing works. + CGContextTranslateCTM(context, 0, intHeight); + CGContextScaleCTM(context, scale, -scale); + + // Save the state so we can restore it and recover our scale in GetImageAndEnd + CGContextSaveGState(context); + + // Transfer context ownership to the UIKit stack. + UIGraphicsPushContext(context); + CGContextRelease(context); +} + +UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; + } + + // Pop the context and make sure we have one. + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context == NULL) { + ASDisplayNodeCFailAssert(@"Can't end image context without having begun one."); + return nil; + } + + // Read the device-specific ICC-based color space to use for the image. + // For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage + // generates an image in a device-specific color space (for wide color support). + // We replicate that behavior, even though at this time CA does not + // require the image to be in this space. Plain DeviceRGB images seem + // to be treated exactly the same, but better safe than sorry. + static CGColorSpaceRef imageColorSpace; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext(); + imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage)); + ASDisplayNodeCAssertNotNil(imageColorSpace, nil); + UIGraphicsEndImageContext(); + }); + + // Retrieve our buffer and create a CGDataProvider from it. + ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey); + ASDisplayNodeCAssertNotNil(buffer, nil); + CGDataProviderRef provider = [buffer createDataProviderAndInvalidate]; + + // Create the CGImage. Options taken from CGBitmapContextCreateImage. + CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault); + CGDataProviderRelease(provider); + + // We saved our GState right after setting the CTM so that we could restore it + // here and get the original scale back. + CGContextRestoreGState(context); + CGFloat scale = CGContextGetCTM(context).a; + + // Note: popping from the UIKit stack will probably destroy the context. + context = NULL; + UIGraphicsPopContext(); + + UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp]; + CGImageRelease(cgImg); + return result; +} + +void ASGraphicsEndImageContext() +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIGraphicsEndImageContext(); + return; + } + + UIGraphicsPopContext(); +} diff --git a/submodules/AsyncDisplayKit/Source/Details/ASHashing.h b/submodules/AsyncDisplayKit/Source/Details/ASHashing.h new file mode 100644 index 0000000000..084d74078a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASHashing.h @@ -0,0 +1,40 @@ +// +// ASHashing.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * When std::hash is unavailable, this function will hash a bucket o' bits real fast. + * The hashing algorithm is copied from CoreFoundation's private function CFHashBytes. + * https://opensource.apple.com/source/CF/CF-1153.18/CFUtilities.c.auto.html + * + * Simple example: + * CGRect myRect = { ... }; + * ASHashBytes(&myRect, sizeof(myRect)); + * + * Example: + * struct { + * NSUInteger imageHash; + * CGSize size; + * } data = { + * _image.hash, + * _bounds.size + * }; + * return ASHashBytes(&data, sizeof(data)); + * + * @warning: If a struct has padding, any fields that are intiailized in {} + * will have garbage data for their padding, which will break this hash! Either + * use `pragma clang diagnostic warning "-Wpadded"` around your struct definition + * or manually initialize the fields of your struct (`myStruct.x = 7;` etc). + */ +AS_EXTERN NSUInteger ASHashBytes(void *bytes, size_t length); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASHashing.mm b/submodules/AsyncDisplayKit/Source/Details/ASHashing.mm new file mode 100644 index 0000000000..17bf66bd82 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASHashing.mm @@ -0,0 +1,38 @@ +// +// ASHashing.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define ELF_STEP(B) T1 = (H << 4) + B; T2 = T1 & 0xF0000000; if (T2) T1 ^= (T2 >> 24); T1 &= (~T2); H = T1; + +/** + * The hashing algorithm copied from CoreFoundation CFHashBytes function. + * https://opensource.apple.com/source/CF/CF-1153.18/CFUtilities.c.auto.html + */ +NSUInteger ASHashBytes(void *bytesarg, size_t length) { + /* The ELF hash algorithm, used in the ELF object file format */ + uint8_t *bytes = (uint8_t *)bytesarg; + UInt32 H = 0, T1, T2; + SInt32 rem = (SInt32)length; + while (3 < rem) { + ELF_STEP(bytes[length - rem]); + ELF_STEP(bytes[length - rem + 1]); + ELF_STEP(bytes[length - rem + 2]); + ELF_STEP(bytes[length - rem + 3]); + rem -= 4; + } + switch (rem) { + case 3: ELF_STEP(bytes[length - 3]); + case 2: ELF_STEP(bytes[length - 2]); + case 1: ELF_STEP(bytes[length - 1]); + case 0: ; + } + return H; +} + +#undef ELF_STEP diff --git a/submodules/AsyncDisplayKit/Source/Details/ASHighlightOverlayLayer.h b/submodules/AsyncDisplayKit/Source/Details/ASHighlightOverlayLayer.h new file mode 100644 index 0000000000..aff6694bf1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASHighlightOverlayLayer.h @@ -0,0 +1,51 @@ +// +// ASHighlightOverlayLayer.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASHighlightOverlayLayer : CALayer + +/** + @summary Initializes with CGRects for the highlighting, in the targetLayer's coordinate space. + + @desc This is the designated initializer. + + @param rects Array containing CGRects wrapped in NSValue. + @param targetLayer The layer that the rects are relative to. The rects will be translated to the receiver's coordinate space when rendering. + */ +- (instancetype)initWithRects:(NSArray *)rects targetLayer:(nullable CALayer *)targetLayer; + +/** + @summary Initializes with CGRects for the highlighting, in the receiver's coordinate space. + + @param rects Array containing CGRects wrapped in NSValue. + */ +- (instancetype)initWithRects:(NSArray *)rects; + +@property (nullable, nonatomic) __attribute__((NSObject)) CGColorRef highlightColor; +@property (nonatomic, weak) CALayer *targetLayer; + +@end + +@interface CALayer (ASHighlightOverlayLayerSupport) + +/** + @summary Set to YES to indicate to a sublayer that this is where highlight overlay layers (for pressed states) should + be added so that the highlight won't be clipped by a neighboring layer. + */ +@property (nonatomic, setter=as_setAllowsHighlightDrawing:) BOOL as_allowsHighlightDrawing; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASHighlightOverlayLayer.mm b/submodules/AsyncDisplayKit/Source/Details/ASHighlightOverlayLayer.mm new file mode 100644 index 0000000000..03cf468165 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASHighlightOverlayLayer.mm @@ -0,0 +1,134 @@ +// +// ASHighlightOverlayLayer.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import + +static const CGFloat kCornerRadius = 2.5; +static const UIEdgeInsets padding = {2, 4, 1.5, 4}; + +@implementation ASHighlightOverlayLayer +{ + NSArray *_rects; +} + ++ (id)defaultValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"contentsScale"]) { + return @(ASScreenScale()); + } else if ([key isEqualToString:@"highlightColor"]) { + CGFloat components[] = {0, 0, 0, 0.25}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGColorRef color = CGColorCreate(colorSpace, components); + CGColorSpaceRelease(colorSpace); + return CFBridgingRelease(color); + } else { + return [super defaultValueForKey:key]; + } +} + ++ (BOOL)needsDisplayForKey:(NSString *)key +{ + if ([key isEqualToString:@"bounds"]) { + return YES; + } else { + return [super needsDisplayForKey:key]; + } +} + ++ (id)defaultActionForKey:(NSString *)event +{ + return (id)[NSNull null]; +} + +- (instancetype)initWithRects:(NSArray *)rects +{ + return [self initWithRects:rects targetLayer:nil]; +} + +- (instancetype)initWithRects:(NSArray *)rects targetLayer:(id)targetLayer +{ + if (self = [super init]) { + _rects = [rects copy]; + _targetLayer = targetLayer; + } + return self; +} + +@dynamic highlightColor; + +- (void)drawInContext:(CGContextRef)ctx +{ + [super drawInContext:ctx]; + + CGAffineTransform affine = CGAffineTransformIdentity; + CGMutablePathRef highlightPath = CGPathCreateMutable(); + CALayer *targetLayer = self.targetLayer; + + for (NSValue *value in _rects) { + CGRect rect = [value CGRectValue]; + + // Don't highlight empty rects. + if (CGRectIsEmpty(rect)) { + continue; + } + + if (targetLayer != nil) { + rect = [self convertRect:rect fromLayer:targetLayer]; + } + rect = CGRectMake(std::round(rect.origin.x), std::round(rect.origin.y), std::round(rect.size.width), std::round(rect.size.height)); + + CGFloat minX = rect.origin.x - padding.left; + CGFloat maxX = CGRectGetMaxX(rect) + padding.right; + CGFloat midX = (maxX - minX) / 2 + minX; + CGFloat minY = rect.origin.y - padding.top; + CGFloat maxY = CGRectGetMaxY(rect) + padding.bottom; + CGFloat midY = (maxY - minY) / 2 + minY; + + CGPathMoveToPoint(highlightPath, &affine, minX, midY); + CGPathAddArcToPoint(highlightPath, &affine, minX, maxY, midX, maxY, kCornerRadius); + CGPathAddArcToPoint(highlightPath, &affine, maxX, maxY, maxX, midY, kCornerRadius); + CGPathAddArcToPoint(highlightPath, &affine, maxX, minY, midX, minY, kCornerRadius); + CGPathAddArcToPoint(highlightPath, &affine, minX, minY, minX, midY, kCornerRadius); + CGPathCloseSubpath(highlightPath); + } + + CGContextAddPath(ctx, highlightPath); + CGContextSetFillColorWithColor(ctx, self.highlightColor); + CGContextDrawPath(ctx, kCGPathFill); + CGPathRelease(highlightPath); +} + +- (CALayer *)hitTest:(CGPoint)p +{ + // Don't handle taps + return nil; +} + +@end + +@implementation CALayer (ASHighlightOverlayLayerSupport) + +static NSString *kAllowsHighlightDrawingKey = @"allows_highlight_drawing"; + +- (BOOL)as_allowsHighlightDrawing +{ + return [[self valueForKey:kAllowsHighlightDrawingKey] boolValue]; +} + +- (void)as_setAllowsHighlightDrawing:(BOOL)allowsHighlightDrawing +{ + [self setValue:@(allowsHighlightDrawing) forKey:kAllowsHighlightDrawingKey]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASImageContainerProtocolCategories.h b/submodules/AsyncDisplayKit/Source/Details/ASImageContainerProtocolCategories.h new file mode 100644 index 0000000000..44ddc354fd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASImageContainerProtocolCategories.h @@ -0,0 +1,19 @@ +// +// ASImageContainerProtocolCategories.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface UIImage (ASImageContainerProtocol) + +@end + +@interface NSData (ASImageContainerProtocol) + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASImageContainerProtocolCategories.mm b/submodules/AsyncDisplayKit/Source/Details/ASImageContainerProtocolCategories.mm new file mode 100644 index 0000000000..c9316c32ab --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASImageContainerProtocolCategories.mm @@ -0,0 +1,38 @@ +// +// ASImageContainerProtocolCategories.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@implementation UIImage (ASImageContainerProtocol) + +- (UIImage *)asdk_image +{ + return self; +} + +- (NSData *)asdk_animatedImageData +{ + return nil; +} + +@end + +@implementation NSData (ASImageContainerProtocol) + +- (UIImage *)asdk_image +{ + return nil; +} + +- (NSData *)asdk_animatedImageData +{ + return self; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASImageProtocols.h b/submodules/AsyncDisplayKit/Source/Details/ASImageProtocols.h new file mode 100644 index 0000000000..fae3b9f5b4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASImageProtocols.h @@ -0,0 +1,240 @@ +// +// ASImageProtocols.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASAnimatedImageProtocol; + +@protocol ASImageContainerProtocol + +- (nullable UIImage *)asdk_image; +- (nullable NSData *)asdk_animatedImageData; + +@end + +typedef void(^ASImageCacherCompletion)(id _Nullable imageFromCache); + +@protocol ASImageCacheProtocol + +/** + @abstract Attempts to fetch an image with the given URL from the cache. + @param URL The URL of the image to retrieve from the cache. + @param callbackQueue The queue to call `completion` on. + @param completion The block to be called when the cache has either hit or missed. + @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image. This method should not block + the calling thread as it is likely to be called from the main thread. + */ +- (void)cachedImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + completion:(ASImageCacherCompletion)completion; + +@optional + +/** + @abstract Attempts to fetch an image with the given URL from a memory cache. + @param URL The URL of the image to retrieve from the cache. + @discussion This method exists to support synchronous rendering of nodes. Before the layer is drawn, this method + is called to attempt to get the image out of the cache synchronously. This allows drawing to occur on the main thread + if displaysAsynchronously is set to NO or recursivelyEnsureDisplaySynchronously: has been called. + + This method *should* block the calling thread to fetch the image from a fast memory cache. It is OK to return nil from + this method and instead support only cachedImageWithURL:callbackQueue:completion: however, synchronous rendering will + not be possible. + */ +- (nullable id )synchronouslyFetchedCachedImageWithURL:(NSURL *)URL; + +/** + @abstract Called during clearPreloadedData. Allows the cache to optionally trim items. + @note Depending on your caches implementation you may *not* wish to respond to this method. It is however useful + if you have a memory and disk cache in which case you'll likely want to clear out the memory cache. + */ +- (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL; + +@end + +/** + @param image The image that was downloaded, if the image could be successfully downloaded; nil otherwise. + @param error An error describing why the download of `URL` failed, if the download failed; nil otherwise. + @param downloadIdentifier The identifier for the download task that completed. + @param userInfo Any additional info that your downloader would like to communicate through Texture. + */ +typedef void(^ASImageDownloaderCompletion)(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo); + +/** + @param progress The progress of the download, in the range of (0.0, 1.0), inclusive. + */ +typedef void(^ASImageDownloaderProgress)(CGFloat progress); +typedef void(^ASImageDownloaderProgressImage)(UIImage *progressImage, CGFloat progress, id _Nullable downloadIdentifier); + +typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { + ASImageDownloaderPriorityPreload = 0, + ASImageDownloaderPriorityImminent, + ASImageDownloaderPriorityVisible +}; + +@protocol ASImageDownloaderProtocol + +@required + +/** + @abstract Downloads an image with the given URL. + @param URL The URL of the image to download. + @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. + @param downloadProgress The block to be invoked when the download of `URL` progresses. + @param completion The block to be invoked when the download has completed, or has failed. + @discussion This method is likely to be called on the main thread, so any custom implementations should make sure to background any expensive download operations. + @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must + retain the identifier if you wish to use it later. + */ +- (nullable id)downloadImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion; + +/** + @abstract Cancels an image download. + @param downloadIdentifier The opaque download identifier object returned from + `downloadImageWithURL:callbackQueue:downloadProgress:completion:`. + @discussion This method has no effect if `downloadIdentifier` is nil. + */ +- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier; + +@optional + +/** + @abstract Downloads an image with the given URL. + @param URL The URL of the image to download. + @param priority The priority at which the image should be downloaded. + @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. + @param downloadProgress The block to be invoked when the download of `URL` progresses. + @param completion The block to be invoked when the download has completed, or has failed. + @discussion This method is likely to be called on the main thread, so any custom implementations should make sure to background any expensive download operations. + @note If this method is implemented, it will be called instead of the required method (`downloadImageWithURL:callbackQueue:downloadProgress:completion:`). + @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must + retain the identifier if you wish to use it later. + */ +- (nullable id)downloadImageWithURL:(NSURL *)URL + priority:(ASImageDownloaderPriority)priority + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion; + +/** + @abstract Cancels an image download, however indicating resume data should be stored in case of redownload. + @param downloadIdentifier The opaque download identifier object returned from + `downloadImageWithURL:callbackQueue:downloadProgress:completion:`. + @discussion This method has no effect if `downloadIdentifier` is nil. If implemented, this method + may be called instead of `cancelImageDownloadForIdentifier:` in cases where ASDK believes there's a chance + the image download will be resumed (currently when an image exits preload range). You can use this to store + any data that has already been downloaded for use in resuming the download later. + */ +- (void)cancelImageDownloadWithResumePossibilityForIdentifier:(id)downloadIdentifier; + +/** + @abstract Return an object that conforms to ASAnimatedImageProtocol + @param animatedImageData Data that represents an animated image. + */ +- (nullable id )animatedImageWithData:(NSData *)animatedImageData; + + +/** + @abstract Sets block to be called when a progress image is available. + @param progressBlock The block to be invoked when the download has a progressive render of an image available. + @param callbackQueue The queue to call `progressImageBlock` on. + @param downloadIdentifier The opaque download identifier object returned from + `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. + */ +- (void)setProgressImageBlock:(nullable ASImageDownloaderProgressImage)progressBlock + callbackQueue:(dispatch_queue_t)callbackQueue + withDownloadIdentifier:(id)downloadIdentifier; + +/** + @abstract Called to indicate what priority an image should be downloaded at. + @param priority The priority at which the image should be downloaded. + @param downloadIdentifier The opaque download identifier object returned from + `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. + */ +- (void)setPriority:(ASImageDownloaderPriority)priority +withDownloadIdentifier:(id)downloadIdentifier; + +@end + +@protocol ASAnimatedImageProtocol + +@optional + +/** + @abstract A block which receives the cover image. Should be called when the objects cover image is ready. + */ +@property (nonatomic) void (^coverImageReadyCallback)(UIImage *coverImage); + +/** + @abstract Returns whether the supplied data contains a supported animated image format. + @param data the data to check if contains a supported animated image. + */ +- (BOOL)isDataSupported:(NSData *)data; + + +@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) 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 + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASIntegerMap.h b/submodules/AsyncDisplayKit/Source/Details/ASIntegerMap.h new file mode 100644 index 0000000000..ece6f5a8bf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASIntegerMap.h @@ -0,0 +1,68 @@ +// +// ASIntegerMap.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An objective-C wrapper for unordered_map. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASIntegerMap : NSObject + +/** + * Creates a map based on the specified update to an array. + * + * If oldCount is 0, returns the empty map. + * If deleted and inserted are empty, returns the identity map. + */ ++ (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount + deleted:(nullable NSIndexSet *)deleted + inserted:(nullable NSIndexSet *)inserted NS_RETURNS_RETAINED; + +/** + * A singleton that maps each integer to itself. Its inverse is itself. + * + * Note: You cannot mutate this. + */ +@property (class, readonly) ASIntegerMap *identityMap; ++ (ASIntegerMap *)identityMap NS_RETURNS_RETAINED; + +/** + * A singleton that returns NSNotFound for all keys. Its inverse is itself. + * + * Note: You cannot mutate this. + */ +@property (class, readonly) ASIntegerMap *emptyMap; ++ (ASIntegerMap *)emptyMap NS_RETURNS_RETAINED; + +/** + * Retrieves the integer for a given key, or NSNotFound if the key is not found. + * + * @param key A key to lookup the value for. + */ +- (NSInteger)integerForKey:(NSInteger)key; + +/** + * Sets the value for a given key. + * + * @param value The new value. + * @param key The key to store the value for. + */ +- (void)setInteger:(NSInteger)value forKey:(NSInteger)key; + +/** + * Create and return a map with the inverse mapping. + */ +- (ASIntegerMap *)inverseMap; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASIntegerMap.mm b/submodules/AsyncDisplayKit/Source/Details/ASIntegerMap.mm new file mode 100644 index 0000000000..b07a7a1806 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASIntegerMap.mm @@ -0,0 +1,185 @@ +// +// ASIntegerMap.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASIntegerMap.h" +#import +#import +#import +#import + +/** + * This is just a friendly Objective-C interface to unordered_map + */ +@interface ASIntegerMap () +@end + +@implementation ASIntegerMap { + std::unordered_map _map; + BOOL _isIdentity; + BOOL _isEmpty; + BOOL _immutable; // identity map and empty mape are immutable. +} + +#pragma mark - Singleton + ++ (ASIntegerMap *)identityMap NS_RETURNS_RETAINED +{ + static ASIntegerMap *identityMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + identityMap = [[ASIntegerMap alloc] init]; + identityMap->_isIdentity = YES; + identityMap->_immutable = YES; + }); + return identityMap; +} + ++ (ASIntegerMap *)emptyMap NS_RETURNS_RETAINED +{ + static ASIntegerMap *emptyMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + emptyMap = [[ASIntegerMap alloc] init]; + emptyMap->_isEmpty = YES; + emptyMap->_immutable = YES; + }); + return emptyMap; +} + ++ (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount deleted:(NSIndexSet *)deletions inserted:(NSIndexSet *)insertions NS_RETURNS_RETAINED +{ + if (oldCount == 0) { + return ASIntegerMap.emptyMap; + } + + if (deletions.count == 0 && insertions.count == 0) { + return ASIntegerMap.identityMap; + } + + ASIntegerMap *result = [[ASIntegerMap alloc] init]; + // Start with the old indexes + NSMutableIndexSet *indexes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)]; + + // Descending order, shift deleted ranges left + [deletions enumerateRangesWithOptions:NSEnumerationReverse usingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [indexes shiftIndexesStartingAtIndex:NSMaxRange(range) by:-range.length]; + }]; + + // Ascending order, shift inserted ranges right + [insertions enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [indexes shiftIndexesStartingAtIndex:range.location by:range.length]; + }]; + + __block NSInteger oldIndex = 0; + [indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + // Note we advance oldIndex unconditionally, not newIndex + for (NSInteger newIndex = range.location; newIndex < NSMaxRange(range); oldIndex++) { + if ([deletions containsIndex:oldIndex]) { + // index was deleted, do nothing, just let oldIndex advance. + } else { + // assign the next index for this item. + result->_map[oldIndex] = newIndex++; + } + } + }]; + return result; +} + +- (NSInteger)integerForKey:(NSInteger)key +{ + if (_isIdentity) { + return key; + } else if (_isEmpty) { + return NSNotFound; + } + + const auto result = _map.find(key); + return result != _map.end() ? result->second : NSNotFound; +} + +- (void)setInteger:(NSInteger)value forKey:(NSInteger)key +{ + if (_immutable) { + ASDisplayNodeFailAssert(@"Cannot mutate special integer map: %@", self); + return; + } + + _map[key] = value; +} + +- (ASIntegerMap *)inverseMap +{ + if (_isIdentity || _isEmpty) { + return self; + } + + const auto result = [[ASIntegerMap alloc] init]; + + for (const auto &e : _map) { + result->_map[e.second] = e.first; + } + return result; +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + if (_immutable) { + return self; + } + + const auto newMap = [[ASIntegerMap allocWithZone:zone] init]; + newMap->_map = _map; + return newMap; +} + +#pragma mark - Description + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if (_isIdentity) { + [result addObject:@{ @"map": @"" }]; + } else if (_isEmpty) { + [result addObject:@{ @"map": @"" }]; + } else { + // { 1->2 3->4 5->6 } + NSMutableString *str = [NSMutableString string]; + for (const auto &e : _map) { + [str appendFormat:@" %ld->%ld", (long)e.first, (long)e.second]; + } + // Remove leading space + if (str.length > 0) { + [str deleteCharactersInRange:NSMakeRange(0, 1)]; + } + [result addObject:@{ @"map": str }]; + } + + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMakeWithoutObject([self propertiesForDescription]); +} + +- (BOOL)isEqual:(id)object +{ + if ([super isEqual:object]) { + return YES; + } + + if (ASIntegerMap *otherMap = ASDynamicCast(object, ASIntegerMap)) { + return otherMap->_map == _map; + } + return NO; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASLayoutController.h b/submodules/AsyncDisplayKit/Source/Details/ASLayoutController.h new file mode 100644 index 0000000000..d0e2f60907 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASLayoutController.h @@ -0,0 +1,40 @@ +// +// ASLayoutController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionElement, ASElementMap; + +struct ASDirectionalScreenfulBuffer { + CGFloat positiveDirection; // Positive relative to iOS Core Animation layer coordinate space. + CGFloat negativeDirection; +}; +typedef struct ASDirectionalScreenfulBuffer ASDirectionalScreenfulBuffer; + +@protocol ASLayoutController + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +- (NSHashTable *)elementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType map:(ASElementMap *)map; + +- (void)allElementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode displaySet:(NSHashTable * _Nullable * _Nullable)displaySet preloadSet:(NSHashTable * _Nullable * _Nullable)preloadSet map:(ASElementMap *)map; + +@optional + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASLayoutRangeType.h b/submodules/AsyncDisplayKit/Source/Details/ASLayoutRangeType.h new file mode 100644 index 0000000000..d4af470c06 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASLayoutRangeType.h @@ -0,0 +1,68 @@ +// +// ASLayoutRangeType.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +typedef struct { + CGFloat leadingBufferScreenfuls; + CGFloat trailingBufferScreenfuls; +} ASRangeTuningParameters; + +AS_EXTERN ASRangeTuningParameters const ASRangeTuningParametersZero; + +AS_EXTERN BOOL ASRangeTuningParametersEqualToRangeTuningParameters(ASRangeTuningParameters lhs, ASRangeTuningParameters rhs); + +/** + * Each mode has a complete set of tuning parameters for range types. + * Depending on some conditions (including interface state and direction of the scroll view, state of rendering engine, etc), + * a range controller can choose which mode it should use at a given time. + */ +typedef NS_ENUM(NSInteger, ASLayoutRangeMode) { + ASLayoutRangeModeUnspecified = -1, + + /** + * Minimum mode is used when a range controller should limit the amount of work it performs. + * Thus, fewer views/layers are created and less data is fetched, saving system resources. + * Range controller can automatically switch to full mode when conditions change. + */ + ASLayoutRangeModeMinimum = 0, + + /** + * Normal/Full mode that a range controller uses to provide the best experience for end users. + * This mode is usually used for an active scroll view. + * A range controller under this requires more resources compare to minimum mode. + */ + ASLayoutRangeModeFull, + + /** + * Visible Only mode is used when a range controller should set its display and preload regions to only the size of their bounds. + * This causes all additional backing stores & preloaded data to be released, while ensuring a user revisiting the view will + * still be able to see the expected content. This mode is automatically set on all ASRangeControllers when the app suspends, + * allowing the operating system to keep the app alive longer and increase the chance it is still warm when the user returns. + */ + ASLayoutRangeModeVisibleOnly, + + /** + * Low Memory mode is used when a range controller should discard ALL graphics buffers, including for the area that would be visible + * the next time the user views it (bounds). The only range it preserves is Preload, which is limited to the bounds, allowing + * the content to be restored relatively quickly by re-decoding images (the compressed images are ~10% the size of the decoded ones, + * and text is a tiny fraction of its rendered size). + */ + ASLayoutRangeModeLowMemory +}; + +static NSInteger const ASLayoutRangeModeCount = 4; + +typedef NS_ENUM(NSInteger, ASLayoutRangeType) { + ASLayoutRangeTypeDisplay, + ASLayoutRangeTypePreload +}; + +static NSInteger const ASLayoutRangeTypeCount = 2; diff --git a/submodules/AsyncDisplayKit/Source/Details/ASMainSerialQueue.h b/submodules/AsyncDisplayKit/Source/Details/ASMainSerialQueue.h new file mode 100644 index 0000000000..405164f75d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASMainSerialQueue.h @@ -0,0 +1,19 @@ +// +// ASMainSerialQueue.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +AS_SUBCLASSING_RESTRICTED +@interface ASMainSerialQueue : NSObject + +@property (nonatomic, readonly) NSUInteger numberOfScheduledBlocks; +- (void)performBlockOnMainThread:(dispatch_block_t)block; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASMainSerialQueue.mm b/submodules/AsyncDisplayKit/Source/Details/ASMainSerialQueue.mm new file mode 100644 index 0000000000..06cb7c3b9b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASMainSerialQueue.mm @@ -0,0 +1,81 @@ +// +// ASMainSerialQueue.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +@interface ASMainSerialQueue () +{ + AS::Mutex _serialQueueLock; + NSMutableArray *_blocks; +} + +@end + +@implementation ASMainSerialQueue + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _blocks = [[NSMutableArray alloc] init]; + return self; +} + +- (NSUInteger)numberOfScheduledBlocks +{ + AS::MutexLocker l(_serialQueueLock); + return _blocks.count; +} + +- (void)performBlockOnMainThread:(dispatch_block_t)block +{ + + AS::UniqueLock l(_serialQueueLock); + [_blocks addObject:block]; + { + l.unlock(); + [self runBlocks]; + l.lock(); + } +} + +- (void)runBlocks +{ + dispatch_block_t mainThread = ^{ + AS::UniqueLock l(self->_serialQueueLock); + do { + dispatch_block_t block; + if (self->_blocks.count > 0) { + block = _blocks[0]; + [self->_blocks removeObjectAtIndex:0]; + } else { + break; + } + { + l.unlock(); + block(); + l.lock(); + } + } while (true); + }; + + ASPerformBlockOnMainThread(mainThread); +} + +- (NSString *)description +{ + return [[super description] stringByAppendingFormat:@" Blocks: %@", _blocks]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASMutableAttributedStringBuilder.h b/submodules/AsyncDisplayKit/Source/Details/ASMutableAttributedStringBuilder.h new file mode 100644 index 0000000000..d531549a0e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASMutableAttributedStringBuilder.h @@ -0,0 +1,65 @@ +// +// ASMutableAttributedStringBuilder.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/* + * Use this class to compose new attributed strings. You may use the normal + * attributed string calls on this the same way you would on a normal mutable + * attributed string, but it coalesces your changes into transactions on the + * actual string allowing improvements in performance. + * + * @discussion This is a use-once and throw away class for each string you make. + * Since this class is designed for increasing performance, we actually hand + * back the internally managed mutable attributed string in the + * `composedAttributedString` call. So once you make that call, any more + * changes will actually modify the string that was handed back to you in that + * method. + * + * Combination of multiple calls into single attribution is managed through + * merging of attribute dictionaries over ranges. For best performance, call + * collections of attributions over a single range together. So for instance, + * don't call addAttributes for range1, then range2, then range1 again. Group + * them together so you call addAttributes for both range1 together, and then + * range2. + * + * Also please note that switching between addAttribute and setAttributes in the + * middle of composition is a bad idea for performance because they have + * semantically different meanings, and trigger a commit of the pending + * attributes. + * + * Please note that ALL of the standard NSString methods are left unimplemented. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASMutableAttributedStringBuilder : NSMutableAttributedString + +- (instancetype)initWithString:(NSString *)str attributes:(nullable NSDictionary *)attrs; +- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr; + +- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str; +- (void)setAttributes:(nullable NSDictionary *)attrs range:(NSRange)range; + +- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range; +- (void)addAttributes:(NSDictionary *)attrs range:(NSRange)range; +- (void)removeAttribute:(NSString *)name range:(NSRange)range; + +- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString; +- (void)insertAttributedString:(NSAttributedString *)attrString atIndex:(NSUInteger)loc; +- (void)appendAttributedString:(NSAttributedString *)attrString; +- (void)deleteCharactersInRange:(NSRange)range; +- (void)setAttributedString:(NSAttributedString *)attrString; + +- (NSMutableAttributedString *)composedAttributedString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASMutableAttributedStringBuilder.mm b/submodules/AsyncDisplayKit/Source/Details/ASMutableAttributedStringBuilder.mm new file mode 100644 index 0000000000..b393fe1118 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASMutableAttributedStringBuilder.mm @@ -0,0 +1,254 @@ +// +// ASMutableAttributedStringBuilder.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@implementation ASMutableAttributedStringBuilder { + // Flag for the type of the current transaction (set or add) + BOOL _setRange; + // The range over which the currently pending transaction will occur + NSRange _pendingRange; + // The actual attribute dictionary that is being composed + NSMutableDictionary *_pendingRangeAttributes; + NSMutableAttributedString *_attrStr; + + // We delay initialization of the _attrStr until we need to + NSString *_initString; +} + +- (instancetype)init +{ + if (self = [super init]) { + _attrStr = [[NSMutableAttributedString alloc] init]; + _pendingRange.location = NSNotFound; + } + return self; +} + +- (instancetype)initWithString:(NSString *)str +{ + return [self initWithString:str attributes:@{}]; +} + +- (instancetype)initWithString:(NSString *)str attributes:(NSDictionary *)attrs +{ + if (self = [super init]) { + // We cache this in an ivar that we can lazily construct the attributed + // string with when we get to a forced commit point. + _initString = str; + // Triggers a creation of the _pendingRangeAttributes dictionary which then + // is filled with entries from the given attrs dict. + [[self _pendingRangeAttributes] addEntriesFromDictionary:attrs]; + _setRange = NO; + _pendingRange = NSMakeRange(0, _initString.length); + } + return self; +} + +- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr +{ + if (self = [super init]) { + _attrStr = [[NSMutableAttributedString alloc] initWithAttributedString:attrStr]; + _pendingRange.location = NSNotFound; + } + return self; +} + +- (NSMutableAttributedString *)_attributedString +{ + if (_attrStr == nil && _initString != nil) { + // We can lazily construct the attributed string if it hasn't already been + // created with the existing pending attributes. This is significantly + // faster if more attributes are added after initializing this instance + // and the new attributions are for the entire string anyway. + _attrStr = [[NSMutableAttributedString alloc] initWithString:_initString attributes:_pendingRangeAttributes]; + _pendingRangeAttributes = nil; + _pendingRange.location = NSNotFound; + _initString = nil; + } + + return _attrStr; +} + +#pragma mark - Pending attribution + +- (NSMutableDictionary *)_pendingRangeAttributes +{ + // Lazy dictionary creation. Call this if you want to force initialization, + // otherwise just use the ivar. + if (_pendingRangeAttributes == nil) { + _pendingRangeAttributes = [[NSMutableDictionary alloc] init]; + } + return _pendingRangeAttributes; +} + +- (void)_applyPendingRangeAttributions +{ + if (_attrStr == nil) { + // Trigger its creation if it doesn't exist. + [self _attributedString]; + } + + if (_pendingRangeAttributes.count == 0) { + return; + } + + if (_pendingRange.location == NSNotFound) { + return; + } + + if (_setRange) { + [[self _attributedString] setAttributes:_pendingRangeAttributes range:_pendingRange]; + } else { + [[self _attributedString] addAttributes:_pendingRangeAttributes range:_pendingRange]; + } + _pendingRangeAttributes = nil; + _pendingRange.location = NSNotFound; +} + +#pragma mark - Editing + +- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] replaceCharactersInRange:range withString:str]; +} + +- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] replaceCharactersInRange:range withAttributedString:attrString]; +} + +- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range +{ + if (_setRange) { + [self _applyPendingRangeAttributions]; + _setRange = NO; + } + + if (!NSEqualRanges(_pendingRange, range)) { + [self _applyPendingRangeAttributions]; + _pendingRange = range; + } + + NSMutableDictionary *pendingAttributes = [self _pendingRangeAttributes]; + pendingAttributes[name] = value; +} + +- (void)addAttributes:(NSDictionary *)attrs range:(NSRange)range +{ + if (_setRange) { + [self _applyPendingRangeAttributions]; + _setRange = NO; + } + + if (!NSEqualRanges(_pendingRange, range)) { + [self _applyPendingRangeAttributions]; + _pendingRange = range; + } + + NSMutableDictionary *pendingAttributes = [self _pendingRangeAttributes]; + [pendingAttributes addEntriesFromDictionary:attrs]; +} + +- (void)insertAttributedString:(NSAttributedString *)attrString atIndex:(NSUInteger)loc +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] insertAttributedString:attrString atIndex:loc]; +} + +- (void)appendAttributedString:(NSAttributedString *)attrString +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] appendAttributedString:attrString]; +} + +- (void)deleteCharactersInRange:(NSRange)range +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] deleteCharactersInRange:range]; +} + +- (void)setAttributedString:(NSAttributedString *)attrString +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] setAttributedString:attrString]; +} + +- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range +{ + if (!_setRange) { + [self _applyPendingRangeAttributions]; + _setRange = YES; + } + + if (!NSEqualRanges(_pendingRange, range)) { + [self _applyPendingRangeAttributions]; + _pendingRange = range; + } + + NSMutableDictionary *pendingAttributes = [self _pendingRangeAttributes]; + [pendingAttributes addEntriesFromDictionary:attrs]; +} + +- (void)removeAttribute:(NSString *)name range:(NSRange)range +{ + // This call looks like the other set/add functions, but in order for this + // function to perform as advertised we MUST first add the attributes we + // currently have pending. + [self _applyPendingRangeAttributions]; + + [[self _attributedString] removeAttribute:name range:range]; +} + +#pragma mark - Output + +- (NSMutableAttributedString *)composedAttributedString +{ + if (_pendingRangeAttributes.count > 0) { + [self _applyPendingRangeAttributions]; + } + return [self _attributedString]; +} + +#pragma mark - Forwarding + +- (NSUInteger)length +{ + // If we just want a length call, no need to lazily construct the attributed string + return _attrStr ? _attrStr.length : _initString.length; +} + +- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range +{ + return [[self _attributedString] attributesAtIndex:location effectiveRange:range]; +} + +- (NSString *)string +{ + return _attrStr ? _attrStr.string : _initString; +} + +- (NSMutableString *)mutableString +{ + return [[self _attributedString] mutableString]; +} + +- (void)beginEditing +{ + [[self _attributedString] beginEditing]; +} + +- (void)endEditing +{ + [[self _attributedString] endEditing]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASObjectDescriptionHelpers.h b/submodules/AsyncDisplayKit/Source/Details/ASObjectDescriptionHelpers.h new file mode 100644 index 0000000000..5a2823bd99 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASObjectDescriptionHelpers.h @@ -0,0 +1,65 @@ +// +// ASObjectDescriptionHelpers.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASDebugNameProvider + +@required +/** + * @abstract Name that is printed by ascii art string and displayed in description. + */ +@property (nullable, nonatomic, copy) NSString *debugName; + +@end + +/** + * Your base class should conform to this and override `-debugDescription` + * to call `[self propertiesForDebugDescription]` and use `ASObjectDescriptionMake` + * to return a string. Subclasses of this base class just need to override + * `propertiesForDebugDescription`, call super, and modify the result as needed. + */ +@protocol ASDebugDescriptionProvider +@required +- (NSMutableArray *)propertiesForDebugDescription; +@end + +/** + * Your base class should conform to this and override `-description` + * to call `[self propertiesForDescription]` and use `ASObjectDescriptionMake` + * to return a string. Subclasses of this base class just need to override + * `propertiesForDescription`, call super, and modify the result as needed. + */ +@protocol ASDescriptionProvider +@required +- (NSMutableArray *)propertiesForDescription; +@end + +AS_EXTERN NSString *ASGetDescriptionValueString(id object); + +/// Useful for structs etc. Returns e.g. { position = (0 0); frame = (0 0; 50 50) } +AS_EXTERN NSString *ASObjectDescriptionMakeWithoutObject(NSArray * _Nullable propertyGroups); + +/// Returns e.g. +AS_EXTERN NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray * _Nullable propertyGroups); + +/** + * Returns e.g. + * + * Note: `object` param is autoreleasing so that this function is dealloc-safe. + * No, unsafe_unretained isn't acceptable here – the optimizer may deallocate object early. + */ +AS_EXTERN NSString *ASObjectDescriptionMakeTiny(__autoreleasing id _Nullable object); + +AS_EXTERN NSString * _Nullable ASStringWithQuotesIfMultiword(NSString * _Nullable string); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASObjectDescriptionHelpers.mm b/submodules/AsyncDisplayKit/Source/Details/ASObjectDescriptionHelpers.mm new file mode 100644 index 0000000000..cbd6be0963 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASObjectDescriptionHelpers.mm @@ -0,0 +1,101 @@ +// +// ASObjectDescriptionHelpers.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +NSString *ASGetDescriptionValueString(id object) +{ + if ([object isKindOfClass:[NSValue class]]) { + // Use shortened NSValue descriptions + NSValue *value = object; + const char *type = value.objCType; + + if (strcmp(type, @encode(CGRect)) == 0) { + CGRect rect = [value CGRectValue]; + return [NSString stringWithFormat:@"(%g %g; %g %g)", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height]; + } else if (strcmp(type, @encode(CGSize)) == 0) { + return NSStringFromCGSize(value.CGSizeValue); + } else if (strcmp(type, @encode(CGPoint)) == 0) { + return NSStringFromCGPoint(value.CGPointValue); + } + + } else if ([object isKindOfClass:[NSIndexSet class]]) { + return [object as_smallDescription]; + } else if ([object isKindOfClass:[NSIndexPath class]]) { + // index paths like (0, 7) + NSIndexPath *indexPath = object; + NSMutableArray *strings = [NSMutableArray array]; + for (NSUInteger i = 0; i < indexPath.length; i++) { + [strings addObject:[NSString stringWithFormat:@"%lu", (unsigned long)[indexPath indexAtPosition:i]]]; + } + return [NSString stringWithFormat:@"(%@)", [strings componentsJoinedByString:@", "]]; + } else if ([object respondsToSelector:@selector(componentsJoinedByString:)]) { + // e.g. "[ ]" + return [NSString stringWithFormat:@"[ %@ ]", [object componentsJoinedByString:@" "]]; + } + return [object description]; +} + +NSString *_ASObjectDescriptionMakePropertyList(NSArray * _Nullable propertyGroups) +{ + NSMutableArray *components = [NSMutableArray array]; + for (NSDictionary *properties in propertyGroups) { + [properties enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + NSString *str; + if (key == (id)kCFNull) { + str = ASGetDescriptionValueString(obj); + } else { + str = [NSString stringWithFormat:@"%@ = %@", key, ASGetDescriptionValueString(obj)]; + } + [components addObject:str]; + }]; + } + return [components componentsJoinedByString:@"; "]; +} + +NSString *ASObjectDescriptionMakeWithoutObject(NSArray * _Nullable propertyGroups) +{ + return [NSString stringWithFormat:@"{ %@ }", _ASObjectDescriptionMakePropertyList(propertyGroups)]; +} + +NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray *propertyGroups) +{ + if (object == nil) { + return @"(null)"; + } + + NSMutableString *str = [NSMutableString stringWithFormat:@"<%s: %p", object_getClassName(object), object]; + + NSString *propList = _ASObjectDescriptionMakePropertyList(propertyGroups); + if (propList.length > 0) { + [str appendFormat:@"; %@", propList]; + } + [str appendString:@">"]; + return str; +} + +NSString *ASObjectDescriptionMakeTiny(__autoreleasing id object) { + return ASObjectDescriptionMake(object, nil); +} + +NSString *ASStringWithQuotesIfMultiword(NSString *string) { + if (string == nil) { + return nil; + } + + if ([string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]].location != NSNotFound) { + return [NSString stringWithFormat:@"\"%@\"", string]; + } else { + return string; + } +} diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPINRemoteImageDownloader.h b/submodules/AsyncDisplayKit/Source/Details/ASPINRemoteImageDownloader.h new file mode 100644 index 0000000000..dceca079e2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPINRemoteImageDownloader.h @@ -0,0 +1,84 @@ +// +// ASPINRemoteImageDownloader.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#if AS_PIN_REMOTE_IMAGE + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class PINRemoteImageManager; +@protocol PINRemoteImageCaching; + +@interface ASPINRemoteImageDownloader : NSObject + +/** + * A shared image downloader which can be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes. + * The userInfo provided by this downloader is an instance of `PINRemoteImageManagerResult`. + * + * This is the default downloader used by network backed image nodes if PINRemoteImage and PINCache are + * available. It uses PINRemoteImage's features to provide caching and progressive image downloads. + */ ++ (ASPINRemoteImageDownloader *)sharedDownloader NS_RETURNS_RETAINED; + +/** + * Sets the default NSURLSessionConfiguration that will be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes + * while loading images off the network. This must be specified early in the application lifecycle before + * `sharedDownloader` is accessed. + * + * @param configuration The session configuration that will be used by `sharedDownloader` + * + */ ++ (void)setSharedImageManagerWithConfiguration:(nullable NSURLSessionConfiguration *)configuration; + +/** + * Sets the default NSURLSessionConfiguration that will be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes + * while loading images off the network. This must be specified early in the application lifecycle before + * `sharedDownloader` is accessed. + * + * @param configuration The session configuration that will be used by `sharedDownloader` + * @param imageCache The cache to be used by PINRemoteImage - nil will set up a default cache: PINCache + * if it is available, PINRemoteImageBasicCache (NSCache) if not. + * + */ ++ (void)setSharedImageManagerWithConfiguration:(nullable NSURLSessionConfiguration *)configuration + imageCache:(nullable id)imageCache; + +/** + * Sets a custom preconfigured PINRemoteImageManager that will be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes + * while loading images off the network. This must be specified early in the application lifecycle before + * `sharedDownloader` is accessed. + * + * @param preconfiguredPINRemoteImageManager The preconfigured remote image manager that will be used by `sharedDownloader` + */ ++ (void)setSharedPreconfiguredRemoteImageManager:(PINRemoteImageManager *)preconfiguredPINRemoteImageManager; + +/** + * The shared instance of a @c PINRemoteImageManager used by all @c ASPINRemoteImageDownloaders + * + * @discussion you can use this method to access the shared manager. This is useful to share a cache + * and resources if you need to download images outside of an @c ASNetworkImageNode or + * @c ASMultiplexImageNode. It's also useful to access the memoryCache and diskCache to set limits + * or handle authentication challenges. + * + * @return An instance of a @c PINRemoteImageManager + */ +- (PINRemoteImageManager *)sharedPINRemoteImageManager; + +@end + +NS_ASSUME_NONNULL_END + +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPINRemoteImageDownloader.mm b/submodules/AsyncDisplayKit/Source/Details/ASPINRemoteImageDownloader.mm new file mode 100644 index 0000000000..84341b40c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPINRemoteImageDownloader.mm @@ -0,0 +1,393 @@ +// +// ASPINRemoteImageDownloader.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#if AS_PIN_REMOTE_IMAGE +#import + +#import +#import +#import + +#if __has_include () +#define PIN_ANIMATED_AVAILABLE 1 +#import +#import +#else +#define PIN_ANIMATED_AVAILABLE 0 +#endif + +#if __has_include() +#define PIN_WEBP_AVAILABLE 1 +#else +#define PIN_WEBP_AVAILABLE 0 +#endif + +#import +#import +#import + +static inline PINRemoteImageManagerPriority PINRemoteImageManagerPriorityWithASImageDownloaderPriority(ASImageDownloaderPriority priority) { + switch (priority) { + case ASImageDownloaderPriorityPreload: + return PINRemoteImageManagerPriorityLow; + + case ASImageDownloaderPriorityImminent: + return PINRemoteImageManagerPriorityDefault; + + case ASImageDownloaderPriorityVisible: + return PINRemoteImageManagerPriorityHigh; + } +} + +#if PIN_ANIMATED_AVAILABLE + +@interface ASPINRemoteImageDownloader () +@end + +@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) +@end + +@implementation PINCachedAnimatedImage (ASPINRemoteImageDownloader) + +- (BOOL)isDataSupported:(NSData *)data +{ + if ([data pin_isGIF]) { + return YES; + } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return YES; + } +#endif + return NO; +} + +@end +#endif + +// Declare two key methods on PINCache objects, avoiding a direct dependency on PINCache.h +@protocol ASPINCache +- (id)diskCache; +@end + +@protocol ASPINDiskCache +@property NSUInteger byteLimit; +@end + +@interface ASPINRemoteImageManager : PINRemoteImageManager +@end + +@implementation ASPINRemoteImageManager + +//Share image cache with sharedImageManager image cache. ++ (id )defaultImageCache +{ + static dispatch_once_t onceToken; + static id cache = nil; + dispatch_once(&onceToken, ^{ + cache = [[PINRemoteImageManager sharedImageManager] cache]; + if ([cache respondsToSelector:@selector(diskCache)]) { + id diskCache = [(id )cache diskCache]; + if ([diskCache respondsToSelector:@selector(setByteLimit:)]) { + // Set a default byteLimit. PINCache recently implemented a 50MB default (PR #201). + // Ensure that older versions of PINCache also have a byteLimit applied. + // NOTE: Using 20MB limit while large cache initialization is being optimized (Issue #144). + ((id )diskCache).byteLimit = 20 * 1024 * 1024; + } + } + }); + return cache; +} + +@end + +static ASPINRemoteImageDownloader *sharedDownloader = nil; +static PINRemoteImageManager *sharedPINRemoteImageManager = nil; + +@interface ASPINRemoteImageDownloader () +@end + +@implementation ASPINRemoteImageDownloader + ++ (ASPINRemoteImageDownloader *)sharedDownloader NS_RETURNS_RETAINED +{ + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + sharedDownloader = [[ASPINRemoteImageDownloader alloc] init]; + }); + return sharedDownloader; +} + ++ (void)setSharedImageManagerWithConfiguration:(nullable NSURLSessionConfiguration *)configuration +{ + NSAssert(sharedDownloader == nil, @"Singleton has been created and session can no longer be configured."); + PINRemoteImageManager *sharedManager = [self PINRemoteImageManagerWithConfiguration:configuration imageCache:nil]; + [self setSharedPreconfiguredRemoteImageManager:sharedManager]; +} + ++ (void)setSharedImageManagerWithConfiguration:(nullable NSURLSessionConfiguration *)configuration + imageCache:(nullable id)imageCache +{ + NSAssert(sharedDownloader == nil, @"Singleton has been created and session can no longer be configured."); + PINRemoteImageManager *sharedManager = [self PINRemoteImageManagerWithConfiguration:configuration imageCache:imageCache]; + [self setSharedPreconfiguredRemoteImageManager:sharedManager]; +} + +static dispatch_once_t shared_init_predicate; + ++ (void)setSharedPreconfiguredRemoteImageManager:(PINRemoteImageManager *)preconfiguredPINRemoteImageManager +{ + NSAssert(preconfiguredPINRemoteImageManager != nil, @"setSharedPreconfiguredRemoteImageManager requires a non-nil parameter"); + NSAssert1(sharedPINRemoteImageManager == nil, @"An instance of %@ has been set. Either configuration or preconfigured image manager can be set at a time and only once.", [[sharedPINRemoteImageManager class] description]); + + dispatch_once(&shared_init_predicate, ^{ + sharedPINRemoteImageManager = preconfiguredPINRemoteImageManager; + }); +} + ++ (PINRemoteImageManager *)PINRemoteImageManagerWithConfiguration:(nullable NSURLSessionConfiguration *)configuration imageCache:(nullable id)imageCache +{ + PINRemoteImageManager *manager = nil; +#if DEBUG + // Check that Carthage users have linked both PINRemoteImage & PINCache by testing for one file each + if (!(NSClassFromString(@"PINRemoteImageManager"))) { + NSException *e = [NSException + exceptionWithName:@"FrameworkSetupException" + reason:@"Missing the path to the PINRemoteImage framework." + userInfo:nil]; + @throw e; + } + if (!(NSClassFromString(@"PINCache"))) { + NSException *e = [NSException + exceptionWithName:@"FrameworkSetupException" + reason:@"Missing the path to the PINCache framework." + userInfo:nil]; + @throw e; + } +#endif +#if PIN_ANIMATED_AVAILABLE + manager = [[ASPINRemoteImageManager alloc] initWithSessionConfiguration:configuration + alternativeRepresentationProvider:[self sharedDownloader] + imageCache:imageCache]; +#else + manager = [[ASPINRemoteImageManager alloc] initWithSessionConfiguration:configuration + alternativeRepresentationProvider:nil + imageCache:imageCache]; +#endif + return manager; +} + +- (PINRemoteImageManager *)sharedPINRemoteImageManager +{ + dispatch_once(&shared_init_predicate, ^{ + sharedPINRemoteImageManager = [ASPINRemoteImageDownloader PINRemoteImageManagerWithConfiguration:nil imageCache:nil]; + }); + return sharedPINRemoteImageManager; +} + +- (BOOL)sharedImageManagerSupportsMemoryRemoval +{ + static BOOL sharedImageManagerSupportsMemoryRemoval = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedImageManagerSupportsMemoryRemoval = [[[self sharedPINRemoteImageManager] cache] respondsToSelector:@selector(removeObjectForKeyFromMemory:)]; + }); + return sharedImageManagerSupportsMemoryRemoval; +} + +#pragma mark ASImageProtocols + +#if PIN_ANIMATED_AVAILABLE +- (nullable id )animatedImageWithData:(NSData *)animatedImageData +{ + return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; +} +#endif + +- (id )synchronouslyFetchedCachedImageWithURL:(NSURL *)URL; +{ + PINRemoteImageManager *manager = [self sharedPINRemoteImageManager]; + PINRemoteImageManagerResult *result = [manager synchronousImageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode]; + +#if PIN_ANIMATED_AVAILABLE + if (result.alternativeRepresentation) { + return result.alternativeRepresentation; + } +#endif + return result.image; +} + +- (void)cachedImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + completion:(ASImageCacherCompletion)completion +{ + [[self sharedPINRemoteImageManager] imageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ +#if PIN_ANIMATED_AVAILABLE + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation); + } else { + completion(result.image); + } +#else + completion(result.image); +#endif + }]; + }]; +} + +- (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL +{ + if ([self sharedImageManagerSupportsMemoryRemoval]) { + PINRemoteImageManager *manager = [self sharedPINRemoteImageManager]; + NSString *key = [manager cacheKeyForURL:URL processorKey:nil]; + [[manager cache] removeObjectForKeyFromMemory:key]; + } +} + +- (nullable id)downloadImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion; +{ + return [self downloadImageWithURL:URL + priority:ASImageDownloaderPriorityImminent // maps to default priority + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; +} + +- (nullable id)downloadImageWithURL:(NSURL *)URL + priority:(ASImageDownloaderPriority)priority + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion +{ + PINRemoteImageManagerPriority pi_priority = PINRemoteImageManagerPriorityWithASImageDownloaderPriority(priority); + + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { + if (downloadProgress == nil) { return; } + + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + downloadProgress(completedBytes / (CGFloat)totalBytes); + }]; + }; + + PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ +#if PIN_ANIMATED_AVAILABLE + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation, result.error, result.UUID, result); + } else { + completion(result.image, result.error, result.UUID, result); + } +#else + completion(result.image, result.error, result.UUID, result); +#endif + }]; + }; + + // add "IgnoreCache" option since we have a caching API so we already checked it, not worth checking again. + // PINRemoteImage is responsible for coalescing downloads, and even if it wasn't, the tiny probability of + // extra downloads isn't worth the effort of rechecking caches every single time. In order to provide + // feedback to the consumer about whether images are cached, we can't simply make the cache a no-op and + // check the cache as part of this download. + return [[self sharedPINRemoteImageManager] downloadImageWithURL:URL + options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache + priority:pi_priority + progressImage:nil + progressDownload:progressDownload + completion:imageCompletion]; +} + +- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier +{ + ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID"); + [[self sharedPINRemoteImageManager] cancelTaskWithUUID:downloadIdentifier storeResumeData:NO]; +} + +- (void)cancelImageDownloadWithResumePossibilityForIdentifier:(id)downloadIdentifier +{ + ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID"); + [[self sharedPINRemoteImageManager] cancelTaskWithUUID:downloadIdentifier storeResumeData:YES]; +} + +- (void)setProgressImageBlock:(ASImageDownloaderProgressImage)progressBlock callbackQueue:(dispatch_queue_t)callbackQueue withDownloadIdentifier:(id)downloadIdentifier +{ + ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID"); + + if (progressBlock) { + [[self sharedPINRemoteImageManager] setProgressImageCallback:^(PINRemoteImageManagerResult * _Nonnull result) { + dispatch_async(callbackQueue, ^{ + progressBlock(result.image, result.renderedImageQuality, result.UUID); + }); + } ofTaskWithUUID:downloadIdentifier]; + } else { + [[self sharedPINRemoteImageManager] setProgressImageCallback:nil ofTaskWithUUID:downloadIdentifier]; + } +} + +- (void)setPriority:(ASImageDownloaderPriority)priority withDownloadIdentifier:(id)downloadIdentifier +{ + ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID"); + + PINRemoteImageManagerPriority pi_priority = PINRemoteImageManagerPriorityWithASImageDownloaderPriority(priority); + [[self sharedPINRemoteImageManager] setPriority:pi_priority ofTaskWithUUID:downloadIdentifier]; +} + +#pragma mark - PINRemoteImageManagerAlternateRepresentationProvider + +- (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageManagerDownloadOptions)options +{ +#if PIN_ANIMATED_AVAILABLE + if ([data pin_isAnimatedGIF]) { + return data; + } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return data; + } +#endif + +#endif + return nil; +} + +#pragma mark - Private + +/** + * If on main thread and queue is main, perform now. + * If queue is nil, assert and perform now. + * Otherwise, dispatch async to queue. + */ ++ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)(void))work +{ + if (work == nil) { + // No need to assert here, really. We aren't expecting any feedback from this method. + return; + } + + if (ASDisplayNodeThreadIsMain() && queue == dispatch_get_main_queue()) { + work(); + } else if (queue == nil) { + ASDisplayNodeFailAssert(@"Callback queue should not be nil."); + work(); + } else { + dispatch_async(queue, work); + } +} + +@end +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPageTable.h b/submodules/AsyncDisplayKit/Source/Details/ASPageTable.h new file mode 100644 index 0000000000..7e01e933a6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPageTable.h @@ -0,0 +1,124 @@ +// +// ASPageTable.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import + +@class ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents x and y coordinates of a page. + */ +typedef uintptr_t ASPageCoordinate; + +/** + * Returns a page coordinate with the given x and y values. Both of them must be less than 65,535. + */ +AS_EXTERN ASPageCoordinate ASPageCoordinateMake(uint16_t x, uint16_t y) AS_WARN_UNUSED_RESULT; + +/** + * Returns coordinate of the page that contains the specified point. + * Similar to CGRectContainsPoint, a point is considered inside a page if its lie inside the page or on the minimum X or minimum Y edge. + * + * @param point The point that the page at the returned should contain. Any negative of the point will be corrected to 0.0 + * + * @param pageSize The size of each page. + */ +AS_EXTERN ASPageCoordinate ASPageCoordinateForPageThatContainsPoint(CGPoint point, CGSize pageSize) AS_WARN_UNUSED_RESULT; + +AS_EXTERN uint16_t ASPageCoordinateGetX(ASPageCoordinate pageCoordinate) AS_WARN_UNUSED_RESULT; + +AS_EXTERN uint16_t ASPageCoordinateGetY(ASPageCoordinate pageCoordinate) AS_WARN_UNUSED_RESULT; + +AS_EXTERN CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSize pageSize) AS_WARN_UNUSED_RESULT; + +/** + * Returns coordinate pointers for pages that intersect the specified rect. For each pointer, use ASPageCoordinateFromPointer() to get the original coordinate. + * The specified rect is restricted to the bounds of a content rect that has an origin of {0, 0} and a size of the given contentSize. + * + * @param rect The rect intersecting the target pages. + * + * @param contentSize The combined size of all pages. + * + * @param pageSize The size of each page. + */ +AS_EXTERN NSPointerArray * _Nullable ASPageCoordinatesForPagesThatIntersectRect(CGRect rect, CGSize contentSize, CGSize pageSize) AS_WARN_UNUSED_RESULT; + +/** + * An alias for an NSMapTable created to store objects using ASPageCoordinates as keys. + * + * You should not call -objectForKey:, -setObject:forKey:, or -removeObjectForKey: + * on these objects. + */ +typedef NSMapTable ASPageTable; + +/** + * A page to array of layout attributes table. + */ +typedef ASPageTable *> ASPageToLayoutAttributesTable; + +/** + * A category for creating & using map tables meant for storing objects using ASPage as keys. + */ +@interface NSMapTable (ASPageTableMethods) + +/** + * Creates a new page table with (NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) for values. + */ ++ (ASPageTable *)pageTableForStrongObjectPointers NS_RETURNS_RETAINED; + +/** + * Creates a new page table with (NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) for values. + */ ++ (ASPageTable *)pageTableForWeakObjectPointers NS_RETURNS_RETAINED; + +/** + * Builds a new page to layout attributes from the given layout attributes. + * + * @param layoutAttributesEnumerator The layout attributes to build from + * + * @param contentSize The combined size of all pages. + * + * @param pageSize The size of each page. + */ ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize NS_RETURNS_RETAINED; + +/** + * Retrieves the object for a given page, or nil if the page is not found. + * + * @param page A page to lookup the object for. + */ +- (nullable ObjectType)objectForPage:(ASPageCoordinate)page; + +/** + * Sets the given object for the associated page. + * + * @param object The object to store as value. + * + * @param page The page to use for the rect. + */ +- (void)setObject:(ObjectType)object forPage:(ASPageCoordinate)page; + +/** + * Removes the object for the given page, if one exists. + * + * @param page The page to remove. + */ +- (void)removeObjectForPage:(ASPageCoordinate)page; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPageTable.mm b/submodules/AsyncDisplayKit/Source/Details/ASPageTable.mm new file mode 100644 index 0000000000..af74d0c59e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPageTable.mm @@ -0,0 +1,151 @@ +// +// ASPageTable.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +ASPageCoordinate ASPageCoordinateMake(uint16_t x, uint16_t y) +{ + // Add 1 to the end result because 0 is not accepted by NSArray and NSMapTable. + // To avoid overflow after adding, x and y can't be UINT16_MAX (0xFFFF) **at the same time**. + // But for API simplification, we enforce the same restriction to both values. + ASDisplayNodeCAssert(x < UINT16_MAX, @"x coordinate must be less than 65,535"); + ASDisplayNodeCAssert(y < UINT16_MAX, @"y coordinate must be less than 65,535"); + return (x << 16) + y + 1; +} + +ASPageCoordinate ASPageCoordinateForPageThatContainsPoint(CGPoint point, CGSize pageSize) +{ + return ASPageCoordinateMake((MAX(0.0, point.x) / pageSize.width), (MAX(0.0, point.y) / pageSize.height)); +} + +uint16_t ASPageCoordinateGetX(ASPageCoordinate pageCoordinate) +{ + return (pageCoordinate - 1) >> 16; +} + +uint16_t ASPageCoordinateGetY(ASPageCoordinate pageCoordinate) +{ + return (pageCoordinate - 1) & ~(0xFFFF<<16); +} + +CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSize pageSize) +{ + CGFloat pageWidth = pageSize.width; + CGFloat pageHeight = pageSize.height; + return CGRectMake(ASPageCoordinateGetX(pageCoordinate) * pageWidth, ASPageCoordinateGetY(pageCoordinate) * pageHeight, pageWidth, pageHeight); +} + +NSPointerArray *ASPageCoordinatesForPagesThatIntersectRect(CGRect rect, CGSize contentSize, CGSize pageSize) +{ + CGRect contentRect = CGRectMake(0.0, 0.0, contentSize.width, contentSize.height); + // Make sure the specified rect is within contentRect + rect = CGRectIntersection(rect, contentRect); + if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) { + return nil; + } + + NSPointerArray *result = [NSPointerArray pointerArrayWithOptions:(NSPointerFunctionsIntegerPersonality | NSPointerFunctionsOpaqueMemory)]; + + ASPageCoordinate minPage = ASPageCoordinateForPageThatContainsPoint(CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect)), pageSize); + ASPageCoordinate maxPage = ASPageCoordinateForPageThatContainsPoint(CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)), pageSize); + if (minPage == maxPage) { + [result addPointer:(void *)minPage]; + return result; + } + + NSUInteger minX = ASPageCoordinateGetX(minPage); + NSUInteger minY = ASPageCoordinateGetY(minPage); + NSUInteger maxX = ASPageCoordinateGetX(maxPage); + NSUInteger maxY = ASPageCoordinateGetY(maxPage); + + for (NSUInteger x = minX; x <= maxX; x++) { + for (NSUInteger y = minY; y <= maxY; y++) { + ASPageCoordinate page = ASPageCoordinateMake(x, y); + [result addPointer:(void *)page]; + } + } + + return result; +} + +@implementation NSMapTable (ASPageTableMethods) + ++ (instancetype)pageTableWithValuePointerFunctions:(NSPointerFunctions *)valueFuncs NS_RETURNS_RETAINED +{ + static NSPointerFunctions *pageCoordinatesFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pageCoordinatesFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsIntegerPersonality | NSPointerFunctionsOpaqueMemory]; + }); + + return [[NSMapTable alloc] initWithKeyPointerFunctions:pageCoordinatesFuncs valuePointerFunctions:valueFuncs capacity:0]; +} + ++ (ASPageTable *)pageTableForStrongObjectPointers NS_RETURNS_RETAINED +{ + static NSPointerFunctions *strongObjectPointerFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + strongObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; + }); + return [self pageTableWithValuePointerFunctions:strongObjectPointerFuncs]; +} + ++ (ASPageTable *)pageTableForWeakObjectPointers NS_RETURNS_RETAINED +{ + static NSPointerFunctions *weakObjectPointerFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + weakObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsWeakMemory]; + }); + return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; +} + ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize NS_RETURNS_RETAINED +{ + ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers]; + for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { + // This attrs may span multiple pages. Make sure it's registered to all of them + NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(attrs.frame, contentSize, pageSize); + + for (id pagePtr in pages) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableArray *attrsInPage = [result objectForPage:page]; + if (attrsInPage == nil) { + attrsInPage = [[NSMutableArray alloc] init]; + [result setObject:attrsInPage forPage:page]; + } + [attrsInPage addObject:attrs]; + } + } + return result; +} + +- (id)objectForPage:(ASPageCoordinate)page +{ + __unsafe_unretained id key = (__bridge id)(void *)page; + return [self objectForKey:key]; +} + +- (void)setObject:(id)object forPage:(ASPageCoordinate)page +{ + __unsafe_unretained id key = (__bridge id)(void *)page; + [self setObject:object forKey:key]; +} + +- (void)removeObjectForPage:(ASPageCoordinate)page +{ + __unsafe_unretained id key = (__bridge id)(void *)page; + [self removeObjectForKey:key]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.h b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.h new file mode 100644 index 0000000000..2ca24bfbee --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.h @@ -0,0 +1,73 @@ +// +// ASPhotosFrameworkImageRequest.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_USE_PHOTOS + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN NSString *const ASPhotosURLScheme; + +/** + @abstract Use ASPhotosFrameworkImageRequest to encapsulate all the information needed to request an image from + the Photos framework and store it in a URL. + */ +API_AVAILABLE(ios(8.0), tvos(10.0)) +@interface ASPhotosFrameworkImageRequest : NSObject + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER; + +/** + @return A new image request deserialized from `url`, or nil if `url` is not a valid photos URL. + */ ++ (nullable ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url; + +/** + @abstract The asset identifier for this image request provided during initialization. + */ +@property (nonatomic, readonly) NSString *assetIdentifier; + +/** + @abstract The target size for this image request. Defaults to `PHImageManagerMaximumSize`. + */ +@property (nonatomic) CGSize targetSize; + +/** + @abstract The content mode for this image request. Defaults to `PHImageContentModeDefault`. + + @see `PHImageManager` + */ +@property (nonatomic) PHImageContentMode contentMode; + +/** + @abstract The options specified for this request. Default value is the result of `[PHImageRequestOptions new]`. + + @discussion Some properties of this object are ignored when converting this request into a URL. + As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`. + */ +@property (nonatomic) PHImageRequestOptions *options; + +/** + @return A new URL converted from this request. + */ +@property (nonatomic, readonly) NSURL *url; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_USE_PHOTOS diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.h.orig b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.h.orig new file mode 100644 index 0000000000..bf52121f9e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.h.orig @@ -0,0 +1,81 @@ +// +// ASPhotosFrameworkImageRequest.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK +======= +#import + +#if AS_USE_PHOTOS + +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN NSString *const ASPhotosURLScheme; + +/** + @abstract Use ASPhotosFrameworkImageRequest to encapsulate all the information needed to request an image from + the Photos framework and store it in a URL. + */ +API_AVAILABLE(ios(8.0), tvos(10.0)) +@interface ASPhotosFrameworkImageRequest : NSObject + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER; + +/** + @return A new image request deserialized from `url`, or nil if `url` is not a valid photos URL. + */ ++ (nullable ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url; + +/** + @abstract The asset identifier for this image request provided during initialization. + */ +@property (nonatomic, readonly) NSString *assetIdentifier; + +/** + @abstract The target size for this image request. Defaults to `PHImageManagerMaximumSize`. + */ +@property (nonatomic) CGSize targetSize; + +/** + @abstract The content mode for this image request. Defaults to `PHImageContentModeDefault`. + + @see `PHImageManager` + */ +@property (nonatomic) PHImageContentMode contentMode; + +/** + @abstract The options specified for this request. Default value is the result of `[PHImageRequestOptions new]`. + + @discussion Some properties of this object are ignored when converting this request into a URL. + As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`. + */ +@property (nonatomic) PHImageRequestOptions *options; + +/** + @return A new URL converted from this request. + */ +@property (nonatomic, readonly) NSURL *url; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END +<<<<<<< HEAD +#endif +======= + +#endif // AS_USE_PHOTOS +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.m.orig b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.m.orig new file mode 100644 index 0000000000..e24ee882b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.m.orig @@ -0,0 +1,163 @@ +// +// ASPhotosFrameworkImageRequest.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import + +#if AS_USE_PHOTOS + +#import + +NSString *const ASPhotosURLScheme = @"ph"; + +static NSString *const _ASPhotosURLQueryKeyWidth = @"width"; +static NSString *const _ASPhotosURLQueryKeyHeight = @"height"; + +// value is PHImageContentMode value +static NSString *const _ASPhotosURLQueryKeyContentMode = @"contentmode"; + +// value is PHImageRequestOptionsResizeMode value +static NSString *const _ASPhotosURLQueryKeyResizeMode = @"resizemode"; + +// value is PHImageRequestOptionsDeliveryMode value +static NSString *const _ASPhotosURLQueryKeyDeliveryMode = @"deliverymode"; + +// value is PHImageRequestOptionsVersion value +static NSString *const _ASPhotosURLQueryKeyVersion = @"version"; + +// value is 0 or 1 +static NSString *const _ASPhotosURLQueryKeyAllowNetworkAccess = @"network"; + +static NSString *const _ASPhotosURLQueryKeyCropOriginX = @"crop_x"; +static NSString *const _ASPhotosURLQueryKeyCropOriginY = @"crop_y"; +static NSString *const _ASPhotosURLQueryKeyCropWidth = @"crop_w"; +static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; + +@implementation ASPhotosFrameworkImageRequest + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier +{ + self = [super init]; + if (self) { + _assetIdentifier = assetIdentifier; + _options = [PHImageRequestOptions new]; + _contentMode = PHImageContentModeDefault; + _targetSize = PHImageManagerMaximumSize; + } + return self; +} + +#pragma mark NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + ASPhotosFrameworkImageRequest *copy = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier]; + copy.options = [self.options copy]; + copy.targetSize = self.targetSize; + copy.contentMode = self.contentMode; + return copy; +} + +#pragma mark Converting to URL + +- (NSURL *)url +{ + NSURLComponents *comp = [NSURLComponents new]; + comp.scheme = ASPhotosURLScheme; + comp.host = _assetIdentifier; + NSMutableArray *queryItems = [NSMutableArray arrayWithObjects: + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyWidth value:@(_targetSize.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyHeight value:@(_targetSize.height).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyVersion value:@(_options.version).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyContentMode value:@(_contentMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyAllowNetworkAccess value:@(_options.networkAccessAllowed).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyDeliveryMode value:@(_options.deliveryMode).stringValue] + , nil]; + + CGRect cropRect = _options.normalizedCropRect; + if (!CGRectIsEmpty(cropRect)) { + [queryItems addObjectsFromArray:@[ + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginX value:@(cropRect.origin.x).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginY value:@(cropRect.origin.y).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropWidth value:@(cropRect.size.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropHeight value:@(cropRect.size.height).stringValue] + ]]; + } + comp.queryItems = queryItems; + return comp.URL; +} + +#pragma mark Converting from URL + ++ (ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url +{ + // not a photos URL + if (![url.scheme isEqualToString:ASPhotosURLScheme]) { + return nil; + } + + NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + + ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:url.host]; + + CGRect cropRect = CGRectZero; + CGSize targetSize = PHImageManagerMaximumSize; + for (NSURLQueryItem *item in comp.queryItems) { + if ([_ASPhotosURLQueryKeyAllowNetworkAccess isEqualToString:item.name]) { + request.options.networkAccessAllowed = item.value.boolValue; + } else if ([_ASPhotosURLQueryKeyWidth isEqualToString:item.name]) { + targetSize.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyHeight isEqualToString:item.name]) { + targetSize.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyContentMode isEqualToString:item.name]) { + request.contentMode = (PHImageContentMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyVersion isEqualToString:item.name]) { + request.options.version = (PHImageRequestOptionsVersion)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyCropOriginX isEqualToString:item.name]) { + cropRect.origin.x = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropOriginY isEqualToString:item.name]) { + cropRect.origin.y = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropWidth isEqualToString:item.name]) { + cropRect.size.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropHeight isEqualToString:item.name]) { + cropRect.size.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyResizeMode isEqualToString:item.name]) { + request.options.resizeMode = (PHImageRequestOptionsResizeMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyDeliveryMode isEqualToString:item.name]) { + request.options.deliveryMode = (PHImageRequestOptionsDeliveryMode)item.value.integerValue; + } + } + request.targetSize = targetSize; + request.options.normalizedCropRect = cropRect; + return request; +} + +#pragma mark NSObject + +- (BOOL)isEqual:(id)object +{ + if (![object isKindOfClass:ASPhotosFrameworkImageRequest.class]) { + return NO; + } + ASPhotosFrameworkImageRequest *other = object; + return [other.assetIdentifier isEqualToString:self.assetIdentifier] && + other.contentMode == self.contentMode && + CGSizeEqualToSize(other.targetSize, self.targetSize) && + CGRectEqualToRect(other.options.normalizedCropRect, self.options.normalizedCropRect) && + other.options.resizeMode == self.options.resizeMode && + other.options.version == self.options.version; +} + +@end +<<<<<<< HEAD +#endif +======= + +#endif // AS_USE_PHOTOS +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e diff --git a/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.mm b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.mm new file mode 100644 index 0000000000..a7e0b41655 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASPhotosFrameworkImageRequest.mm @@ -0,0 +1,159 @@ +// +// ASPhotosFrameworkImageRequest.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_USE_PHOTOS + +#import + +NSString *const ASPhotosURLScheme = @"ph"; + +static NSString *const _ASPhotosURLQueryKeyWidth = @"width"; +static NSString *const _ASPhotosURLQueryKeyHeight = @"height"; + +// value is PHImageContentMode value +static NSString *const _ASPhotosURLQueryKeyContentMode = @"contentmode"; + +// value is PHImageRequestOptionsResizeMode value +static NSString *const _ASPhotosURLQueryKeyResizeMode = @"resizemode"; + +// value is PHImageRequestOptionsDeliveryMode value +static NSString *const _ASPhotosURLQueryKeyDeliveryMode = @"deliverymode"; + +// value is PHImageRequestOptionsVersion value +static NSString *const _ASPhotosURLQueryKeyVersion = @"version"; + +// value is 0 or 1 +static NSString *const _ASPhotosURLQueryKeyAllowNetworkAccess = @"network"; + +static NSString *const _ASPhotosURLQueryKeyCropOriginX = @"crop_x"; +static NSString *const _ASPhotosURLQueryKeyCropOriginY = @"crop_y"; +static NSString *const _ASPhotosURLQueryKeyCropWidth = @"crop_w"; +static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; + +@implementation ASPhotosFrameworkImageRequest + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier +{ + self = [super init]; + if (self) { + _assetIdentifier = assetIdentifier; + _options = [PHImageRequestOptions new]; + _contentMode = PHImageContentModeDefault; + _targetSize = PHImageManagerMaximumSize; + } + return self; +} + +#pragma mark NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + ASPhotosFrameworkImageRequest *copy = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier]; + copy.options = [self.options copy]; + copy.targetSize = self.targetSize; + copy.contentMode = self.contentMode; + return copy; +} + +#pragma mark Converting to URL + +- (NSURL *)url +{ + NSURLComponents *comp = [NSURLComponents new]; + comp.scheme = ASPhotosURLScheme; + comp.host = _assetIdentifier; + NSMutableArray *queryItems = [NSMutableArray arrayWithObjects: + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyWidth value:@(_targetSize.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyHeight value:@(_targetSize.height).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyVersion value:@(_options.version).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyContentMode value:@(_contentMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyAllowNetworkAccess value:@(_options.networkAccessAllowed).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyDeliveryMode value:@(_options.deliveryMode).stringValue] + , nil]; + + CGRect cropRect = _options.normalizedCropRect; + if (!CGRectIsEmpty(cropRect)) { + [queryItems addObjectsFromArray:@[ + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginX value:@(cropRect.origin.x).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginY value:@(cropRect.origin.y).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropWidth value:@(cropRect.size.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropHeight value:@(cropRect.size.height).stringValue] + ]]; + } + comp.queryItems = queryItems; + return comp.URL; +} + +#pragma mark Converting from URL + ++ (ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url +{ + // not a photos URL + if (![url.scheme isEqualToString:ASPhotosURLScheme]) { + return nil; + } + + NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + + ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:url.host]; + + CGRect cropRect = CGRectZero; + CGSize targetSize = PHImageManagerMaximumSize; + for (NSURLQueryItem *item in comp.queryItems) { + if ([_ASPhotosURLQueryKeyAllowNetworkAccess isEqualToString:item.name]) { + request.options.networkAccessAllowed = item.value.boolValue; + } else if ([_ASPhotosURLQueryKeyWidth isEqualToString:item.name]) { + targetSize.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyHeight isEqualToString:item.name]) { + targetSize.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyContentMode isEqualToString:item.name]) { + request.contentMode = (PHImageContentMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyVersion isEqualToString:item.name]) { + request.options.version = (PHImageRequestOptionsVersion)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyCropOriginX isEqualToString:item.name]) { + cropRect.origin.x = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropOriginY isEqualToString:item.name]) { + cropRect.origin.y = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropWidth isEqualToString:item.name]) { + cropRect.size.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropHeight isEqualToString:item.name]) { + cropRect.size.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyResizeMode isEqualToString:item.name]) { + request.options.resizeMode = (PHImageRequestOptionsResizeMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyDeliveryMode isEqualToString:item.name]) { + request.options.deliveryMode = (PHImageRequestOptionsDeliveryMode)item.value.integerValue; + } + } + request.targetSize = targetSize; + request.options.normalizedCropRect = cropRect; + return request; +} + +#pragma mark NSObject + +- (BOOL)isEqual:(id)object +{ + if (![object isKindOfClass:ASPhotosFrameworkImageRequest.class]) { + return NO; + } + ASPhotosFrameworkImageRequest *other = object; + return [other.assetIdentifier isEqualToString:self.assetIdentifier] && + other.contentMode == self.contentMode && + CGSizeEqualToSize(other.targetSize, self.targetSize) && + CGRectEqualToRect(other.options.normalizedCropRect, self.options.normalizedCropRect) && + other.options.resizeMode == self.options.resizeMode && + other.options.version == self.options.version; +} + +@end + +#endif // AS_USE_PHOTOS diff --git a/submodules/AsyncDisplayKit/Source/Details/ASRangeController.h b/submodules/AsyncDisplayKit/Source/Details/ASRangeController.h new file mode 100644 index 0000000000..ebdc9befaf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASRangeController.h @@ -0,0 +1,206 @@ +// +// ASRangeController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import +#import +#import +#import + +#define ASRangeControllerLoggingEnabled 0 + +NS_ASSUME_NONNULL_BEGIN + +@class _ASHierarchyChangeSet; +@protocol ASRangeControllerDataSource; +@protocol ASRangeControllerDelegate; +@protocol ASLayoutController; + +/** + * Working range controller. + * + * Used internally by ASTableView and ASCollectionView. It is paired with ASDataController. + * It is designed to support custom scrolling containers as well. Observes the visible range, maintains + * "working ranges" to trigger network calls and rendering, and is responsible for driving asynchronous layout of cells. + * This includes cancelling those asynchronous operations as cells fall outside of the working ranges. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASRangeController : NSObject +{ + id _layoutController; + __weak id _dataSource; + __weak id _delegate; +} + +/** + * Notify the range controller that the visible range has been updated. + * This is the primary input call that drives updating the working ranges, and triggering their actions. + * The ranges will be updated in the next turn of the main loop, or when -updateIfNeeded is called. + * + * @see [ASRangeControllerDelegate rangeControllerVisibleNodeIndexPaths:] + */ +- (void)setNeedsUpdate; + +/** + * Update the ranges immediately, if -setNeedsUpdate has been called since the last update. + * This is useful because the ranges must be updated immediately after a cell is added + * into a table/collection to satisfy interface state API guarantees. + */ +- (void)updateIfNeeded; + +/** + * Force update the ranges immediately. + */ +- (void)updateRanges; + +/** + * Add the sized node for `indexPath` as a subview of `contentView`. + * + * @param contentView UIView to add a (sized) node's view to. + * + * @param node The cell node to be added. + */ +- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node; + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +// These methods call the corresponding method on each node, visiting each one that +// the range controller has set a non-default interface state on. +- (void)clearContents; +- (void)clearPreloadedData; + +/** + * An object that describes the layout behavior of the ranged component (table view, collection view, etc.) + * + * Used primarily for providing the current range of index paths and identifying when the + * range controller should invalidate its range. + */ +@property (nonatomic) id layoutController; + +/** + * The underlying data source for the range controller + */ +@property (nonatomic, weak) id dataSource; + +/** + * Delegate for handling range controller events. Must not be nil. + */ +@property (nonatomic, weak) id delegate; + +/** + * Property that indicates whether the scroll view for this range controller has ever changed its contentOffset. + */ +@property (nonatomic) BOOL contentHasBeenScrolled; + +@end + + +/** + * Data source for ASRangeController. + * + * Allows the range controller to perform external queries on the range. + * Ex. range nodes, visible index paths, and viewport size. + */ +@protocol ASRangeControllerDataSource + +/** + * @param rangeController Sender. + * + * @return an table of elements corresponding to the data currently visible onscreen (i.e., the visible range). + */ +- (nullable NSHashTable *)visibleElementsForRangeController:(ASRangeController *)rangeController; + +/** + * @param rangeController Sender. + * + * @return the current scroll direction of the view using this range controller. + */ +- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController; + +/** + * @param rangeController Sender. + * + * @return the ASInterfaceState of the node that this controller is powering. This allows nested range controllers + * to collaborate with one another, as an outer controller may set bits in .interfaceState such as Visible. + * If this controller is an orthogonally scrolling element, it waits until it is visible to preload outside the viewport. + */ +- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController; + +- (ASElementMap *)elementMapForRangeController:(ASRangeController *)rangeController; + +- (NSString *)nameForRangeControllerDataSource; + +@end + +/** + * Delegate for ASRangeController. + */ +@protocol ASRangeControllerDelegate + +/** + * Called to update with given change set. + * + * @param changeSet The change set that includes all updates + * + * @param updates The block that performs relevant data updates. + * + * @discussion The updates block must always be executed or the data controller will get into a bad state. + * It should be called at the time the backing view is ready to process the updates, + * i.e inside the updates block of `-[UICollectionView performBatchUpdates:completion:] or after calling `-[UITableView beginUpdates]`. + */ +- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates; + +- (BOOL)rangeControllerShouldUpdateRanges:(ASRangeController *)rangeController; + +@end + +@interface ASRangeController (ASRangeControllerUpdateRangeProtocol) + +/** + * Update the range mode for a range controller to a explicitly set mode until the node that contains the range + * controller becomes visible again + * + * Logic for the automatic range mode: + * 1. If there are no visible node paths available nothing is to be done and no range update will happen + * 2. The initial range update if the range controller is visible always will be + * ASLayoutRangeModeMinimum as it's the initial fetch + * 3. The range mode set explicitly via updateCurrentRangeWithMode: will last at least one range update. After that it + the range controller will use the explicit set range mode until it becomes visible and a new range update was + triggered or a new range mode via updateCurrentRangeWithMode: is set + * 4. If range mode is not explicitly set the range mode is variying based if the range controller is visible or not + */ +- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode; + +@end + +@interface ASRangeController (DebugInternal) + ++ (void)layoutDebugOverlayIfNeeded; + +- (void)addRangeControllerToRangeDebugOverlay; + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters + interfaceState:(ASInterfaceState)interfaceState; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASRangeController.mm b/submodules/AsyncDisplayKit/Source/Details/ASRangeController.mm new file mode 100644 index 0000000000..f3539ea4b7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASRangeController.mm @@ -0,0 +1,673 @@ +// +// ASRangeController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +#import +#import +#import +#import +#import +#import // Required for interfaceState and hierarchyState setter methods. +#import +#import +#import +#import +#import + +#import +#import + +#define AS_RANGECONTROLLER_LOG_UPDATE_FREQ 0 + +#ifndef ASRangeControllerAutomaticLowMemoryHandling +#define ASRangeControllerAutomaticLowMemoryHandling 1 +#endif + +@interface ASRangeController () +{ + BOOL _rangeIsValid; + BOOL _needsRangeUpdate; + NSSet *_allPreviousIndexPaths; + NSHashTable *_visibleNodes; + ASLayoutRangeMode _currentRangeMode; + BOOL _contentHasBeenScrolled; + BOOL _preserveCurrentRangeMode; + BOOL _didRegisterForNodeDisplayNotifications; + CFTimeInterval _pendingDisplayNodesTimestamp; + + // If the user is not currently scrolling, we will keep our ranges + // configured to match their previous scroll direction. Defaults + // to [.right, .down] so that when the user first opens a screen + // the ranges point down into the content. + ASScrollDirection _previousScrollDirection; + +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + NSUInteger _updateCountThisFrame; + CADisplayLink *_displayLink; +#endif +} + +@end + +static UIApplicationState __ApplicationState = UIApplicationStateActive; + +@implementation ASRangeController + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _rangeIsValid = YES; + _currentRangeMode = ASLayoutRangeModeUnspecified; + _contentHasBeenScrolled = NO; + _preserveCurrentRangeMode = NO; + _previousScrollDirection = ASScrollDirectionDown | ASScrollDirectionRight; + + [[[self class] allRangeControllersWeakSet] addObject:self]; + +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_updateCountDisplayLinkDidFire)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +#endif + + if (ASDisplayNode.shouldShowRangeDebugOverlay) { + [self addRangeControllerToRangeDebugOverlay]; + } + + return self; +} + +- (void)dealloc +{ +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + [_displayLink invalidate]; +#endif + + if (_didRegisterForNodeDisplayNotifications) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; + } +} + +#pragma mark - Core visible node range management API + ++ (BOOL)isFirstRangeUpdateForRangeMode:(ASLayoutRangeMode)rangeMode +{ + return (rangeMode == ASLayoutRangeModeUnspecified); +} + ++ (ASLayoutRangeMode)rangeModeForInterfaceState:(ASInterfaceState)interfaceState + currentRangeMode:(ASLayoutRangeMode)currentRangeMode +{ + BOOL isVisible = (ASInterfaceStateIncludesVisible(interfaceState)); + BOOL isFirstRangeUpdate = [self isFirstRangeUpdateForRangeMode:currentRangeMode]; + if (!isVisible || isFirstRangeUpdate) { + return ASLayoutRangeModeMinimum; + } + + return ASLayoutRangeModeFull; +} + +- (ASInterfaceState)interfaceState +{ + ASInterfaceState selfInterfaceState = ASInterfaceStateNone; + if (_dataSource) { + selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; + } + if (__ApplicationState == UIApplicationStateBackground) { + // If the app is background, pretend to be invisible so that we inform each cell it is no longer being viewed by the user + selfInterfaceState &= ~(ASInterfaceStateVisible); + } + return selfInterfaceState; +} + +- (void)setNeedsUpdate +{ + if (!_needsRangeUpdate) { + _needsRangeUpdate = YES; + + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateIfNeeded]; + }); + } +} + +- (void)updateIfNeeded +{ + if (_needsRangeUpdate) { + [self updateRanges]; + } +} + +- (void)updateRanges +{ + _needsRangeUpdate = NO; + [self _updateVisibleNodeIndexPaths]; +} + +- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode +{ + _preserveCurrentRangeMode = YES; + if (_currentRangeMode != rangeMode) { + _currentRangeMode = rangeMode; + + [self setNeedsUpdate]; + } +} + +- (void)setLayoutController:(id)layoutController +{ + _layoutController = layoutController; + if (layoutController && _dataSource) { + [self updateIfNeeded]; + } +} + +- (void)setDataSource:(id)dataSource +{ + _dataSource = dataSource; + if (dataSource && _layoutController) { + [self updateIfNeeded]; + } +} + +// Clear the visible bit from any nodes that disappeared since last update. +// Currently we guarantee that nodes will not be marked visible when deallocated, +// but it's OK to be in e.g. the preload range. So for the visible bit specifically, +// we add this extra mechanism to account for e.g. deleted items. +// +// NOTE: There is a minor risk here, if a node is transferred from one range controller +// to another before the first rc updates and clears the node out of this set. It's a pretty +// wild scenario that I doubt happens in practice. +- (void)_setVisibleNodes:(NSHashTable *)newVisibleNodes +{ + for (ASCellNode *node in _visibleNodes) { + if (![newVisibleNodes containsObject:node] && node.isVisible) { + [node exitInterfaceState:ASInterfaceStateVisible]; + } + } + _visibleNodes = newVisibleNodes; +} + +- (void)_updateVisibleNodeIndexPaths +{ + as_activity_scope_verbose(as_activity_create("Update range controller", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASCollectionLog(), "Updating ranges for %@", ASViewToDisplayNode(ASDynamicCast(self.delegate, UIView))); + ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); + if (!_layoutController || !_dataSource) { + return; + } + + if (![_delegate rangeControllerShouldUpdateRanges:self]) { + return; + } + +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + _updateCountThisFrame += 1; +#endif + + ASElementMap *map = [_dataSource elementMapForRangeController:self]; + + // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges + // Example: ... = [_layoutController indexPathsForScrolling:scrollDirection rangeType:ASLayoutRangeTypeVisible]; + auto visibleElements = [_dataSource visibleElementsForRangeController:self]; + NSHashTable *newVisibleNodes = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + + ASSignpostStart(ASSignpostRangeControllerUpdate); + + // Get the scroll direction. Default to using the previous one, if they're not scrolling. + ASScrollDirection scrollDirection = [_dataSource scrollDirectionForRangeController:self]; + if (scrollDirection == ASScrollDirectionNone) { + scrollDirection = _previousScrollDirection; + } + _previousScrollDirection = scrollDirection; + + if (visibleElements.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... + // Verify the actual state by checking the layout with a "VisibleOnly" range. + // This allows us to avoid thrashing through -didExitVisibleState in the case of -reloadData, since that generates didEndDisplayingCell calls. + // Those didEndDisplayingCell calls result in items being removed from the visibleElements returned by the _dataSource, even though the layout remains correct. + visibleElements = [_layoutController elementsForScrolling:scrollDirection rangeMode:ASLayoutRangeModeVisibleOnly rangeType:ASLayoutRangeTypeDisplay map:map]; + for (ASCollectionElement *element in visibleElements) { + [newVisibleNodes addObject:element.node]; + } + [self _setVisibleNodes:newVisibleNodes]; + return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later + } + + ASInterfaceState selfInterfaceState = [self interfaceState]; + ASLayoutRangeMode rangeMode = _currentRangeMode; + BOOL updateRangeMode = (!_preserveCurrentRangeMode && _contentHasBeenScrolled); + + // If we've never scrolled before, we never update the range mode, so it doesn't jump into Full too early. + // This can happen if we have multiple, noisy updates occurring from application code before the user has engaged. + // If the range mode is explicitly set via updateCurrentRangeWithMode:, we'll preserve that for at least one update cycle. + // Once the user has scrolled and the range is visible, we'll always resume managing the range mode automatically. + if ((updateRangeMode && ASInterfaceStateIncludesVisible(selfInterfaceState)) || [[self class] isFirstRangeUpdateForRangeMode:rangeMode]) { + rangeMode = [ASRangeController rangeModeForInterfaceState:selfInterfaceState currentRangeMode:_currentRangeMode]; + } + + ASRangeTuningParameters parametersPreload = [_layoutController tuningParametersForRangeMode:rangeMode + rangeType:ASLayoutRangeTypePreload]; + ASRangeTuningParameters parametersDisplay = [_layoutController tuningParametersForRangeMode:rangeMode + rangeType:ASLayoutRangeTypeDisplay]; + + // Preload can express the ultra-low-memory state with 0, 0 returned for its tuningParameters above, and will match Visible. + // However, in this rangeMode, Display is not supposed to contain *any* paths -- not even the visible bounds. TuningParameters can't express this. + BOOL emptyDisplayRange = (rangeMode == ASLayoutRangeModeLowMemory); + BOOL equalDisplayPreload = ASRangeTuningParametersEqualToRangeTuningParameters(parametersDisplay, parametersPreload); + BOOL equalDisplayVisible = (ASRangeTuningParametersEqualToRangeTuningParameters(parametersDisplay, ASRangeTuningParametersZero) + && emptyDisplayRange == NO); + + // Check if both Display and Preload are unique. If they are, we load them with a single fetch from the layout controller for performance. + BOOL optimizedLoadingOfBothRanges = (equalDisplayPreload == NO && equalDisplayVisible == NO && emptyDisplayRange == NO); + + NSHashTable *displayElements = nil; + NSHashTable *preloadElements = nil; + + if (optimizedLoadingOfBothRanges) { + [_layoutController allElementsForScrolling:scrollDirection rangeMode:rangeMode displaySet:&displayElements preloadSet:&preloadElements map:map]; + } else { + if (emptyDisplayRange == YES) { + displayElements = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + } else if (equalDisplayVisible == YES) { + displayElements = visibleElements; + } else { + // Calculating only the Display range means the Preload range is either the same as Display or Visible. + displayElements = [_layoutController elementsForScrolling:scrollDirection rangeMode:rangeMode rangeType:ASLayoutRangeTypeDisplay map:map]; + } + + BOOL equalPreloadVisible = ASRangeTuningParametersEqualToRangeTuningParameters(parametersPreload, ASRangeTuningParametersZero); + if (equalDisplayPreload == YES) { + preloadElements = displayElements; + } else if (equalPreloadVisible == YES) { + preloadElements = visibleElements; + } else { + preloadElements = [_layoutController elementsForScrolling:scrollDirection rangeMode:rangeMode rangeType:ASLayoutRangeTypePreload map:map]; + } + } + + // For now we are only interested in items. Filter-map out from element to item-index-path. + NSSet *visibleIndexPaths = ASSetByFlatMapping(visibleElements, ASCollectionElement *element, [map indexPathForElementIfCell:element]); + NSSet *displayIndexPaths = ASSetByFlatMapping(displayElements, ASCollectionElement *element, [map indexPathForElementIfCell:element]); + NSSet *preloadIndexPaths = ASSetByFlatMapping(preloadElements, ASCollectionElement *element, [map indexPathForElementIfCell:element]); + + // Prioritize the order in which we visit each. Visible nodes should be updated first so they are enqueued on + // the network or display queues before preloading (offscreen) nodes are enqueued. + NSMutableOrderedSet *allIndexPaths = [[NSMutableOrderedSet alloc] initWithSet:visibleIndexPaths]; + + // Typically the preloadIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. + // Because allIndexPaths is an NSMutableOrderedSet, this adds the non-duplicate items /after/ the existing items. + // This means that during iteration, we will first visit visible, then display, then preload nodes. + [allIndexPaths unionSet:displayIndexPaths]; + [allIndexPaths unionSet:preloadIndexPaths]; + + // Add anything we had applied interfaceState to in the last update, but is no longer in range, so we can clear any + // range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic + // scroll or major main thread stall could cause entirely disjoint sets. In either case we must visit all. + // Calling "-set" on NSMutableOrderedSet just references the underlying mutable data store, so we must copy it. + NSSet *allCurrentIndexPaths = [[allIndexPaths set] copy]; + [allIndexPaths unionSet:_allPreviousIndexPaths]; + _allPreviousIndexPaths = allCurrentIndexPaths; + + _currentRangeMode = rangeMode; + _preserveCurrentRangeMode = NO; + + if (!_rangeIsValid) { + [allIndexPaths addObjectsFromArray:map.itemIndexPaths]; + } + +#if ASRangeControllerLoggingEnabled + ASDisplayNodeAssertTrue([visibleIndexPaths isSubsetOfSet:displayIndexPaths]); + NSMutableArray *modifiedIndexPaths = (ASRangeControllerLoggingEnabled ? [NSMutableArray array] : nil); +#endif + + for (NSIndexPath *indexPath in allIndexPaths) { + // Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it. + // For consistency, make sure each node knows that it should measure itself if something changes. + ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout; + + if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { + if ([visibleIndexPaths containsObject:indexPath]) { + interfaceState |= (ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStatePreload); + } else { + if ([preloadIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStatePreload; + } + if ([displayIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateDisplay; + } + } + } else { + // If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the + // instant we come onscreen. So, preload and display all of those things, but don't waste resources preloading yet. + // We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:. + + if ([allCurrentIndexPaths containsObject:indexPath]) { + // DO NOT set Visible: even though these elements are in the visible range / "viewport", + // our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above + + // Set Layout, Preload + interfaceState |= ASInterfaceStatePreload; + + if (rangeMode != ASLayoutRangeModeLowMemory) { + // Add Display. + // We might be looking at an indexPath that was previously in-range, but now we need to clear it. + // In that case we'll just set it back to MeasureLayout. Only set Display | Preload if in allCurrentIndexPaths. + interfaceState |= ASInterfaceStateDisplay; + } + } + } + + ASCellNode *node = [map elementForItemAtIndexPath:indexPath].nodeIfAllocated; + if (node != nil) { + ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset."); + if (ASInterfaceStateIncludesVisible(interfaceState)) { + [newVisibleNodes addObject:node]; + } + // Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. + if (node.pendingInterfaceState != interfaceState) { +#if ASRangeControllerLoggingEnabled + [modifiedIndexPaths addObject:indexPath]; +#endif + + BOOL nodeShouldScheduleDisplay = [node shouldScheduleDisplayWithNewInterfaceState:interfaceState]; + [node recursivelySetInterfaceState:interfaceState]; + + if (nodeShouldScheduleDisplay) { + [self registerForNodeDisplayNotificationsForInterfaceStateIfNeeded:selfInterfaceState]; + if (_didRegisterForNodeDisplayNotifications) { + _pendingDisplayNodesTimestamp = CACurrentMediaTime(); + } + } + } + } + } + + [self _setVisibleNodes:newVisibleNodes]; + + // TODO: This code is for debugging only, but would be great to clean up with a delegate method implementation. + if (ASDisplayNode.shouldShowRangeDebugOverlay) { + ASScrollDirection scrollableDirections = ASScrollDirectionUp | ASScrollDirectionDown; + if ([_dataSource isKindOfClass:NSClassFromString(@"ASCollectionView")]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + scrollableDirections = (ASScrollDirection)[_dataSource performSelector:@selector(scrollableDirections)]; +#pragma clang diagnostic pop + } + + [self updateRangeController:self + withScrollableDirections:scrollableDirections + scrollDirection:scrollDirection + rangeMode:rangeMode + displayTuningParameters:parametersDisplay + preloadTuningParameters:parametersPreload + interfaceState:selfInterfaceState]; + } + + _rangeIsValid = YES; + +#if ASRangeControllerLoggingEnabled +// NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; +// BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet]; +// NSLog(@"visible sets are equal: %d", setsAreEqual); +// if (!setsAreEqual) { +// NSLog(@"standard: %@", visibleIndexPaths); +// NSLog(@"custom: %@", visibleNodePathsSet); +// } + [modifiedIndexPaths sortUsingSelector:@selector(compare:)]; + NSLog(@"Range update complete; modifiedIndexPaths: %@, rangeMode: %d", [self descriptionWithIndexPaths:modifiedIndexPaths], rangeMode); +#endif + + ASSignpostEnd(ASSignpostRangeControllerUpdate); +} + +#pragma mark - Notification observers + +/** + * If we're in a restricted range mode, but we're going to change to a full range mode soon, + * go ahead and schedule the transition as soon as all the currently-scheduled rendering is done #1163. + */ +- (void)registerForNodeDisplayNotificationsForInterfaceStateIfNeeded:(ASInterfaceState)interfaceState +{ + // Do not schedule to listen if we're already in full range mode. + // This avoids updating the range controller during a collection teardown when it is removed + // from the hierarchy and its data source is cleared, causing UIKit to call -reloadData. + if (!_didRegisterForNodeDisplayNotifications && _currentRangeMode != ASLayoutRangeModeFull) { + ASLayoutRangeMode nextRangeMode = [ASRangeController rangeModeForInterfaceState:interfaceState + currentRangeMode:_currentRangeMode]; + if (_currentRangeMode != nextRangeMode) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(scheduledNodesDidDisplay:) + name:ASRenderingEngineDidDisplayScheduledNodesNotification + object:nil]; + _didRegisterForNodeDisplayNotifications = YES; + } + } +} + +- (void)scheduledNodesDidDisplay:(NSNotification *)notification +{ + CFAbsoluteTime notificationTimestamp = ((NSNumber *) notification.userInfo[ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp]).doubleValue; + if (_pendingDisplayNodesTimestamp < notificationTimestamp) { + // The rendering engine has processed all the nodes this range controller scheduled. Let's schedule a range update + [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; + _didRegisterForNodeDisplayNotifications = NO; + + [self setNeedsUpdate]; + } +} + +#pragma mark - Cell node view handling + +- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(node, @"Cannot move a nil node to a view"); + ASDisplayNodeAssert(contentView, @"Cannot move a node to a non-existent view"); + + if (node.shouldUseUIKitCell) { + // When using UIKit cells, the ASCellNode is just a placeholder object with a preferredSize. + // In this case, we should not disrupt the subviews of the contentView. + return; + } + + if (node.view.superview == contentView) { + // this content view is already correctly configured + return; + } + + // clean the content view + for (UIView *view in contentView.subviews) { + [view removeFromSuperview]; + } + + [contentView addSubview:node.view]; +} + +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + [_layoutController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType]; +} + +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType +{ + return [_layoutController tuningParametersForRangeMode:rangeMode rangeType:rangeType]; +} + +#pragma mark - ASDataControllerDelegete + +- (void)dataController:(ASDataController *)dataController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates +{ + ASDisplayNodeAssertMainThread(); + if (changeSet.includesReloadData) { + [self _setVisibleNodes:nil]; + } + _rangeIsValid = NO; + [_delegate rangeController:self updateWithChangeSet:changeSet updates:updates]; +} + +#pragma mark - Memory Management + +// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. +- (void)clearContents +{ + ASDisplayNodeAssertMainThread(); + for (ASCollectionElement *element in [_dataSource elementMapForRangeController:self]) { + ASCellNode *node = element.nodeIfAllocated; + if (ASInterfaceStateIncludesDisplay(node.interfaceState)) { + [node exitInterfaceState:ASInterfaceStateDisplay]; + } + } +} + +- (void)clearPreloadedData +{ + ASDisplayNodeAssertMainThread(); + for (ASCollectionElement *element in [_dataSource elementMapForRangeController:self]) { + ASCellNode *node = element.nodeIfAllocated; + if (ASInterfaceStateIncludesPreload(node.interfaceState)) { + [node exitInterfaceState:ASInterfaceStatePreload]; + } + } +} + +#pragma mark - Class Methods (Application Notification Handlers) + ++ (ASWeakSet *)allRangeControllersWeakSet +{ + static ASWeakSet *__allRangeControllersWeakSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __allRangeControllersWeakSet = [[ASWeakSet alloc] init]; + [self registerSharedApplicationNotifications]; + }); + return __allRangeControllersWeakSet; +} + ++ (void)registerSharedApplicationNotifications +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; +#if ASRangeControllerAutomaticLowMemoryHandling + [center addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif + [center addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [center addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; +} + +static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeLowMemory; ++ (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode +{ + ASDisplayNodeAssert(rangeMode == ASLayoutRangeModeVisibleOnly || rangeMode == ASLayoutRangeModeLowMemory, @"It is highly inadvisable to engage a larger range mode when a memory warning occurs, as this will almost certainly cause app eviction"); + __rangeModeForMemoryWarnings = rangeMode; +} + ++ (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + for (ASRangeController *rangeController in allRangeControllers) { + BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); + [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeVisibleOnly : __rangeModeForMemoryWarnings]; + // There's no need to call needs update as updateCurrentRangeWithMode sets this if necessary. + [rangeController updateIfNeeded]; + } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController didReceiveMemoryWarning] with controllers: %@", allRangeControllers); +#endif +} + ++ (void)didEnterBackground:(NSNotification *)notification +{ + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + for (ASRangeController *rangeController in allRangeControllers) { + // We do not want to fully collapse the Display ranges of any visible range controllers so that flashes can be avoided when + // the app is resumed. Non-visible controllers can be more aggressively culled to the LowMemory state (see definitions for documentation) + BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); + [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeVisibleOnly : ASLayoutRangeModeLowMemory]; + } + + // Because -interfaceState checks __ApplicationState and always clears the "visible" bit if Backgrounded, we must set this after updating the range mode. + __ApplicationState = UIApplicationStateBackground; + for (ASRangeController *rangeController in allRangeControllers) { + // Trigger a range update immediately, as we may not be allowed by the system to run the update block scheduled by changing range mode. + // There's no need to call needs update as updateCurrentRangeWithMode sets this if necessary. + [rangeController updateIfNeeded]; + } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController didEnterBackground] with controllers, after backgrounding: %@", allRangeControllers); +#endif +} + ++ (void)willEnterForeground:(NSNotification *)notification +{ + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + __ApplicationState = UIApplicationStateActive; + for (ASRangeController *rangeController in allRangeControllers) { + BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); + [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; + // There's no need to call needs update as updateCurrentRangeWithMode sets this if necessary. + [rangeController updateIfNeeded]; + } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController willEnterForeground] with controllers, after foregrounding: %@", allRangeControllers); +#endif +} + +#pragma mark - Debugging + +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ +- (void)_updateCountDisplayLinkDidFire +{ + if (_updateCountThisFrame > 1) { + NSLog(@"ASRangeController %p updated %lu times this frame.", self, (unsigned long)_updateCountThisFrame); + } + _updateCountThisFrame = 0; +} +#endif + +- (NSString *)descriptionWithIndexPaths:(NSArray *)indexPaths +{ + NSMutableString *description = [NSMutableString stringWithFormat:@"%@ %@", [super description], @" allPreviousIndexPaths:\n"]; + for (NSIndexPath *indexPath in indexPaths) { + ASDisplayNode *node = [[_dataSource elementMapForRangeController:self] elementForItemAtIndexPath:indexPath].nodeIfAllocated; + ASInterfaceState interfaceState = node.interfaceState; + BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState); + BOOL inDisplay = ASInterfaceStateIncludesDisplay(interfaceState); + BOOL inPreload = ASInterfaceStateIncludesPreload(interfaceState); + [description appendFormat:@"indexPath %@, Visible: %d, Display: %d, Preload: %d\n", indexPath, inVisible, inDisplay, inPreload]; + } + return description; +} + +- (NSString *)description +{ + NSArray *indexPaths = [[_allPreviousIndexPaths allObjects] sortedArrayUsingSelector:@selector(compare:)]; + return [self descriptionWithIndexPaths:indexPaths]; +} + +@end + +@implementation ASDisplayNode (RangeModeConfiguring) + ++ (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode +{ + [ASRangeController setRangeModeForMemoryWarnings:rangeMode]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASRangeControllerUpdateRangeProtocol+Beta.h b/submodules/AsyncDisplayKit/Source/Details/ASRangeControllerUpdateRangeProtocol+Beta.h new file mode 100644 index 0000000000..5bc4fae301 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASRangeControllerUpdateRangeProtocol+Beta.h @@ -0,0 +1,24 @@ +// +// ASRangeControllerUpdateRangeProtocol+Beta.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +@protocol ASRangeControllerUpdateRangeProtocol + +/** + * Updates the current range mode of the range controller for at least the next range update + * and, if the new mode is different from the previous mode, enqueues a range update. + */ +- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASRecursiveUnfairLock.h b/submodules/AsyncDisplayKit/Source/Details/ASRecursiveUnfairLock.h new file mode 100644 index 0000000000..d705e4d384 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASRecursiveUnfairLock.h @@ -0,0 +1,47 @@ +// +// ASRecursiveUnfairLock.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +// Note: We don't use ATOMIC_VAR_INIT here because C++ compilers don't like it, +// and it literally does absolutely nothing. +#define AS_RECURSIVE_UNFAIR_LOCK_INIT ((ASRecursiveUnfairLock){ OS_UNFAIR_LOCK_INIT, NULL, 0}) + +NS_ASSUME_NONNULL_BEGIN + +OS_UNFAIR_LOCK_AVAILABILITY +typedef struct { + os_unfair_lock _lock OS_UNFAIR_LOCK_AVAILABILITY; + _Atomic(pthread_t) _thread; // Write-protected by lock + int _count; // Protected by lock +} ASRecursiveUnfairLock; + +/** + * Lock, blocking if needed. + */ +AS_EXTERN OS_UNFAIR_LOCK_AVAILABILITY +void ASRecursiveUnfairLockLock(ASRecursiveUnfairLock *l); + +/** + * Try to lock without blocking. Returns whether we took the lock. + */ +AS_EXTERN OS_UNFAIR_LOCK_AVAILABILITY +BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l); + +/** + * Unlock. Calling this on a thread that does not own + * the lock will result in an assertion failure, and undefined + * behavior if foundation assertions are disabled. + */ +AS_EXTERN OS_UNFAIR_LOCK_AVAILABILITY +void ASRecursiveUnfairLockUnlock(ASRecursiveUnfairLock *l); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASRecursiveUnfairLock.mm b/submodules/AsyncDisplayKit/Source/Details/ASRecursiveUnfairLock.mm new file mode 100644 index 0000000000..9e4a29d47a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASRecursiveUnfairLock.mm @@ -0,0 +1,83 @@ +// +// ASRecursiveUnfairLock.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASRecursiveUnfairLock.h" + +#import + +/** + * For our atomic _thread, we use acquire/release memory order so that we can have + * the minimum possible constraint on the hardware. The default, `memory_order_seq_cst` + * demands that there be a total order of all such modifications as seen by all threads. + * Acquire/release only requires that modifications to this specific atomic are + * synchronized across acquire/release pairs. + * http://en.cppreference.com/w/cpp/atomic/memory_order + * + * Note also that the unfair_lock involves a thread fence as well, so we don't need to + * take care of synchronizing other values. Just the thread value. + */ +#define rul_set_thread(l, t) atomic_store_explicit(&l->_thread, t, memory_order_release) +#define rul_get_thread(l) atomic_load_explicit(&l->_thread, memory_order_acquire) + +void ASRecursiveUnfairLockLock(ASRecursiveUnfairLock *l) +{ + // Try to lock without blocking. If we fail, check what thread owns it. + // Note that the owning thread CAN CHANGE freely, but it can't become `self` + // because only we are `self`. And if it's already `self` then we already have + // the lock, because we reset it to NULL before we unlock. So (thread == self) is + // invariant. + + const pthread_t s = pthread_self(); + if (os_unfair_lock_trylock(&l->_lock)) { + // Owned by nobody. We now have the lock. Assign self. + rul_set_thread(l, s); + } else if (rul_get_thread(l) == s) { + // Owned by self (recursive lock). nop. + } else { + // Owned by other thread. Block and then set thread to self. + os_unfair_lock_lock(&l->_lock); + rul_set_thread(l, s); + } + + l->_count++; +} + +BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l) +{ + // Same as Lock above. See comments there. + + const pthread_t s = pthread_self(); + if (os_unfair_lock_trylock(&l->_lock)) { + // Owned by nobody. We now have the lock. Assign self. + rul_set_thread(l, s); + } else if (rul_get_thread(l) == s) { + // Owned by self (recursive lock). nop. + } else { + // Owned by other thread. Fail. + return NO; + } + + l->_count++; + return YES; +} + +void ASRecursiveUnfairLockUnlock(ASRecursiveUnfairLock *l) +{ + // Ensure we have the lock. This check may miss some pathological cases, + // but it'll catch 99.999999% of this serious programmer error. + NSCAssert(rul_get_thread(l) == pthread_self(), @"Unlocking from a different thread than locked."); + + if (0 == --l->_count) { + // Note that we have to clear this before unlocking because, if another thread + // succeeds in locking above, but hasn't managed to update _thread, and we + // try to re-lock, and fail the -tryLock, and read _thread, then we'll mistakenly + // think that we still own the lock and proceed without blocking. + rul_set_thread(l, NULL); + os_unfair_lock_unlock(&l->_lock); + } +} diff --git a/submodules/AsyncDisplayKit/Source/Details/ASScrollDirection.h b/submodules/AsyncDisplayKit/Source/Details/ASScrollDirection.h new file mode 100644 index 0000000000..41538ed31b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASScrollDirection.h @@ -0,0 +1,37 @@ +// +// ASScrollDirection.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_OPTIONS(NSInteger, ASScrollDirection) { + ASScrollDirectionNone = 0, + ASScrollDirectionRight = 1 << 0, + ASScrollDirectionLeft = 1 << 1, + ASScrollDirectionUp = 1 << 2, + ASScrollDirectionDown = 1 << 3 +}; + +AS_EXTERN const ASScrollDirection ASScrollDirectionHorizontalDirections; +AS_EXTERN const ASScrollDirection ASScrollDirectionVerticalDirections; + +AS_EXTERN BOOL ASScrollDirectionContainsVerticalDirection(ASScrollDirection scrollDirection); +AS_EXTERN BOOL ASScrollDirectionContainsHorizontalDirection(ASScrollDirection scrollDirection); + +AS_EXTERN BOOL ASScrollDirectionContainsRight(ASScrollDirection scrollDirection); +AS_EXTERN BOOL ASScrollDirectionContainsLeft(ASScrollDirection scrollDirection); +AS_EXTERN BOOL ASScrollDirectionContainsUp(ASScrollDirection scrollDirection); +AS_EXTERN BOOL ASScrollDirectionContainsDown(ASScrollDirection scrollDirection); +AS_EXTERN ASScrollDirection ASScrollDirectionApplyTransform(ASScrollDirection scrollDirection, CGAffineTransform transform); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASScrollDirection.mm b/submodules/AsyncDisplayKit/Source/Details/ASScrollDirection.mm new file mode 100644 index 0000000000..3dff6ba9b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASScrollDirection.mm @@ -0,0 +1,64 @@ +// +// ASScrollDirection.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +const ASScrollDirection ASScrollDirectionHorizontalDirections = ASScrollDirectionLeft | ASScrollDirectionRight; +const ASScrollDirection ASScrollDirectionVerticalDirections = ASScrollDirectionUp | ASScrollDirectionDown; + +BOOL ASScrollDirectionContainsVerticalDirection(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionVerticalDirections) != 0; +} + +BOOL ASScrollDirectionContainsHorizontalDirection(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionHorizontalDirections) != 0; +} + +BOOL ASScrollDirectionContainsRight(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionRight) != 0; +} + +BOOL ASScrollDirectionContainsLeft(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionLeft) != 0; +} + +BOOL ASScrollDirectionContainsUp(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionUp) != 0; +} + +BOOL ASScrollDirectionContainsDown(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionDown) != 0; +} + +ASScrollDirection ASScrollDirectionInvertHorizontally(ASScrollDirection scrollDirection) { + if (scrollDirection == ASScrollDirectionRight) { + return ASScrollDirectionLeft; + } else if (scrollDirection == ASScrollDirectionLeft) { + return ASScrollDirectionRight; + } + return scrollDirection; +} + +ASScrollDirection ASScrollDirectionInvertVertically(ASScrollDirection scrollDirection) { + if (scrollDirection == ASScrollDirectionUp) { + return ASScrollDirectionDown; + } else if (scrollDirection == ASScrollDirectionDown) { + return ASScrollDirectionUp; + } + return scrollDirection; +} + +ASScrollDirection ASScrollDirectionApplyTransform(ASScrollDirection scrollDirection, CGAffineTransform transform) { + if ((transform.a < 0) && ASScrollDirectionContainsHorizontalDirection(scrollDirection)) { + return ASScrollDirectionInvertHorizontally(scrollDirection); + } else if ((transform.d < 0) && ASScrollDirectionContainsVerticalDirection(scrollDirection)) { + return ASScrollDirectionInvertVertically(scrollDirection); + } + return scrollDirection; +} diff --git a/submodules/AsyncDisplayKit/Source/Details/ASSectionContext.h b/submodules/AsyncDisplayKit/Source/Details/ASSectionContext.h new file mode 100644 index 0000000000..e7d4a190a4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASSectionContext.h @@ -0,0 +1,25 @@ +// +// ASSectionContext.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +@class ASCollectionView; + +@protocol ASSectionContext + +/** + * Custom name of this section, for debugging only. + */ +@property (nonatomic, copy, nullable) NSString *sectionName; +@property (nonatomic, weak, nullable) ASCollectionView *collectionView; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASTableLayoutController.h b/submodules/AsyncDisplayKit/Source/Details/ASTableLayoutController.h new file mode 100644 index 0000000000..c294c2ecb4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASTableLayoutController.h @@ -0,0 +1,31 @@ +// +// ASTableLayoutController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UITableView; + +/** + * A layout controller designed for use with UITableView. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTableLayoutController : ASAbstractLayoutController + +@property (nonatomic, weak, readonly) UITableView *tableView; + +- (instancetype)initWithTableView:(UITableView *)tableView; + +@end + +NS_ASSUME_NONNULL_END +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASTableLayoutController.mm b/submodules/AsyncDisplayKit/Source/Details/ASTableLayoutController.mm new file mode 100644 index 0000000000..0669c76131 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASTableLayoutController.mm @@ -0,0 +1,55 @@ +// +// ASTableLayoutController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import + +#import + +#import +#import + +@interface ASTableLayoutController() +@end + +@implementation ASTableLayoutController + +- (instancetype)initWithTableView:(UITableView *)tableView +{ + if (!(self = [super init])) { + return nil; + } + _tableView = tableView; + return self; +} + +#pragma mark - ASLayoutController + +- (NSHashTable *)elementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType map:(ASElementMap *)map +{ + CGRect bounds = _tableView.bounds; + + ASRangeTuningParameters tuningParameters = [self tuningParametersForRangeMode:rangeMode rangeType:rangeType]; + CGRect rangeBounds = CGRectExpandToRangeWithScrollableDirections(bounds, tuningParameters, ASScrollDirectionVerticalDirections, scrollDirection); + NSArray *array = [_tableView indexPathsForRowsInRect:rangeBounds]; + return ASPointerTableByFlatMapping(array, NSIndexPath *indexPath, [map elementForItemAtIndexPath:indexPath]); +} + +- (void)allElementsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode displaySet:(NSHashTable *__autoreleasing _Nullable *)displaySet preloadSet:(NSHashTable *__autoreleasing _Nullable *)preloadSet map:(ASElementMap *)map +{ + if (displaySet == NULL || preloadSet == NULL) { + return; + } + + *displaySet = [self elementsForScrolling:scrollDirection rangeMode:rangeMode rangeType:ASLayoutRangeTypeDisplay map:map]; + *preloadSet = [self elementsForScrolling:scrollDirection rangeMode:rangeMode rangeType:ASLayoutRangeTypePreload map:map]; + return; +} + +@end +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/ASThread.h b/submodules/AsyncDisplayKit/Source/Details/ASThread.h new file mode 100644 index 0000000000..9faa357430 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASThread.h @@ -0,0 +1,295 @@ +// +// ASThread.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import +#import + +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASDisplayNodeThreadIsMain() +{ + return 0 != pthread_main_np(); +} + +/** + * Adds the lock to the current scope. + * + * A C version of the C++ lockers. Pass in any id. + * One benefit this has over C++ lockers is that the lock is retained. We + * had bugs in the past where an object would be deallocated while someone + * had locked its instanceLock, and we'd get a crash. This macro + * retains the locked object until it can be unlocked, which is nice. + */ +#define ASLockScope(nsLocking) \ + id __lockToken __attribute__((cleanup(_ASLockScopeCleanup))) NS_VALID_UNTIL_END_OF_SCOPE = nsLocking; \ + [__lockToken lock]; + +/// Same as ASLockScope(1) but lock isn't retained (be careful). +#define ASLockScopeUnowned(nsLocking) \ + __unsafe_unretained id __lockToken __attribute__((cleanup(_ASLockScopeUnownedCleanup))) = nsLocking; \ + [__lockToken lock]; + +ASDISPLAYNODE_INLINE void _ASLockScopeCleanup(id __strong * const lockPtr) { + [*lockPtr unlock]; +} + +ASDISPLAYNODE_INLINE void _ASLockScopeUnownedCleanup(id __unsafe_unretained * const lockPtr) { + [*lockPtr unlock]; +} + +/** + * Same as ASLockScope(1) but it uses self, so we can skip retain/release. + */ +#define ASLockScopeSelf() ASLockScopeUnowned(self) + +/// One-liner while holding the lock. +#define ASLocked(nsLocking, expr) ({ ASLockScope(nsLocking); expr; }) + +/// Faster self-version. +#define ASLockedSelf(expr) ({ ASLockScopeSelf(); expr; }) + +#define ASLockedSelfCompareAssign(lvalue, newValue) \ + ASLockedSelf(ASCompareAssign(lvalue, newValue)) + +#define ASLockedSelfCompareAssignObjects(lvalue, newValue) \ + ASLockedSelf(ASCompareAssignObjects(lvalue, newValue)) + +#define ASLockedSelfCompareAssignCustom(lvalue, newValue, isequal) \ + ASLockedSelf(ASCompareAssignCustom(lvalue, newValue, isequal)) + +#define ASLockedSelfCompareAssignCopy(lvalue, obj) \ + ASLockedSelf(ASCompareAssignCopy(lvalue, obj)) + +#define ASUnlockScope(nsLocking) \ + id __lockToken __attribute__((cleanup(_ASUnlockScopeCleanup))) NS_VALID_UNTIL_END_OF_SCOPE = nsLocking; \ + [__lockToken unlock]; + +#define ASSynthesizeLockingMethodsWithMutex(mutex) \ +- (void)lock { mutex.lock(); } \ +- (void)unlock { mutex.unlock(); } \ +- (BOOL)tryLock { return (BOOL)mutex.try_lock(); } + +#define ASSynthesizeLockingMethodsWithObject(object) \ +- (void)lock { [object lock]; } \ +- (void)unlock { [object unlock]; } \ +- (BOOL)tryLock { return [object tryLock]; } + +ASDISPLAYNODE_INLINE void _ASUnlockScopeCleanup(id __strong *lockPtr) { + [*lockPtr lock]; +} + +#ifdef __cplusplus + +#include +#include +#include +#include + +// These macros are here for legacy reasons. We may get rid of them later. +#define ASAssertLocked(m) m.AssertHeld() +#define ASAssertUnlocked(m) m.AssertNotHeld() + +namespace AS { + + // Set once in Mutex constructor. Linker fails if this is a member variable. ?? + static bool gMutex_unfair; + +// Silence unguarded availability warnings in here, because +// perf is critical and we will check availability once +// and not again. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + class Mutex + { + public: + /// Constructs a plain mutex (the default). + Mutex () : Mutex (false) {} + + ~Mutex () { + // Manually destroy since unions can't do it. + switch (_type) { + case Plain: + _plain.~mutex(); + break; + case Recursive: + _recursive.~recursive_mutex(); + break; + case Unfair: + // nop + break; + case RecursiveUnfair: + // nop + break; + } + } + + Mutex (const Mutex&) = delete; + Mutex &operator=(const Mutex&) = delete; + + bool try_lock() { + bool success = false; + switch (_type) { + case Plain: + success = _plain.try_lock(); + break; + case Recursive: + success = _recursive.try_lock(); + break; + case Unfair: + success = os_unfair_lock_trylock(&_unfair); + break; + case RecursiveUnfair: + success = ASRecursiveUnfairLockTryLock(&_runfair); + break; + } + if (success) { + DidLock(); + } + return success; + } + + void lock() { + switch (_type) { + case Plain: + _plain.lock(); + break; + case Recursive: + _recursive.lock(); + break; + case Unfair: + os_unfair_lock_lock(&_unfair); + break; + case RecursiveUnfair: + ASRecursiveUnfairLockLock(&_runfair); + break; + } + DidLock(); + } + + void unlock() { + WillUnlock(); + switch (_type) { + case Plain: + _plain.unlock(); + break; + case Recursive: + _recursive.unlock(); + break; + case Unfair: + os_unfair_lock_unlock(&_unfair); + break; + case RecursiveUnfair: + ASRecursiveUnfairLockUnlock(&_runfair); + break; + } + } + + void AssertHeld() { + ASDisplayNodeCAssert(_owner == std::this_thread::get_id(), @"Thread should hold lock"); + } + + void AssertNotHeld() { + ASDisplayNodeCAssert(_owner != std::this_thread::get_id(), @"Thread should not hold lock"); + } + + explicit Mutex (bool recursive) { + + // Check if we can use unfair lock and store in static var. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + gMutex_unfair = ASActivateExperimentalFeature(ASExperimentalUnfairLock); + } + }); + + if (recursive) { + if (gMutex_unfair) { + _type = RecursiveUnfair; + _runfair = AS_RECURSIVE_UNFAIR_LOCK_INIT; + } else { + _type = Recursive; + new (&_recursive) std::recursive_mutex(); + } + } else { + if (gMutex_unfair) { + _type = Unfair; + _unfair = OS_UNFAIR_LOCK_INIT; + } else { + _type = Plain; + new (&_plain) std::mutex(); + } + } + } + + private: + enum Type { + Plain, + Recursive, + Unfair, + RecursiveUnfair + }; + + void WillUnlock() { +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + if (--_count == 0) { + _owner = std::thread::id(); + } +#endif + } + + void DidLock() { +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + if (++_count == 1) { + // New owner. + _owner = std::this_thread::get_id(); + } +#endif + } + + Type _type; + union { + os_unfair_lock _unfair; + ASRecursiveUnfairLock _runfair; + std::mutex _plain; + std::recursive_mutex _recursive; + }; +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + std::thread::id _owner = std::thread::id(); + int _count = 0; +#endif + }; +#pragma clang diagnostic pop // ignored "-Wunguarded-availability" + + /** + Obj-C doesn't allow you to pass parameters to C++ ivar constructors. + Provide a convenience to change the default from non-recursive to recursive. + + But wait! Recursive mutexes are a bad idea. Think twice before using one: + + http://www.zaval.org/resources/library/butenhof1.html + http://www.fieryrobot.com/blog/2008/10/14/recursive-locks-will-kill-you/ + */ + class RecursiveMutex : public Mutex + { + public: + RecursiveMutex () : Mutex (true) {} + }; + + typedef std::lock_guard MutexLocker; + typedef std::unique_lock UniqueLock; + +} // namespace AS + +#endif /* __cplusplus */ diff --git a/submodules/AsyncDisplayKit/Source/Details/ASTraceEvent.h b/submodules/AsyncDisplayKit/Source/Details/ASTraceEvent.h new file mode 100644 index 0000000000..8ca7b32fa6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASTraceEvent.h @@ -0,0 +1,32 @@ +// +// ASTraceEvent.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTraceEvent : NSObject + +/** + * This method is dealloc safe. + */ +- (instancetype)initWithBacktrace:(nullable NSArray *)backtrace + format:(NSString *)format + arguments:(va_list)arguments NS_FORMAT_FUNCTION(2,0); + +// Will be nil unless AS_SAVE_EVENT_BACKTRACES=1 (default=0) +@property (nonatomic, nullable, readonly) NSArray *backtrace; +@property (nonatomic, readonly) NSString *message; +@property (nonatomic, readonly) NSTimeInterval timestamp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASTraceEvent.mm b/submodules/AsyncDisplayKit/Source/Details/ASTraceEvent.mm new file mode 100644 index 0000000000..c809865591 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASTraceEvent.mm @@ -0,0 +1,67 @@ +// +// ASTraceEvent.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +static NSString *const ASTraceEventThreadDescriptionKey = @"ASThreadTraceEventDescription"; + +@interface ASTraceEvent () +@property (nonatomic, readonly) NSString *objectDescription; +@property (nonatomic, readonly) NSString *threadDescription; +@end + +@implementation ASTraceEvent + +- (instancetype)initWithBacktrace:(NSArray *)backtrace format:(NSString *)format arguments:(va_list)args +{ + self = [super init]; + if (self != nil) { + static NSTimeInterval refTime; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + refTime = CACurrentMediaTime(); + }); + + // Create the format string passed to us. + _message = [[NSString alloc] initWithFormat:format arguments:args]; + + NSThread *thread = [NSThread currentThread]; + NSString *threadDescription = thread.name; + if (threadDescription.length == 0) { + if ([thread isMainThread]) { + threadDescription = @"Main"; + } else { + // If the bg thread has no name, we cache a 4-character ptr string to identify it by + // inside the thread dictionary. + NSMutableDictionary *threadDict = thread.threadDictionary; + threadDescription = threadDict[ASTraceEventThreadDescriptionKey]; + if (threadDescription == nil) { + // Want these to be 4-chars to line up with "Main". It's possible that a collision could happen + // here but it's so unbelievably likely to impact development, the risk is acceptable. + NSString *ptrString = [NSString stringWithFormat:@"%p", thread]; + threadDescription = [ptrString substringFromIndex:MAX(0, ptrString.length - 4)]; + threadDict[ASTraceEventThreadDescriptionKey] = threadDescription; + } + } + } + _threadDescription = threadDescription; + + _backtrace = backtrace; + _timestamp = CACurrentMediaTime() - refTime; + } + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<(%@) t=%7.3f: %@>", _threadDescription, _timestamp, _message]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASTraitCollection.h b/submodules/AsyncDisplayKit/Source/Details/ASTraitCollection.h new file mode 100644 index 0000000000..68244a6603 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASTraitCollection.h @@ -0,0 +1,174 @@ +// +// ASTraitCollection.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import + +#import + +@class ASTraitCollection; +@protocol ASLayoutElement; +@protocol ASTraitEnvironment; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - ASPrimitiveTraitCollection + +/** + * @abstract This is an internal struct-representation of ASTraitCollection. + * + * @discussion This struct is for internal use only. Framework users should always use ASTraitCollection. + * + * If you use ASPrimitiveTraitCollection, please do make sure to initialize it with ASPrimitiveTraitCollectionMakeDefault() + * or ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection*). + */ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" +typedef struct { + UIUserInterfaceSizeClass horizontalSizeClass; + UIUserInterfaceSizeClass verticalSizeClass; + + CGFloat displayScale; + UIDisplayGamut displayGamut API_AVAILABLE(ios(10.0)); + + UIUserInterfaceIdiom userInterfaceIdiom; + UIForceTouchCapability forceTouchCapability; + UITraitEnvironmentLayoutDirection layoutDirection API_AVAILABLE(ios(10.0)); +#if AS_BUILD_UIUSERINTERFACESTYLE + UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(tvos(10.0), ios(12.0)); +#endif + + // NOTE: This must be a constant. We will assert. + unowned UIContentSizeCategory preferredContentSizeCategory API_AVAILABLE(ios(10.0)); + + CGSize containerSize; +} ASPrimitiveTraitCollection; +#pragma clang diagnostic pop + +/** + * Creates ASPrimitiveTraitCollection with default values. + */ +AS_EXTERN ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(void); + +/** + * Creates a ASPrimitiveTraitCollection from a given UITraitCollection. + */ +AS_EXTERN ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection); + + +/** + * Compares two ASPrimitiveTraitCollection to determine if they are the same. + */ +AS_EXTERN BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTraitCollection lhs, ASPrimitiveTraitCollection rhs); + +/** + * Returns a string representation of a ASPrimitiveTraitCollection. + */ +AS_EXTERN NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection traits); + +/** + * This function will walk the layout element hierarchy and updates the layout element trait collection for every + * layout element within the hierarchy. + */ +AS_EXTERN void ASTraitCollectionPropagateDown(id element, ASPrimitiveTraitCollection traitCollection); + +/** + * Abstraction on top of UITraitCollection for propagation within AsyncDisplayKit-Layout + */ +@protocol ASTraitEnvironment + +/** + * @abstract Returns a struct-representation of the environment's ASEnvironmentDisplayTraits. + * + * @discussion This only exists as an internal convenience method. Users should access the trait collections through + * the NSObject based asyncTraitCollection API + */ +- (ASPrimitiveTraitCollection)primitiveTraitCollection; + +/** + * @abstract Sets a trait collection on this environment state. + * + * @discussion This only exists as an internal convenience method. Users should not override trait collection using it. + * Use [ASViewController overrideDisplayTraitsWithTraitCollection] block instead. + */ +- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection; + +/** + * @abstract Returns the thread-safe UITraitCollection equivalent. + */ +- (ASTraitCollection *)asyncTraitCollection; + +@end + +#define ASPrimitiveTraitCollectionDefaults \ +- (ASPrimitiveTraitCollection)primitiveTraitCollection\ +{\ + return _primitiveTraitCollection.load();\ +}\ +- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection\ +{\ + _primitiveTraitCollection = traitCollection;\ +}\ + +#define ASLayoutElementCollectionTableSetTraitCollection(lock) \ +- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection\ +{\ + AS::MutexLocker l(lock);\ +\ + ASPrimitiveTraitCollection oldTraits = self.primitiveTraitCollection;\ + [super setPrimitiveTraitCollection:traitCollection];\ +\ + /* Extra Trait Collection Handling */\ +\ + /* If the node is not loaded yet don't do anything as otherwise the access of the view will trigger a load */\ + if (! self.isNodeLoaded) { return; }\ +\ + ASPrimitiveTraitCollection currentTraits = self.primitiveTraitCollection;\ + if (ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(currentTraits, oldTraits) == NO) {\ + [self.dataController environmentDidChange];\ + }\ +}\ + +#pragma mark - ASTraitCollection + +AS_SUBCLASSING_RESTRICTED +@interface ASTraitCollection : NSObject + +@property (readonly) UIUserInterfaceSizeClass horizontalSizeClass; +@property (readonly) UIUserInterfaceSizeClass verticalSizeClass; + +@property (readonly) CGFloat displayScale; +@property (readonly) UIDisplayGamut displayGamut API_AVAILABLE(ios(10.0)); + +@property (readonly) UIUserInterfaceIdiom userInterfaceIdiom; +@property (readonly) UIForceTouchCapability forceTouchCapability; +@property (readonly) UITraitEnvironmentLayoutDirection layoutDirection API_AVAILABLE(ios(10.0)); +#if AS_BUILD_UIUSERINTERFACESTYLE +@property (readonly) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(tvos(10.0), ios(12.0)); +#endif +@property (readonly) UIContentSizeCategory preferredContentSizeCategory API_AVAILABLE(ios(10.0)); + +@property (readonly) CGSize containerSize; + +- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection; + +@end + +/** + * These are internal helper methods. Should never be called by the framework users. + */ +@interface ASTraitCollection (PrimitiveTraits) + ++ (ASTraitCollection *)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits NS_RETURNS_RETAINED; + +- (ASPrimitiveTraitCollection)primitiveTraitCollection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASTraitCollection.mm b/submodules/AsyncDisplayKit/Source/Details/ASTraitCollection.mm new file mode 100644 index 0000000000..e885239211 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASTraitCollection.mm @@ -0,0 +1,256 @@ +// +// ASTraitCollection.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import + +#pragma mark - ASPrimitiveTraitCollection + +void ASTraitCollectionPropagateDown(id element, ASPrimitiveTraitCollection traitCollection) { + if (element) { + element.primitiveTraitCollection = traitCollection; + } + + for (id subelement in element.sublayoutElements) { + ASTraitCollectionPropagateDown(subelement, traitCollection); + } +} + +ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault() { + ASPrimitiveTraitCollection tc = {}; + tc.userInterfaceIdiom = UIUserInterfaceIdiomUnspecified; + tc.forceTouchCapability = UIForceTouchCapabilityUnknown; + tc.displayScale = 0.0; + tc.horizontalSizeClass = UIUserInterfaceSizeClassUnspecified; + tc.verticalSizeClass = UIUserInterfaceSizeClassUnspecified; + tc.containerSize = CGSizeZero; + if (AS_AVAILABLE_IOS(10)) { + tc.displayGamut = UIDisplayGamutUnspecified; + tc.preferredContentSizeCategory = UIContentSizeCategoryUnspecified; + tc.layoutDirection = UITraitEnvironmentLayoutDirectionUnspecified; + } +#if AS_BUILD_UIUSERINTERFACESTYLE + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + tc.userInterfaceStyle = UIUserInterfaceStyleUnspecified; + } +#endif + return tc; +} + +ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection) { + ASPrimitiveTraitCollection environmentTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + environmentTraitCollection.horizontalSizeClass = traitCollection.horizontalSizeClass; + environmentTraitCollection.verticalSizeClass = traitCollection.verticalSizeClass; + environmentTraitCollection.displayScale = traitCollection.displayScale; + environmentTraitCollection.userInterfaceIdiom = traitCollection.userInterfaceIdiom; + environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability; + if (AS_AVAILABLE_IOS(10)) { + environmentTraitCollection.displayGamut = traitCollection.displayGamut; + environmentTraitCollection.layoutDirection = traitCollection.layoutDirection; + + ASDisplayNodeCAssertPermanent(traitCollection.preferredContentSizeCategory); + environmentTraitCollection.preferredContentSizeCategory = traitCollection.preferredContentSizeCategory; + } +#if AS_BUILD_UIUSERINTERFACESTYLE + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + environmentTraitCollection.userInterfaceStyle = traitCollection.userInterfaceStyle; + } +#endif + return environmentTraitCollection; +} + +BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTraitCollection lhs, ASPrimitiveTraitCollection rhs) { + return !memcmp(&lhs, &rhs, sizeof(ASPrimitiveTraitCollection)); +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceIdiom(UIUserInterfaceIdiom idiom) { + switch (idiom) { + case UIUserInterfaceIdiomTV: + return @"TV"; + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + case UIUserInterfaceIdiomCarPlay: + return @"CarPlay"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIForceTouchCapability(UIForceTouchCapability capability) { + switch (capability) { + case UIForceTouchCapabilityAvailable: + return @"Available"; + case UIForceTouchCapabilityUnavailable: + return @"Unavailable"; + default: + return @"Unknown"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceSizeClass(UIUserInterfaceSizeClass sizeClass) { + switch (sizeClass) { + case UIUserInterfaceSizeClassCompact: + return @"Compact"; + case UIUserInterfaceSizeClassRegular: + return @"Regular"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +API_AVAILABLE(ios(10)) +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIDisplayGamut(UIDisplayGamut displayGamut) { + switch (displayGamut) { + case UIDisplayGamutSRGB: + return @"sRGB"; + case UIDisplayGamutP3: + return @"P3"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +API_AVAILABLE(ios(10)) +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUITraitEnvironmentLayoutDirection(UITraitEnvironmentLayoutDirection layoutDirection) { + switch (layoutDirection) { + case UITraitEnvironmentLayoutDirectionLeftToRight: + return @"LeftToRight"; + case UITraitEnvironmentLayoutDirectionRightToLeft: + return @"RightToLeft"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +#if AS_BUILD_UIUSERINTERFACESTYLE +API_AVAILABLE(tvos(10.0), ios(12.0)) +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceStyle(UIUserInterfaceStyle userInterfaceStyle) { + switch (userInterfaceStyle) { + case UIUserInterfaceStyleLight: + return @"Light"; + case UIUserInterfaceStyleDark: + return @"Dark"; + default: + return @"Unspecified"; + } +} +#endif + +NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection traits) { + NSMutableArray *props = [NSMutableArray array]; + [props addObject:@{ @"verticalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.verticalSizeClass) }]; + [props addObject:@{ @"horizontalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.horizontalSizeClass) }]; + [props addObject:@{ @"displayScale": [NSString stringWithFormat: @"%.0lf", (double)traits.displayScale] }]; + [props addObject:@{ @"userInterfaceIdiom": AS_NSStringFromUIUserInterfaceIdiom(traits.userInterfaceIdiom) }]; + [props addObject:@{ @"forceTouchCapability": AS_NSStringFromUIForceTouchCapability(traits.forceTouchCapability) }]; +#if AS_BUILD_UIUSERINTERFACESTYLE + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + [props addObject:@{ @"userInterfaceStyle": AS_NSStringFromUIUserInterfaceStyle(traits.userInterfaceStyle) }]; + } +#endif + if (AS_AVAILABLE_IOS(10)) { + [props addObject:@{ @"layoutDirection": AS_NSStringFromUITraitEnvironmentLayoutDirection(traits.layoutDirection) }]; + [props addObject:@{ @"preferredContentSizeCategory": traits.preferredContentSizeCategory }]; + [props addObject:@{ @"displayGamut": AS_NSStringFromUIDisplayGamut(traits.displayGamut) }]; + } + [props addObject:@{ @"containerSize": NSStringFromCGSize(traits.containerSize) }]; + return ASObjectDescriptionMakeWithoutObject(props); +} + +#pragma mark - ASTraitCollection + +@implementation ASTraitCollection { + ASPrimitiveTraitCollection _prim; +} + ++ (ASTraitCollection *)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits NS_RETURNS_RETAINED { + ASTraitCollection *tc = [[ASTraitCollection alloc] init]; + if (AS_AVAILABLE_IOS(10)) { + ASDisplayNodeCAssertPermanent(traits.preferredContentSizeCategory); + } + tc->_prim = traits; + return tc; +} + +- (ASPrimitiveTraitCollection)primitiveTraitCollection { + return _prim; +} +- (UIUserInterfaceSizeClass)horizontalSizeClass +{ + return _prim.horizontalSizeClass; +} +-(UIUserInterfaceSizeClass)verticalSizeClass +{ + return _prim.verticalSizeClass; +} +- (CGFloat)displayScale +{ + return _prim.displayScale; +} +- (UIDisplayGamut)displayGamut +{ + return _prim.displayGamut; +} +- (UIForceTouchCapability)forceTouchCapability +{ + return _prim.forceTouchCapability; +} +- (UITraitEnvironmentLayoutDirection)layoutDirection +{ + return _prim.layoutDirection; +} +- (CGSize)containerSize +{ + return _prim.containerSize; +} +#if AS_BUILD_UIUSERINTERFACESTYLE +- (UIUserInterfaceStyle)userInterfaceStyle +{ + return _prim.userInterfaceStyle; +} +#endif +- (UIContentSizeCategory)preferredContentSizeCategory +{ + return _prim.preferredContentSizeCategory; +} +- (NSUInteger)hash { + return ASHashBytes(&_prim, sizeof(ASPrimitiveTraitCollection)); +} + +- (BOOL)isEqual:(id)object { + if (!object || ![object isKindOfClass:ASTraitCollection.class]) { + return NO; + } + return [self isEqualToTraitCollection:object]; +} + +- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection +{ + if (traitCollection == nil) { + return NO; + } + + if (self == traitCollection) { + return YES; + } + return ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(_prim, traitCollection->_prim); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASWeakProxy.h b/submodules/AsyncDisplayKit/Source/Details/ASWeakProxy.h new file mode 100644 index 0000000000..7396474b1c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASWeakProxy.h @@ -0,0 +1,32 @@ +// +// ASWeakProxy.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +AS_SUBCLASSING_RESTRICTED +@interface ASWeakProxy : NSProxy + +/** + * @return target The target which will be forwarded all messages sent to the weak proxy. + */ +@property (nonatomic, weak, readonly) id target; + +/** + * An object which forwards messages to a target which it weakly references + * + * @discussion This class is useful for breaking retain cycles. You can pass this in place + * of the target to something which creates a strong reference. All messages sent to the + * proxy will be passed onto the target. + * + * @return an instance of ASWeakProxy + */ ++ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASWeakProxy.mm b/submodules/AsyncDisplayKit/Source/Details/ASWeakProxy.mm new file mode 100644 index 0000000000..4a73408dd5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASWeakProxy.mm @@ -0,0 +1,72 @@ +// +// ASWeakProxy.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@implementation ASWeakProxy + +- (instancetype)initWithTarget:(id)target +{ + if (self) { + _target = target; + } + return self; +} + ++ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED +{ + return [[ASWeakProxy alloc] initWithTarget:target]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + return _target; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + return [_target respondsToSelector:aSelector]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol +{ + return [_target conformsToProtocol:aProtocol]; +} + +/// Strangely, this method doesn't get forwarded by ObjC. +- (BOOL)isKindOfClass:(Class)aClass +{ + return [_target isKindOfClass:aClass]; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, @[@{ @"target": _target ?: (id)kCFNull }]); +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel +{ + ASDisplayNodeAssertNil(_target, @"ASWeakProxy got %@ when its target is still alive, which is unexpected.", NSStringFromSelector(_cmd)); + // 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 target does not respond to the selector, + // the dud NSMethodSignature simply gets us around the exception. + return [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; +} +- (void)forwardInvocation:(NSInvocation *)invocation +{ + ASDisplayNodeAssertNil(_target, @"ASWeakProxy got %@ when its target is still alive, which is unexpected.", NSStringFromSelector(_cmd)); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/ASWeakSet.h b/submodules/AsyncDisplayKit/Source/Details/ASWeakSet.h new file mode 100644 index 0000000000..cc435dec0e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASWeakSet.h @@ -0,0 +1,50 @@ +// +// ASWeakSet.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A class similar to NSSet that stores objects weakly. + * Note that this class uses NSPointerFunctionsObjectPointerPersonality – + * that is, it uses shifted pointer for hashing, and identity comparison for equality. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASWeakSet<__covariant ObjectType> : NSObject + +/// Returns YES if the receiver is empty, NO otherwise. +@property (nonatomic, readonly, getter=isEmpty) BOOL empty; + +/// Returns YES if `object` is in the receiver, NO otherwise. +- (BOOL)containsObject:(ObjectType)object AS_WARN_UNUSED_RESULT; + +/// Insets `object` into the set. +- (void)addObject:(ObjectType)object; + +/// Removes object from the set. +- (void)removeObject:(ObjectType)object; + +/// Removes all objects from the set. +- (void)removeAllObjects; + +/// Returns a standard *retained* NSArray of all objects. Not free to generate, but useful for iterating over contents. +- (NSArray *)allObjects AS_WARN_UNUSED_RESULT; + +/** + * How many objects are contained in this set. + + * NOTE: This computed property is O(N). Consider using the `empty` property. + */ +@property (nonatomic, readonly) NSUInteger count; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/ASWeakSet.mm b/submodules/AsyncDisplayKit/Source/Details/ASWeakSet.mm new file mode 100644 index 0000000000..6530271a5a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/ASWeakSet.mm @@ -0,0 +1,84 @@ +// +// ASWeakSet.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASWeakSet<__covariant ObjectType> () +@property (nonatomic, readonly) NSHashTable *hashTable; +@end + +@implementation ASWeakSet + +- (instancetype)init +{ + self = [super init]; + if (self) { + _hashTable = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality]; + } + return self; +} + +- (void)addObject:(id)object +{ + [_hashTable addObject:object]; +} + +- (void)removeObject:(id)object +{ + [_hashTable removeObject:object]; +} + +- (void)removeAllObjects +{ + [_hashTable removeAllObjects]; +} + +- (NSArray *)allObjects +{ + return _hashTable.allObjects; +} + +- (BOOL)containsObject:(id)object +{ + return [_hashTable containsObject:object]; +} + +- (BOOL)isEmpty +{ + return [_hashTable anyObject] == nil; +} + +/** + Note: The `count` property of NSHashTable is unreliable + in the case of weak-memory hash tables because entries + that have been deallocated are not removed immediately. + + In order to get the true count we have to fall back to using + fast enumeration. + */ +- (NSUInteger)count +{ + NSUInteger count = 0; + for (__unused id object in _hashTable) { + count += 1; + } + return count; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id _Nonnull *)buffer count:(NSUInteger)len +{ + return [_hashTable countByEnumeratingWithState:state objects:buffer count:len]; +} + +- (NSString *)description +{ + return [[super description] stringByAppendingFormat:@" count: %tu, contents: %@", self.count, _hashTable]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/CoreGraphics+ASConvenience.h b/submodules/AsyncDisplayKit/Source/Details/CoreGraphics+ASConvenience.h new file mode 100644 index 0000000000..a0805f1701 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/CoreGraphics+ASConvenience.h @@ -0,0 +1,51 @@ +// +// CoreGraphics+ASConvenience.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import + + +#ifndef CGFLOAT_EPSILON + #if CGFLOAT_IS_DOUBLE + #define CGFLOAT_EPSILON DBL_EPSILON + #else + #define CGFLOAT_EPSILON FLT_EPSILON + #endif +#endif + +NS_ASSUME_NONNULL_BEGIN + +ASDISPLAYNODE_INLINE CGFloat ASCGFloatFromString(NSString *string) +{ +#if CGFLOAT_IS_DOUBLE + return string.doubleValue; +#else + return string.floatValue; +#endif +} + +ASDISPLAYNODE_INLINE CGFloat ASCGFloatFromNumber(NSNumber *number) +{ +#if CGFLOAT_IS_DOUBLE + return number.doubleValue; +#else + return number.floatValue; +#endif +} + +ASDISPLAYNODE_INLINE BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta) +{ + return fabs(size1.width - size2.width) < delta && fabs(size1.height - size2.height) < delta; +}; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/NSArray+Diffing.h b/submodules/AsyncDisplayKit/Source/Details/NSArray+Diffing.h new file mode 100644 index 0000000000..c463903144 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/NSArray+Diffing.h @@ -0,0 +1,92 @@ +// +// NSArray+Diffing.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * These changes can be used to transform `self` to `array` by applying them in (any) order, *without shifting* the + * other elements. This can be done (in an NSMutableArray) by calling `setObject:atIndexedSubscript:` (or just use + * [subscripting] directly) for insertions from `array` into `self` (not the seemingly more apt `insertObject:atIndex`!), + * and using the same method for deletions from `self` (*set* a `[NSNull null]` as opposed to `removeObject:atIndex:`). + * After all inserts/deletes have been applied, there will be no nulls left (except possibly at the end of the array if + * `[array count] < [self count]`) + + * Some examples: + * in: ab c + * out: abdc + * diff: ..+. + * + * in: abcd + * out: dcba + * dif: ---.+++ + * + * in: abcd + * out: ab d + * diff: ..-. + * + * in: a bcd + * out: adbc + * diff: .+..- + * + * If `moves` pointer is passed in, instances where one element moves to another location are detected and reported, + * possibly replacing pairs of delete/insert. The process for transforming an array remains the same, however now it is + * important to apply the moves in order and not overwrite an element that needs to be moved somewhere else. + * + * the same examples, with moves: + * in: ab c + * out: abdc + * diff: ..+. + * + * in: abcd + * out: dcba + * diff: 321. + * + * in: abcd + * out: ab d + * diff: ..-. + * + * in: abcd + * out: adbc + * diff: .312 + * + * Other notes: + * + * No index will be both moved from and deleted. + * Each index 0...[self count] will be either moved from or deleted. If it is moved to the same location, we omit it. + * Each index 0...[array count] will be the destination of ONE move or ONE insert. + * Knowing these things means any two of the three (delete, move, insert) implies the third. + */ + +@interface NSArray (Diffing) + +/** + * @abstract Compares two arrays, providing the insertion and deletion indexes needed to transform into the target array. + * @discussion This compares the equality of each object with `isEqual:`. + * This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences. + * It runs in O(mn) complexity. + */ +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions; + +/** + * @abstract Compares two arrays, providing the insertion and deletion indexes needed to transform into the target array. + * @discussion The `compareBlock` is used to identify the equality of the objects within the arrays. + * This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences. + * It runs in O(mn) complexity. + */ +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison; + +/** + * @abstract Compares two arrays, providing the insertion, deletion, and move indexes needed to transform into the target array. + * @discussion This compares the equality of each object with `isEqual:`. + * This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences. + * It runs in O(mn) complexity. + * The moves are returned in ascending order of their destination index. + */ +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions moves:(NSArray **)moves; +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/NSArray+Diffing.mm b/submodules/AsyncDisplayKit/Source/Details/NSArray+Diffing.mm new file mode 100644 index 0000000000..83d32fea68 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/NSArray+Diffing.mm @@ -0,0 +1,177 @@ +// +// NSArray+Diffing.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@implementation NSArray (Diffing) + +typedef BOOL (^compareBlock)(id _Nonnull lhs, id _Nonnull rhs); + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:[NSArray defaultCompareBlock]]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + compareBlock:(compareBlock)comparison +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:comparison]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + moves:(NSArray **)moves +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:moves + compareBlock:[NSArray defaultCompareBlock]]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + moves:(NSArray **)moves compareBlock:(compareBlock)comparison +{ + struct NSObjectHash + { + std::size_t operator()(id k) const { return (std::size_t) [k hash]; }; + }; + struct NSObjectCompare + { + bool operator()(id lhs, id rhs) const { return (bool) [lhs isEqual:rhs]; }; + }; + std::unordered_multimap potentialMoves; + + NSAssert(comparison != nil, @"Comparison block is required"); + NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)"); + NSMutableArray *moveIndexPaths = nil; + NSMutableIndexSet *insertionIndexes = nil, *deletionIndexes = nil; + if (moves) { + moveIndexPaths = [NSMutableArray new]; + } + NSMutableIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison]; + + if (deletions || moves) { + deletionIndexes = [NSMutableIndexSet indexSet]; + NSUInteger i = 0; + for (id element in self) { + if (![commonIndexes containsIndex:i]) { + [deletionIndexes addIndex:i]; + } + if (moves) { + potentialMoves.insert(std::pair(element, i)); + } + ++i; + } + } + + if (insertions || moves) { + insertionIndexes = [NSMutableIndexSet indexSet]; + NSArray *commonObjects = [self objectsAtIndexes:commonIndexes]; + for (NSUInteger i = 0, j = 0; j < array.count; j++) { + auto moveFound = potentialMoves.find(array[j]); + NSUInteger movedFrom = NSNotFound; + if (moveFound != potentialMoves.end() && moveFound->second != j) { + movedFrom = moveFound->second; + potentialMoves.erase(moveFound); + [moveIndexPaths addObject:[NSIndexPath indexPathForItem:j inSection:movedFrom]]; + } + if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) { + i++; + } else { + if (movedFrom != NSNotFound) { + // moves will coalesce a delete / insert - the insert is just not done, and here we remove the delete: + [deletionIndexes removeIndex:movedFrom]; + // OR a move will have come from the LCS: + if ([commonIndexes containsIndex:movedFrom]) { + [commonIndexes removeIndex:movedFrom]; + commonObjects = [self objectsAtIndexes:commonIndexes]; + } + } else { + [insertionIndexes addIndex:j]; + } + } + } + } + + if (moves) {*moves = moveIndexPaths;} + if (deletions) {*deletions = deletionIndexes;} + if (insertions) {*insertions = insertionIndexes;} +} + +// https://github.com/raywenderlich/swift-algorithm-club/tree/master/Longest%20Common%20Subsequence is not exactly this code (obviously), but +// is a good commentary on the algorithm. +- (NSMutableIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison +{ + NSAssert(comparison != nil, @"Comparison block is required"); + + NSInteger selfCount = self.count; + NSInteger arrayCount = array.count; + + // Allocate the diff map in the heap so we don't blow the stack for large arrays. + NSInteger **lengths = NULL; + lengths = (NSInteger **)malloc(sizeof(NSInteger*) * (selfCount+1)); + if (lengths == NULL) { + ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); + return nil; + } + // Fill in a LCS length matrix: + for (NSInteger i = 0; i <= selfCount; i++) { + lengths[i] = (NSInteger *)malloc(sizeof(NSInteger) * (arrayCount+1)); + if (lengths[i] == NULL) { + ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); + return nil; + } + id selfObj = i > 0 ? self[i-1] : nil; + for (NSInteger j = 0; j <= arrayCount; j++) { + if (i == 0 || j == 0) { + lengths[i][j] = 0; + } else if (comparison(selfObj, 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]); + } + } + } + // Backtrack to fill in indices based on length matrix: + NSMutableIndexSet *common = [NSMutableIndexSet indexSet]; + 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--; + } + } + + for (NSInteger i = 0; i <= selfCount; i++) { + free(lengths[i]); + } + free(lengths); + return common; +} + +static compareBlock defaultCompare = nil; + ++ (compareBlock)defaultCompareBlock +{ + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + defaultCompare = ^BOOL(id lhs, id rhs) { + return [lhs isEqual:rhs]; + }; + }); + + return defaultCompare; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/NSIndexSet+ASHelpers.h b/submodules/AsyncDisplayKit/Source/Details/NSIndexSet+ASHelpers.h new file mode 100644 index 0000000000..cab7c94310 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/NSIndexSet+ASHelpers.h @@ -0,0 +1,29 @@ +// +// NSIndexSet+ASHelpers.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface NSIndexSet (ASHelpers) + +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger idx))block; + +- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes; + +/// Returns all the item indexes from the given index paths that are in the given section. ++ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section; + +/// If you've got an old index, and you insert items using this index set, this returns the change to get to the new index. +- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index; + +- (NSString *)as_smallDescription; + +/// Returns all the section indexes contained in the index paths array. ++ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray *)indexPaths; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/NSIndexSet+ASHelpers.mm b/submodules/AsyncDisplayKit/Source/Details/NSIndexSet+ASHelpers.mm new file mode 100644 index 0000000000..0eba0358f4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/NSIndexSet+ASHelpers.mm @@ -0,0 +1,91 @@ +// +// NSIndexSet+ASHelpers.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +// UIKit indexPath helpers +#import + +#import + +@implementation NSIndexSet (ASHelpers) + +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger))block +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + NSUInteger newIndex = block(i); + if (newIndex != NSNotFound) { + [result addIndex:newIndex]; + } + } + }]; + return result; +} + +- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [indexes enumerateRangesInRange:range options:kNilOptions usingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [result addIndexesInRange:range]; + }]; + }]; + return result; +} + ++ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + for (NSIndexPath *indexPath in indexPaths) { + if (indexPath.section == section) { + [result addIndex:indexPath.item]; + } + } + return result; +} + +- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index +{ + __block NSUInteger newIndex = index; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + if (i <= newIndex) { + newIndex += 1; + } else { + *stop = YES; + } + } + }]; + return newIndex - index; +} + +- (NSString *)as_smallDescription +{ + NSMutableString *result = [NSMutableString stringWithString:@"{ "]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + if (range.length == 1) { + [result appendFormat:@"%tu ", range.location]; + } else { + [result appendFormat:@"%tu-%tu ", range.location, NSMaxRange(range) - 1]; + } + }]; + [result appendString:@"}"]; + return result; +} + ++ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray *)indexPaths +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + for (NSIndexPath *indexPath in indexPaths) { + [result addIndex:indexPath.section]; + } + return result; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/NSMutableAttributedString+TextKitAdditions.h b/submodules/AsyncDisplayKit/Source/Details/NSMutableAttributedString+TextKitAdditions.h new file mode 100644 index 0000000000..afb96722c6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/NSMutableAttributedString+TextKitAdditions.h @@ -0,0 +1,26 @@ +// +// NSMutableAttributedString+TextKitAdditions.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSMutableAttributedString (TextKitAdditions) + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight; + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight maximumLineHeight:(CGFloat)maximumLineHeight; + +- (void)attributeTextInRange:(NSRange)range withTextKitLineHeight:(CGFloat)lineHeight; + +- (void)attributeTextInRange:(NSRange)range withTextKitParagraphStyle:(NSParagraphStyle *)paragraphStyle; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/NSMutableAttributedString+TextKitAdditions.mm b/submodules/AsyncDisplayKit/Source/Details/NSMutableAttributedString+TextKitAdditions.mm new file mode 100644 index 0000000000..3f2ed9d35c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/NSMutableAttributedString+TextKitAdditions.mm @@ -0,0 +1,49 @@ +// +// NSMutableAttributedString+TextKitAdditions.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@implementation NSMutableAttributedString (TextKitAdditions) + +#pragma mark - Convenience Methods + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight +{ + if (range.length) { + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + [style setMinimumLineHeight:minimumLineHeight]; + [self attributeTextInRange:range withTextKitParagraphStyle:style]; + } +} + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight maximumLineHeight:(CGFloat)maximumLineHeight +{ + if (range.length) { + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + [style setMinimumLineHeight:minimumLineHeight]; + [style setMaximumLineHeight:maximumLineHeight]; + [self attributeTextInRange:range withTextKitParagraphStyle:style]; + } +} + +- (void)attributeTextInRange:(NSRange)range withTextKitLineHeight:(CGFloat)lineHeight +{ + [self attributeTextInRange:range withTextKitMinimumLineHeight:lineHeight maximumLineHeight:lineHeight]; +} + +- (void)attributeTextInRange:(NSRange)range withTextKitParagraphStyle:(NSParagraphStyle *)paragraphStyle +{ + if (range.length) { + [self addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransaction.h b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransaction.h new file mode 100644 index 0000000000..245c62b295 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransaction.h @@ -0,0 +1,119 @@ +// +// _ASAsyncTransaction.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#define ASDISPLAYNODE_DELAY_DISPLAY 0 + +@class _ASAsyncTransaction; + +typedef void(^asyncdisplaykit_async_transaction_completion_block_t)(_ASAsyncTransaction *completedTransaction, BOOL canceled); +typedef id _Nullable(^asyncdisplaykit_async_transaction_operation_block_t)(void); +typedef void(^asyncdisplaykit_async_transaction_operation_completion_block_t)(id _Nullable value, BOOL canceled); + +/** + State is initially ASAsyncTransactionStateOpen. + Every transaction MUST be committed. It is an error to fail to commit a transaction. + A committed transaction MAY be canceled. You cannot cancel an open (uncommitted) transaction. + */ +typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) { + ASAsyncTransactionStateOpen = 0, + ASAsyncTransactionStateCommitted, + ASAsyncTransactionStateCanceled, + ASAsyncTransactionStateComplete +}; + +AS_EXTERN NSInteger const ASDefaultTransactionPriority; + +/** + @summary ASAsyncTransaction provides lightweight transaction semantics for asynchronous operations. + + @desc ASAsyncTransaction provides the following properties: + + - Transactions group an arbitrary number of operations, each consisting of an execution block and a completion block. + - The execution block returns a single object that will be passed to the completion block. + - Execution blocks added to a transaction will run in parallel on the global background dispatch queues; + the completion blocks are dispatched to the callback queue. + - Every operation completion block is guaranteed to execute, regardless of cancelation. + However, execution blocks may be skipped if the transaction is canceled. + - Operation completion blocks are always executed in the order they were added to the transaction, assuming the + callback queue is serial of course. + */ +@interface _ASAsyncTransaction : NSObject + +/** + @summary Initialize a transaction that can start collecting async operations. + + @param completionBlock A block that is called when the transaction is completed. + */ +- (instancetype)initWithCompletionBlock:(nullable asyncdisplaykit_async_transaction_completion_block_t)completionBlock; + +/** + @summary Block the main thread until the transaction is complete, including callbacks. + + @desc This must be called on the main thread. + */ +- (void)waitUntilComplete; + +/** + A block that is called when the transaction is completed. + */ +@property (nullable, readonly) asyncdisplaykit_async_transaction_completion_block_t completionBlock; + +/** + The state of the transaction. + @see ASAsyncTransactionState + */ +@property (readonly) ASAsyncTransactionState state; + +/** + @summary Adds a synchronous operation to the transaction. The execution block will be executed immediately. + + @desc The block will be executed on the specified queue and is expected to complete synchronously. The async + transaction will wait for all operations to execute on their appropriate queues, so the blocks may still be executing + async if they are running on a concurrent queue, even though the work for this block is synchronous. + + @param block The execution block that will be executed on a background queue. This is where the expensive work goes. + @param priority Execution priority; Tasks with higher priority will be executed sooner + @param queue The dispatch queue on which to execute the block. + @param completion The completion block that will be executed with the output of the execution block when all of the + operations in the transaction are completed. Executed and released on callbackQueue. + */ +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + priority:(NSInteger)priority + queue:(dispatch_queue_t)queue + completion:(nullable asyncdisplaykit_async_transaction_operation_completion_block_t)completion; + +/** + @summary Cancels all operations in the transaction. + + @desc You can only cancel a committed transaction. + + All completion blocks are always called, regardless of cancelation. Execution blocks may be skipped if canceled. + */ +- (void)cancel; + +/** + @summary Marks the end of adding operations to the transaction. + + @desc You MUST commit every transaction you create. It is an error to create a transaction that is never committed. + + When all of the operations that have been added have completed the transaction will execute their completion + blocks. + + If no operations were added to this transaction, invoking commit will execute the transaction's completion block synchronously. + */ +- (void)commit; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransaction.mm b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransaction.mm new file mode 100644 index 0000000000..c147f5f263 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransaction.mm @@ -0,0 +1,465 @@ +// +// _ASAsyncTransaction.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import +#import +#import +#import +#import +#import +#import + +#ifndef __STRICT_ANSI__ + #warning "Texture must be compiled with std=c++11 to prevent layout issues. gnu++ is not supported. This is hopefully temporary." +#endif + +AS_EXTERN NSRunLoopMode const UITrackingRunLoopMode; + +NSInteger const ASDefaultTransactionPriority = 0; + +@interface ASAsyncTransactionOperation : NSObject +- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock; +@property (nonatomic) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock; +@property id value; // set on bg queue by the operation block +@end + +@implementation ASAsyncTransactionOperation + +- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock +{ + if ((self = [super init])) { + _operationCompletionBlock = operationCompletionBlock; + } + return self; +} + +- (void)dealloc +{ + NSAssert(_operationCompletionBlock == nil, @"Should have been called and released before -dealloc"); +} + +- (void)callAndReleaseCompletionBlock:(BOOL)canceled; +{ + ASDisplayNodeAssertMainThread(); + if (_operationCompletionBlock) { + _operationCompletionBlock(self.value, canceled); + // Guarantee that _operationCompletionBlock is released on main thread + _operationCompletionBlock = nil; + } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, self.value]; +} + +@end + +// Lightweight operation queue for _ASAsyncTransaction that limits number of spawned threads +class ASAsyncTransactionQueue +{ +public: + + // Similar to dispatch_group_t + class Group + { + public: + // call when group is no longer needed; after last scheduled operation the group will delete itself + virtual void release() = 0; + + // schedule block on given queue + virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) = 0; + + // dispatch block on given queue when all previously scheduled blocks finished executing + virtual void notify(dispatch_queue_t queue, dispatch_block_t block) = 0; + + // used when manually executing blocks + virtual void enter() = 0; + virtual void leave() = 0; + + // wait until all scheduled blocks finished executing + virtual void wait() = 0; + + protected: + virtual ~Group() { }; // call release() instead + }; + + // Create new group + Group *createGroup(); + + static ASAsyncTransactionQueue &instance(); + +private: + + struct GroupNotify + { + dispatch_block_t _block; + dispatch_queue_t _queue; + }; + + class GroupImpl : public Group + { + public: + GroupImpl(ASAsyncTransactionQueue &queue) + : _pendingOperations(0) + , _releaseCalled(false) + , _queue(queue) + { + } + + virtual void release(); + virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block); + virtual void notify(dispatch_queue_t queue, dispatch_block_t block); + virtual void enter(); + virtual void leave(); + virtual void wait(); + + int _pendingOperations; + std::list _notifyList; + std::condition_variable _condition; + BOOL _releaseCalled; + ASAsyncTransactionQueue &_queue; + }; + + struct Operation + { + dispatch_block_t _block; + GroupImpl *_group; + NSInteger _priority; + }; + + struct DispatchEntry // entry for each dispatch queue + { + typedef std::list OperationQueue; + typedef std::list OperationIteratorList; // each item points to operation queue + typedef std::map OperationPriorityMap; // sorted by priority + + OperationQueue _operationQueue; + OperationPriorityMap _operationPriorityMap; + int _threadCount; + + Operation popNextOperation(bool respectPriority); // assumes locked mutex + void pushOperation(Operation operation); // assumes locked mutex + }; + + std::map _entries; + std::mutex _mutex; +}; + +ASAsyncTransactionQueue::Group* ASAsyncTransactionQueue::createGroup() +{ + Group *res = new GroupImpl(*this); + return res; +} + +void ASAsyncTransactionQueue::GroupImpl::release() +{ + std::lock_guard l(_queue._mutex); + + if (_pendingOperations == 0) { + delete this; + } else { + _releaseCalled = YES; + } +} + +ASAsyncTransactionQueue::Operation ASAsyncTransactionQueue::DispatchEntry::popNextOperation(bool respectPriority) +{ + NSCAssert(!_operationQueue.empty() && !_operationPriorityMap.empty(), @"No scheduled operations available"); + + OperationQueue::iterator queueIterator; + OperationPriorityMap::iterator mapIterator; + + if (respectPriority) { + mapIterator = --_operationPriorityMap.end(); // highest priority "bucket" + queueIterator = *mapIterator->second.begin(); + } else { + queueIterator = _operationQueue.begin(); + mapIterator = _operationPriorityMap.find(queueIterator->_priority); + } + + // no matter what, first item in "bucket" must match item in queue + NSCAssert(mapIterator->second.front() == queueIterator, @"Queue inconsistency"); + + Operation res = *queueIterator; + _operationQueue.erase(queueIterator); + + mapIterator->second.pop_front(); + if (mapIterator->second.empty()) { + _operationPriorityMap.erase(mapIterator); + } + + return res; +} + +void ASAsyncTransactionQueue::DispatchEntry::pushOperation(ASAsyncTransactionQueue::Operation operation) +{ + _operationQueue.push_back(operation); + + OperationIteratorList &list = _operationPriorityMap[operation._priority]; + list.push_back(--_operationQueue.end()); +} + +void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) +{ + ASAsyncTransactionQueue &q = _queue; + std::lock_guard l(q._mutex); + + DispatchEntry &entry = q._entries[queue]; + + Operation operation; + operation._block = block; + operation._group = this; + operation._priority = priority; + entry.pushOperation(operation); + + ++_pendingOperations; // enter group + +#if ASDISPLAYNODE_DELAY_DISPLAY + NSUInteger maxThreads = 1; +#else + NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2; + + // Bit questionable maybe - we can give main thread more CPU time during tracking. + if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode]) + --maxThreads; +#endif + + if (entry._threadCount < maxThreads) { // we need to spawn another thread + + // first thread will take operations in queue order (regardless of priority), other threads will respect priority + bool respectPriority = entry._threadCount > 0; + ++entry._threadCount; + + dispatch_async(queue, ^{ + std::unique_lock lock(q._mutex); + + // go until there are no more pending operations + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); + lock.unlock(); + if (operation._block) { + operation._block(); + } + operation._group->leave(); + operation._block = nil; // the block must be freed while mutex is unlocked + lock.lock(); + } + --entry._threadCount; + + if (entry._threadCount == 0) { + NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen + q._entries.erase(queue); + } + }); + } +} + +void ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t queue, dispatch_block_t block) +{ + std::lock_guard l(_queue._mutex); + + if (_pendingOperations == 0) { + dispatch_async(queue, block); + } else { + GroupNotify notify; + notify._block = block; + notify._queue = queue; + _notifyList.push_back(notify); + } +} + +void ASAsyncTransactionQueue::GroupImpl::enter() +{ + std::lock_guard l(_queue._mutex); + ++_pendingOperations; +} + +void ASAsyncTransactionQueue::GroupImpl::leave() +{ + std::lock_guard l(_queue._mutex); + --_pendingOperations; + + if (_pendingOperations == 0) { + std::list notifyList; + _notifyList.swap(notifyList); + + for (GroupNotify & notify : notifyList) { + dispatch_async(notify._queue, notify._block); + } + + _condition.notify_one(); + + // there was attempt to release the group before, but we still + // had operations scheduled so now is good time + if (_releaseCalled) { + delete this; + } + } +} + +void ASAsyncTransactionQueue::GroupImpl::wait() +{ + std::unique_lock lock(_queue._mutex); + while (_pendingOperations > 0) { + _condition.wait(lock); + } +} + +ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() +{ + static ASAsyncTransactionQueue *instance = new ASAsyncTransactionQueue(); + return *instance; +} + +@interface _ASAsyncTransaction () +@property ASAsyncTransactionState state; +@end + + +@implementation _ASAsyncTransaction +{ + ASAsyncTransactionQueue::Group *_group; + NSMutableArray *_operations; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithCompletionBlock:(void(^)(_ASAsyncTransaction *, BOOL))completionBlock +{ + if ((self = [self init])) { + _completionBlock = completionBlock; + self.state = ASAsyncTransactionStateOpen; + } + return self; +} + +- (void)dealloc +{ + // Uncommitted transactions break our guarantees about releasing completion blocks on callbackQueue. + NSAssert(self.state != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed"); + if (_group) { + _group->release(); + } +} + +#pragma mark - Transaction Management + +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + priority:(NSInteger)priority + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion +{ + ASDisplayNodeAssertMainThread(); + NSAssert(self.state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); + + [self _ensureTransactionData]; + + ASAsyncTransactionOperation *operation = [[ASAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; + [_operations addObject:operation]; + _group->schedule(priority, queue, ^{ + @autoreleasepool { + if (self.state != ASAsyncTransactionStateCanceled) { + operation.value = block(); + } + } + }); +} + +- (void)cancel +{ + ASDisplayNodeAssertMainThread(); + NSAssert(self.state != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction"); + self.state = ASAsyncTransactionStateCanceled; +} + +- (void)commit +{ + ASDisplayNodeAssertMainThread(); + NSAssert(self.state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction"); + self.state = ASAsyncTransactionStateCommitted; + + if ([_operations count] == 0) { + // Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously. + if (_completionBlock) { + _completionBlock(self, NO); + } + } else { + NSAssert(_group != NULL, @"If there are operations, dispatch group should have been created"); + + _group->notify(dispatch_get_main_queue(), ^{ + [self completeTransaction]; + }); + } +} + +- (void)completeTransaction +{ + ASDisplayNodeAssertMainThread(); + ASAsyncTransactionState state = self.state; + if (state != ASAsyncTransactionStateComplete) { + BOOL isCanceled = (state == ASAsyncTransactionStateCanceled); + for (ASAsyncTransactionOperation *operation in _operations) { + [operation callAndReleaseCompletionBlock:isCanceled]; + } + + // Always set state to Complete, even if we were cancelled, to block any extraneous + // calls to this method that may have been scheduled for the next runloop + // (e.g. if we needed to force one in this runloop with -waitUntilComplete, but another was already scheduled) + self.state = ASAsyncTransactionStateComplete; + + if (_completionBlock) { + _completionBlock(self, isCanceled); + } + } +} + +- (void)waitUntilComplete +{ + ASDisplayNodeAssertMainThread(); + if (self.state != ASAsyncTransactionStateComplete) { + if (_group) { + _group->wait(); + + // At this point, the asynchronous operation may have completed, but the runloop + // observer has not committed the batch of transactions we belong to. It's important to + // commit ourselves via the group to avoid double-committing the transaction. + // This is only necessary when forcing display work to complete before allowing the runloop + // to continue, e.g. in the implementation of -[ASDisplayNode recursivelyEnsureDisplay]. + if (self.state == ASAsyncTransactionStateOpen) { + [_ASAsyncTransactionGroup.mainTransactionGroup commit]; + NSAssert(self.state != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group"); + } + // If we needed to commit the group above, -completeTransaction may have already been run. + // It is designed to accommodate this by checking _state to ensure it is not complete. + [self completeTransaction]; + } + } +} + +#pragma mark - Helper Methods + +- (void)_ensureTransactionData +{ + // Lazily initialize _group and _operations to avoid overhead in the case where no operations are added to the transaction + if (_group == NULL) { + _group = ASAsyncTransactionQueue::instance().createGroup(); + } + if (_operations == nil) { + _operations = [[NSMutableArray alloc] init]; + } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)self.state, _group, _operations]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer+Private.h b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer+Private.h new file mode 100644 index 0000000000..003184c586 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer+Private.h @@ -0,0 +1,24 @@ +// +// _ASAsyncTransactionContainer+Private.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class _ASAsyncTransaction; + +@interface CALayer (ASAsyncTransactionContainerTransactions) +@property (nonatomic, nullable, setter=asyncdisplaykit_setAsyncLayerTransactions:) NSHashTable<_ASAsyncTransaction *> *asyncdisplaykit_asyncLayerTransactions; + +- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction; +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer.h b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer.h new file mode 100644 index 0000000000..53fe40f809 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer.h @@ -0,0 +1,81 @@ +// +// _ASAsyncTransactionContainer.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class _ASAsyncTransaction; + +typedef NS_ENUM(NSUInteger, ASAsyncTransactionContainerState) { + /** + The async container has no outstanding transactions. + Whatever it is displaying is up-to-date. + */ + ASAsyncTransactionContainerStateNoTransactions = 0, + /** + The async container has one or more outstanding async transactions. + Its contents may be out of date or showing a placeholder, depending on the configuration of the contained ASDisplayLayers. + */ + ASAsyncTransactionContainerStatePendingTransactions, +}; + +@protocol ASAsyncTransactionContainer + +/** + @summary If YES, the receiver is marked as a container for async transactions, grouping all of the transactions + in the container hierarchy below the receiver together in a single ASAsyncTransaction. + + @default NO + */ +@property (nonatomic, getter=asyncdisplaykit_isAsyncTransactionContainer, setter=asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; + +/** + @summary The current state of the receiver; indicates if it is currently performing asynchronous operations or if all operations have finished/canceled. + */ +@property (nonatomic, readonly) ASAsyncTransactionContainerState asyncdisplaykit_asyncTransactionContainerState; + +/** + @summary Cancels all async transactions on the receiver. + */ +- (void)asyncdisplaykit_cancelAsyncTransactions; + +@property (nullable, nonatomic, setter=asyncdisplaykit_setCurrentAsyncTransaction:) _ASAsyncTransaction *asyncdisplaykit_currentAsyncTransaction; + +@end + +@interface CALayer (ASAsyncTransactionContainer) +/** + @summary Returns the current async transaction for this layer. A new transaction is created if one + did not already exist. This method will always return an open, uncommitted transaction. + @desc asyncdisplaykit_asyncTransactionContainer does not need to be YES for this to return a transaction. + Defaults to nil. + */ +@property (nullable, nonatomic, readonly) _ASAsyncTransaction *asyncdisplaykit_asyncTransaction; + +/** + @summary Goes up the superlayer chain until it finds the first layer with asyncdisplaykit_asyncTransactionContainer=YES (including the receiver) and returns it. + Returns nil if no parent container is found. + */ +@property (nullable, nonatomic, readonly) CALayer *asyncdisplaykit_parentTransactionContainer; + +/** + @summary Whether or not this layer should serve as a transaction container. + Defaults to NO. + */ +@property (nonatomic, getter=asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; + +@end + +@interface UIView (ASAsyncTransactionContainer) +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer.mm b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer.mm new file mode 100644 index 0000000000..ed44231ce2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionContainer.mm @@ -0,0 +1,121 @@ +// +// _ASAsyncTransactionContainer.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import +#import + +@implementation CALayer (ASAsyncTransactionContainerTransactions) +@dynamic asyncdisplaykit_asyncLayerTransactions; + +// No-ops in the base class. Mostly exposed for testing. +- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction {} +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction {} +@end + +@implementation CALayer (ASAsyncTransactionContainer) +@dynamic asyncdisplaykit_currentAsyncTransaction; +@dynamic asyncdisplaykit_asyncTransactionContainer; + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + return ([self.asyncdisplaykit_asyncLayerTransactions count] == 0) ? ASAsyncTransactionContainerStateNoTransactions : ASAsyncTransactionContainerStatePendingTransactions; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + // If there was an open transaction, commit and clear the current transaction. Otherwise: + // (1) The run loop observer will try to commit a canceled transaction which is not allowed + // (2) We leave the canceled transaction attached to the layer, dooming future operations + _ASAsyncTransaction *currentTransaction = self.asyncdisplaykit_currentAsyncTransaction; + [currentTransaction commit]; + self.asyncdisplaykit_currentAsyncTransaction = nil; + + for (_ASAsyncTransaction *transaction in [self.asyncdisplaykit_asyncLayerTransactions copy]) { + [transaction cancel]; + } +} + +- (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction +{ + _ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncTransaction; + if (transaction == nil) { + NSHashTable *transactions = self.asyncdisplaykit_asyncLayerTransactions; + if (transactions == nil) { + transactions = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + self.asyncdisplaykit_asyncLayerTransactions = transactions; + } + __weak CALayer *weakSelf = self; + transaction = [[_ASAsyncTransaction alloc] initWithCompletionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) { + __strong CALayer *self = weakSelf; + if (self == nil) { + return; + } + [transactions removeObject:completedTransaction]; + [self asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:completedTransaction]; + }]; + [transactions addObject:transaction]; + self.asyncdisplaykit_currentAsyncTransaction = transaction; + [self asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:transaction]; + } + [_ASAsyncTransactionGroup.mainTransactionGroup addTransactionContainer:self]; + return transaction; +} + +- (CALayer *)asyncdisplaykit_parentTransactionContainer +{ + CALayer *containerLayer = self; + while (containerLayer && !containerLayer.asyncdisplaykit_isAsyncTransactionContainer) { + containerLayer = containerLayer.superlayer; + } + return containerLayer; +} + +@end + +@implementation UIView (ASAsyncTransactionContainer) + +- (BOOL)asyncdisplaykit_isAsyncTransactionContainer +{ + return self.layer.asyncdisplaykit_isAsyncTransactionContainer; +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)asyncTransactionContainer +{ + self.layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; +} + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + return self.layer.asyncdisplaykit_asyncTransactionContainerState; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + [self.layer asyncdisplaykit_cancelAsyncTransactions]; +} + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +{ + // No-op in the base class. +} + +- (void)asyncdisplaykit_setCurrentAsyncTransaction:(_ASAsyncTransaction *)transaction +{ + self.layer.asyncdisplaykit_currentAsyncTransaction = transaction; +} + +- (_ASAsyncTransaction *)asyncdisplaykit_currentAsyncTransaction +{ + return self.layer.asyncdisplaykit_currentAsyncTransaction; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionGroup.h b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionGroup.h new file mode 100644 index 0000000000..cd0b216c06 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionGroup.h @@ -0,0 +1,36 @@ +// +// _ASAsyncTransactionGroup.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASAsyncTransactionContainer; + +/// A group of transaction containers, for which the current transactions are committed together at the end of the next runloop tick. +AS_SUBCLASSING_RESTRICTED +@interface _ASAsyncTransactionGroup : NSObject + +/// The main transaction group is scheduled to commit on every tick of the main runloop. +/// Access from the main thread only. +@property (class, nonatomic, readonly) _ASAsyncTransactionGroup *mainTransactionGroup; + +- (void)commit; + +/// Add a transaction container to be committed. +- (void)addTransactionContainer:(id)container; + +/// Use the main group. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionGroup.mm b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionGroup.mm new file mode 100644 index 0000000000..ae651d1870 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/Transactions/_ASAsyncTransactionGroup.mm @@ -0,0 +1,88 @@ +// +// _ASAsyncTransactionGroup.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import + +@implementation _ASAsyncTransactionGroup { + NSHashTable> *_containers; +} + ++ (_ASAsyncTransactionGroup *)mainTransactionGroup +{ + ASDisplayNodeAssertMainThread(); + static _ASAsyncTransactionGroup *mainTransactionGroup; + + if (mainTransactionGroup == nil) { + mainTransactionGroup = [[_ASAsyncTransactionGroup alloc] _init]; + [mainTransactionGroup registerAsMainRunloopObserver]; + } + return mainTransactionGroup; +} + +- (void)registerAsMainRunloopObserver +{ + ASDisplayNodeAssertMainThread(); + static CFRunLoopObserverRef observer; + ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice"); + // defer the commit of the transaction so we can add more during the current runloop iteration + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping + kCFRunLoopExit); // before exiting a runloop run + + observer = CFRunLoopObserverCreateWithHandler(NULL, // allocator + activities, // activities + YES, // repeats + INT_MAX, // order after CA transaction commits + ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + ASDisplayNodeCAssertMainThread(); + [self commit]; + }); + CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes); + CFRelease(observer); +} + +- (instancetype)_init +{ + if ((self = [super init])) { + _containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + } + return self; +} + +- (void)addTransactionContainer:(id)container +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(container != nil, @"No container"); + [_containers addObject:container]; +} + +- (void)commit +{ + ASDisplayNodeAssertMainThread(); + + if ([_containers count]) { + NSHashTable *containersToCommit = _containers; + _containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + + for (id container in containersToCommit) { + // Note that the act of committing a transaction may open a new transaction, + // so we must nil out the transaction we're committing first. + _ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction; + container.asyncdisplaykit_currentAsyncTransaction = nil; + [transaction commit]; + } + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/UICollectionViewLayout+ASConvenience.h b/submodules/AsyncDisplayKit/Source/Details/UICollectionViewLayout+ASConvenience.h new file mode 100644 index 0000000000..78e55a17bd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/UICollectionViewLayout+ASConvenience.h @@ -0,0 +1,28 @@ +// +// UICollectionViewLayout+ASConvenience.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@protocol ASCollectionViewLayoutInspecting; + +NS_ASSUME_NONNULL_BEGIN + +@interface UICollectionViewLayout (ASLayoutInspectorProviding) + +/** + * You can override this method on your @c UICollectionViewLayout subclass to + * return a layout inspector tailored to your layout. + * + * It's fine to return @c self. You must not return @c nil. + */ +- (id)asdk_layoutInspector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/UICollectionViewLayout+ASConvenience.mm b/submodules/AsyncDisplayKit/Source/Details/UICollectionViewLayout+ASConvenience.mm new file mode 100644 index 0000000000..d63b51a157 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/UICollectionViewLayout+ASConvenience.mm @@ -0,0 +1,32 @@ +// +// UICollectionViewLayout+ASConvenience.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import + +#import + +@implementation UICollectionViewLayout (ASLayoutInspectorProviding) + +- (id)asdk_layoutInspector +{ + UICollectionViewFlowLayout *flow = ASDynamicCast(self, UICollectionViewFlowLayout); + if (flow != nil) { + return [[ASCollectionViewFlowLayoutInspector alloc] initWithFlowLayout:flow]; + } else { + return [[ASCollectionViewLayoutInspector alloc] init]; + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/UIView+ASConvenience.h b/submodules/AsyncDisplayKit/Source/Details/UIView+ASConvenience.h new file mode 100644 index 0000000000..30d58a07a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/UIView+ASConvenience.h @@ -0,0 +1,99 @@ +// +// UIView+ASConvenience.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + + +/** + These are the properties we support from CALayer (implemented in the pending state) + */ + +@protocol ASDisplayProperties + +@property (nonatomic) CGPoint position; +@property (nonatomic) CGFloat zPosition; +@property (nonatomic) CGPoint anchorPoint; +@property (nonatomic) CGFloat cornerRadius; +@property (nullable, nonatomic) id contents; +@property (nonatomic, copy) NSString *contentsGravity; +@property (nonatomic) CGRect contentsRect; +@property (nonatomic) CGRect contentsCenter; +@property (nonatomic) CGFloat contentsScale; +@property (nonatomic) CGFloat rasterizationScale; +@property (nonatomic) CATransform3D transform; +@property (nonatomic) CATransform3D sublayerTransform; +@property (nonatomic) BOOL needsDisplayOnBoundsChange; +@property (nonatomic) __attribute__((NSObject)) CGColorRef shadowColor; +@property (nonatomic) CGFloat shadowOpacity; +@property (nonatomic) CGSize shadowOffset; +@property (nonatomic) CGFloat shadowRadius; +@property (nonatomic) CGFloat borderWidth; +@property (nonatomic, getter = isOpaque) BOOL opaque; +@property (nonatomic) __attribute__((NSObject)) CGColorRef borderColor; +@property (nonatomic) __attribute__((NSObject)) CGColorRef backgroundColor; +@property (nonatomic) BOOL allowsGroupOpacity; +@property (nonatomic) BOOL allowsEdgeAntialiasing; +@property (nonatomic) unsigned int edgeAntialiasingMask; + +- (void)setNeedsDisplay; +- (void)setNeedsLayout; +- (void)layoutIfNeeded; + +@end + +/** + These are all of the "good" properties of the UIView API that we support in pendingViewState or view of an ASDisplayNode. + */ +@protocol ASDisplayNodeViewProperties + +@property (nonatomic) BOOL clipsToBounds; +@property (nonatomic, getter=isHidden) BOOL hidden; +@property (nonatomic) BOOL autoresizesSubviews; +@property (nonatomic) UIViewAutoresizing autoresizingMask; +@property (nonatomic, null_resettable) UIColor *tintColor; +@property (nonatomic) CGFloat alpha; +@property (nonatomic) CGRect bounds; +@property (nonatomic) CGRect frame; // Only for use with nodes wrapping synchronous views +@property (nonatomic) UIViewContentMode contentMode; +@property (nonatomic) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); +@property (nonatomic, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; +@property (nonatomic, getter=isExclusiveTouch) BOOL exclusiveTouch; +@property (nonatomic, getter=asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; +@property (nonatomic) UIEdgeInsets layoutMargins; +@property (nonatomic) BOOL preservesSuperviewLayoutMargins; +@property (nonatomic) BOOL insetsLayoutMarginsFromSafeArea; + +/** + Following properties of the UIAccessibility informal protocol are supported as well. + We don't declare them here, so _ASPendingState does not complain about them being not implemented, + as they are already on NSObject + + @property (nonatomic) BOOL isAccessibilityElement; + @property (nonatomic, copy, nullable) NSString *accessibilityLabel; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic, copy, nullable) NSString *accessibilityHint; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic, copy, nullable) NSString *accessibilityValue; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic) UIAccessibilityTraits accessibilityTraits; + @property (nonatomic) CGRect accessibilityFrame; + @property (nonatomic, nullable) NSString *accessibilityLanguage; + @property (nonatomic) BOOL accessibilityElementsHidden; + @property (nonatomic) BOOL accessibilityViewIsModal; + @property (nonatomic) BOOL shouldGroupAccessibilityChildren; + */ + +// Accessibility identification support +@property (nullable, nonatomic, copy) NSString *accessibilityIdentifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASCollectionReusableView.h b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionReusableView.h new file mode 100644 index 0000000000..bb38bb25bf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionReusableView.h @@ -0,0 +1,30 @@ +// +// _ASCollectionReusableView.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +@class ASCellNode, ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED // Note: ASDynamicCastStrict is used on instances of this class based on this restriction. +@interface _ASCollectionReusableView : UICollectionReusableView + +@property (nullable, nonatomic, readonly) ASCellNode *node; +@property (nullable, nonatomic) ASCollectionElement *element; +@property (nullable, nonatomic) UICollectionViewLayoutAttributes *layoutAttributes; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASCollectionReusableView.mm b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionReusableView.mm new file mode 100644 index 0000000000..6f5445654f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionReusableView.mm @@ -0,0 +1,93 @@ +// +// _ASCollectionReusableView.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import + +@implementation _ASCollectionReusableView + +- (ASCellNode *)node +{ + return self.element.node; +} + +- (void)setElement:(ASCollectionElement *)element +{ + ASDisplayNodeAssertMainThread(); + element.node.layoutAttributes = _layoutAttributes; + _element = element; +} + +- (void)setLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + _layoutAttributes = layoutAttributes; + self.node.layoutAttributes = layoutAttributes; +} + +- (void)prepareForReuse +{ + self.layoutAttributes = nil; + + // Need to clear element before UIKit calls setSelected:NO / setHighlighted:NO on its cells + self.element = nil; + [super prepareForReuse]; +} + +/** + * In the initial case, this is called by UICollectionView during cell dequeueing, before + * we get a chance to assign a node to it, so we must be sure to set these layout attributes + * on our node when one is next assigned to us in @c setNode: . Since there may be cases when we _do_ already + * have our node assigned e.g. during a layout update for existing cells, we also attempt + * to update it now. + */ +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + self.layoutAttributes = layoutAttributes; +} + +/** + * Keep our node filling our content view. + */ +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.node.frame = self.bounds; +} + +@end + +/** + * A category that makes _ASCollectionReusableView conform to IGListBindable. + * + * We don't need to do anything to bind the view model – the cell node + * serves the same purpose. + */ +#if __has_include() + +#import + +@interface _ASCollectionReusableView (IGListBindable) +@end + +@implementation _ASCollectionReusableView (IGListBindable) + +- (void)bindViewModel:(id)viewModel +{ + // nop +} + +@end + +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASCollectionViewCell.h b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionViewCell.h new file mode 100644 index 0000000000..cf08d0d508 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionViewCell.h @@ -0,0 +1,39 @@ +// +// _ASCollectionViewCell.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED // Note: ASDynamicCastStrict is used on instances of this class based on this restriction. +@interface _ASCollectionViewCell : UICollectionViewCell + +@property (nonatomic, nullable) ASCollectionElement *element; +@property (nullable, nonatomic, readonly) ASCellNode *node; +@property (nonatomic, nullable) UICollectionViewLayoutAttributes *layoutAttributes; + +/** + * Whether or not this cell is interested in cell node visibility events. + * -cellNodeVisibilityEvent:inScrollView: should be called only if this property is YES. + */ +@property (nonatomic, readonly) BOOL consumesCellNodeVisibilityEvents; + +- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASCollectionViewCell.mm b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionViewCell.mm new file mode 100644 index 0000000000..a7f0b55d3f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASCollectionViewCell.mm @@ -0,0 +1,149 @@ +// +// _ASCollectionViewCell.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import + +#import +#import +#import + +@implementation _ASCollectionViewCell + +- (ASCellNode *)node +{ + return self.element.node; +} + +- (void)setElement:(ASCollectionElement *)element +{ + ASDisplayNodeAssertMainThread(); + ASCellNode *node = element.node; + node.layoutAttributes = _layoutAttributes; + _element = element; + + [node __setSelectedFromUIKit:self.selected]; + [node __setHighlightedFromUIKit:self.highlighted]; +} + +- (BOOL)consumesCellNodeVisibilityEvents +{ + ASCellNode *node = self.node; + if (node == nil) { + return NO; + } + return ASSubclassOverridesSelector([ASCellNode class], [node class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:)); +} + +- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView +{ + [self.node cellNodeVisibilityEvent:event inScrollView:scrollView withCellFrame:self.frame]; +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self.node __setSelectedFromUIKit:selected]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self.node __setHighlightedFromUIKit:highlighted]; +} + +- (void)setLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + _layoutAttributes = layoutAttributes; + self.node.layoutAttributes = layoutAttributes; +} + +- (void)prepareForReuse +{ + self.layoutAttributes = nil; + + // Need to clear element before UIKit calls setSelected:NO / setHighlighted:NO on its cells + self.element = nil; + [super prepareForReuse]; +} + +/** + * In the initial case, this is called by UICollectionView during cell dequeueing, before + * we get a chance to assign a node to it, so we must be sure to set these layout attributes + * on our node when one is next assigned to us in @c setNode: . Since there may be cases when we _do_ already + * have our node assigned e.g. during a layout update for existing cells, we also attempt + * to update it now. + */ +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + [super applyLayoutAttributes:layoutAttributes]; + self.layoutAttributes = layoutAttributes; +} + +/** + * Keep our node filling our content view. + */ +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.node.frame = self.contentView.bounds; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + /** + * The documentation for hitTest:withEvent: on an UIView explicitly states the fact that: + * it ignores view objects that are hidden, that have disabled user interactions, or have an + * alpha level less than 0.01. + * To be able to determine if the collection view cell should skip going further down the tree + * based on the states above we use a valid point within the cells bounds and check the + * superclass hitTest:withEvent: implementation. If this returns a valid value we can go on with + * checking the node as it's expected to not be in one of these states. + */ + if (![super hitTest:self.bounds.origin withEvent:event]) { + return nil; + } + + CGPoint pointOnNode = [self.node.view convertPoint:point fromView:self]; + return [self.node hitTest:pointOnNode withEvent:event]; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event +{ + CGPoint pointOnNode = [self.node.view convertPoint:point fromView:self]; + return [self.node pointInside:pointOnNode withEvent:event]; +} + +@end + +/** + * A category that makes _ASCollectionViewCell conform to IGListBindable. + * + * We don't need to do anything to bind the view model – the cell node + * serves the same purpose. + */ +#if __has_include() + +#import + +@interface _ASCollectionViewCell (IGListBindable) +@end + +@implementation _ASCollectionViewCell (IGListBindable) + +- (void)bindViewModel:(id)viewModel +{ + // nop +} + +@end + +#endif +#endif diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASDisplayLayer.h b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayLayer.h new file mode 100644 index 0000000000..1066a36f1d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayLayer.h @@ -0,0 +1,149 @@ +// +// _ASDisplayLayer.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASDisplayNode; +@protocol _ASDisplayLayerDelegate; + +@interface _ASDisplayLayer : CALayer + +/** + @discussion This property overrides the CALayer category method which implements this via associated objects. + This should result in much better performance for _ASDisplayLayers. + */ +@property (nullable, nonatomic, weak) ASDisplayNode *asyncdisplaykit_node; + +/** + @summary Set to YES to enable asynchronous display for the receiver. + + @default YES (note that this might change for subclasses) + */ +@property (nonatomic) BOOL displaysAsynchronously; + +/** + @summary Cancels any pending async display. + + @desc If the receiver has had display called and is waiting for the dispatched async display to be executed, this will + cancel that dispatched async display. This method is useful to call when removing the receiver from the window. + */ +- (void)cancelAsyncDisplay; + +/** + @summary The dispatch queue used for async display. + + @desc This is exposed here for tests only. + */ ++ (dispatch_queue_t)displayQueue; + +/** + @summary Delegate for asynchronous display of the layer. This should be the node (default) unless you REALLY know what you're doing. + + @desc The asyncDelegate will have the opportunity to override the methods related to async display. + */ +@property (nullable, weak) id<_ASDisplayLayerDelegate> asyncDelegate; + +/** + @summary Suspends both asynchronous and synchronous display of the receiver if YES. + + @desc This can be used to suspend all display calls while the receiver is still in the view hierarchy. If you + want to just cancel pending async display, use cancelAsyncDisplay instead. + + @default NO + */ +@property (nonatomic, getter=isDisplaySuspended) BOOL displaySuspended; + +/** + @summary Bypasses asynchronous rendering and performs a blocking display immediately on the current thread. + + @desc Used by ASDisplayNode to display the layer synchronously on-demand (must be called on the main thread). + */ +- (void)displayImmediately; + +@end + +/** + * Optional methods that the view associated with an _ASDisplayLayer can implement. + * This is distinguished from _ASDisplayLayerDelegate in that it points to the _view_ + * not the node. Unfortunately this is required by ASCollectionView, since we currently + * can't guarantee that an ASCollectionNode exists for it. + */ +@protocol ASCALayerExtendedDelegate + +@optional + +- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds; + +@end + +/** + Implement one of +displayAsyncLayer:parameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide drawing for your node. + Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for use on the display queue. + display/drawRect implementations MUST be thread-safe, as they can be called on the displayQueue (async) or the main thread (sync/displayImmediately) + */ +@protocol _ASDisplayLayerDelegate + +@optional + +// Called on the display queue and/or main queue (MUST BE THREAD SAFE) + +/** + @summary Delegate method to draw layer contents into a CGBitmapContext. The current UIGraphics context will be set to an appropriate context. + @param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer: + @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. + @param isRasterizing YES if the layer is being rasterized into another layer, in which case drawRect: probably wants to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store. + */ ++ (void)drawRect:(CGRect)bounds + withParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock + isRasterizing:(BOOL)isRasterizing; + +/** + @summary Delegate override to provide new layer contents as a UIImage. + @param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer: + @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. + @return A UIImage (backed by a CGImage) with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. + */ ++ (UIImage *)displayWithParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; + +// Called on the main thread only + +/** + @summary Delegate override for drawParameters + */ +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; + +/** + @summary Delegate override for willDisplay + */ +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer asynchronously:(BOOL)asynchronously; + +/** + @summary Delegate override for didDisplay + */ +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer; + +/** + @summary Delegate callback to display a layer, synchronously or asynchronously. 'asyncLayer' does not necessarily need to exist (can be nil). Typically, a delegate will display/draw its own contents and then set .contents on the layer when finished. + */ +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously; + +/** + @summary Delegate callback to handle a layer which requests its asynchronous display be cancelled. + */ +- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASDisplayLayer.mm b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayLayer.mm new file mode 100644 index 0000000000..a7478f1883 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayLayer.mm @@ -0,0 +1,214 @@ +// +// _ASDisplayLayer.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import +#import +#import +#import +#import +#import + +@implementation _ASDisplayLayer +{ + BOOL _attemptedDisplayWhileZeroSized; + + struct { + BOOL delegateDidChangeBounds:1; + } _delegateFlags; +} + +@dynamic displaysAsynchronously; + +#ifdef DEBUG +- (void)dealloc { + if (![NSThread isMainThread]) { + assert(true); + } +} +#endif + +#pragma mark - Properties + +- (void)setDelegate:(id)delegate +{ + [super setDelegate:delegate]; + _delegateFlags.delegateDidChangeBounds = [delegate respondsToSelector:@selector(layer:didChangeBoundsWithOldValue:newValue:)]; +} + +- (void)setDisplaySuspended:(BOOL)displaySuspended +{ + ASDisplayNodeAssertMainThread(); + if (_displaySuspended != displaySuspended) { + _displaySuspended = displaySuspended; + if (!displaySuspended) { + // If resuming display, trigger a display now. + [self setNeedsDisplay]; + } else { + // If suspending display, cancel any current async display so that we don't have contents set on us when it's finished. + [self cancelAsyncDisplay]; + } + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL valid = ASDisplayNodeAssertNonFatal(ASIsCGRectValidForLayout(bounds), @"Caught attempt to set invalid bounds %@ on %@.", NSStringFromCGRect(bounds), self); + if (!valid) { + return; + } + if (_delegateFlags.delegateDidChangeBounds) { + CGRect oldBounds = self.bounds; + [super setBounds:bounds]; + self.asyncdisplaykit_node.threadSafeBounds = bounds; + [(id)self.delegate layer:self didChangeBoundsWithOldValue:oldBounds newValue:bounds]; + + } else { + [super setBounds:bounds]; + self.asyncdisplaykit_node.threadSafeBounds = bounds; + } + + if (_attemptedDisplayWhileZeroSized && CGRectIsEmpty(bounds) == NO && self.needsDisplayOnBoundsChange == NO) { + _attemptedDisplayWhileZeroSized = NO; + [self setNeedsDisplay]; + } +} + +#if DEBUG // These override is strictly to help detect application-level threading errors. Avoid method overhead in release. +- (void)setContents:(id)contents +{ + ASDisplayNodeAssertMainThread(); + [super setContents:contents]; +} + +- (void)setNeedsLayout +{ + ASDisplayNodeAssertMainThread(); + as_log_verbose(ASNodeLog(), "%s on %@", sel_getName(_cmd), self); + [super setNeedsLayout]; +} +#endif + +- (void)layoutSublayers +{ + ASDisplayNodeAssertMainThread(); + [super layoutSublayers]; + + [self.asyncdisplaykit_node __layout]; +} + +- (void)setNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + + // FIXME: Reconsider whether we should cancel a display in progress. + // We should definitely cancel a display that is scheduled, but unstarted display. + [self cancelAsyncDisplay]; + + // Short circuit if display is suspended. When resumed, we will setNeedsDisplay at that time. + if (!_displaySuspended) { + [super setNeedsDisplay]; + } +} + +#pragma mark - + ++ (dispatch_queue_t)displayQueue +{ + static dispatch_queue_t displayQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT); + // we use the highpri queue to prioritize UI rendering over other async operations + dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + }); + + return displayQueue; +} + ++ (id)defaultValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"displaysAsynchronously"]) { + return @YES; + } else if ([key isEqualToString:@"opaque"]) { + return @YES; + } else { + return [super defaultValueForKey:key]; + } +} + +#pragma mark - Display + +- (void)displayImmediately +{ + // This method is a low-level bypass that avoids touching CA, including any reset of the + // needsDisplay flag, until the .contents property is set with the result. + // It is designed to be able to block the thread of any caller and fully execute the display. + + ASDisplayNodeAssertMainThread(); + [self display:NO]; +} + +- (void)_hackResetNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + // Don't listen to our subclasses crazy ideas about setContents by going through super + super.contents = super.contents; +} + +- (void)display +{ + ASDisplayNodeAssertMainThread(); + [self _hackResetNeedsDisplay]; + + if (self.displaySuspended) { + return; + } + + [self display:self.displaysAsynchronously]; +} + +- (void)display:(BOOL)asynchronously +{ + if (CGRectIsEmpty(self.bounds)) { + _attemptedDisplayWhileZeroSized = YES; + } + + [self.asyncDelegate displayAsyncLayer:self asynchronously:asynchronously]; +} + +- (void)cancelAsyncDisplay +{ + ASDisplayNodeAssertMainThread(); + + [self.asyncDelegate cancelDisplayAsyncLayer:self]; +} + +// e.g. > +- (NSString *)description +{ + NSMutableString *description = [[super description] mutableCopy]; + ASDisplayNode *node = self.asyncdisplaykit_node; + if (node != nil) { + NSString *classString = [NSString stringWithFormat:@"%s-", object_getClassName(node)]; + [description replaceOccurrencesOfString:@"_ASDisplay" withString:classString options:kNilOptions range:NSMakeRange(0, description.length)]; + NSUInteger insertionIndex = [description rangeOfString:@">"].location; + if (insertionIndex != NSNotFound) { + NSString *nodeString = [NSString stringWithFormat:@"; node = %@", node]; + [description insertString:nodeString atIndex:insertionIndex]; + } + } + return description; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASDisplayView.h b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayView.h new file mode 100644 index 0000000000..519fde85fa --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayView.h @@ -0,0 +1,44 @@ +// +// _ASDisplayView.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// This class is only for use by ASDisplayNode and should never be subclassed or used directly. +// Note that the "node" property is added to UIView directly via a category in ASDisplayNode. + +@class ASDisplayNode; + +@interface _ASDisplayView : UIView + +/** + @discussion This property overrides the UIView category method which implements this via associated objects. + This should result in much better performance for _ASDisplayView. + */ +@property (nullable, nonatomic, weak) ASDisplayNode *asyncdisplaykit_node; + +// These methods expose a way for ASDisplayNode touch events to let the view call super touch events +// Some UIKit mechanisms, like UITableView and UICollectionView selection handling, require this to work +- (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)__forwardTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; + +// These methods expose a way for ASDisplayNode responder methods to let the view call super responder methods +// They are called from ASDisplayNode to pass through UIResponder methods to the view +- (BOOL)__canBecomeFirstResponder; +- (BOOL)__becomeFirstResponder; +- (BOOL)__canResignFirstResponder; +- (BOOL)__resignFirstResponder; +- (BOOL)__isFirstResponder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASDisplayView.mm b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayView.mm new file mode 100644 index 0000000000..5225fb9958 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayView.mm @@ -0,0 +1,570 @@ +// +// _ASDisplayView.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - _ASDisplayViewMethodOverrides + +typedef NS_OPTIONS(NSUInteger, _ASDisplayViewMethodOverrides) +{ + _ASDisplayViewMethodOverrideNone = 0, + _ASDisplayViewMethodOverrideCanBecomeFirstResponder = 1 << 0, + _ASDisplayViewMethodOverrideBecomeFirstResponder = 1 << 1, + _ASDisplayViewMethodOverrideCanResignFirstResponder = 1 << 2, + _ASDisplayViewMethodOverrideResignFirstResponder = 1 << 3, + _ASDisplayViewMethodOverrideIsFirstResponder = 1 << 4, +}; + +/** + * Returns _ASDisplayViewMethodOverrides for the given class + * + * @param c the class, required. + * + * @return _ASDisplayViewMethodOverrides. + */ +static _ASDisplayViewMethodOverrides GetASDisplayViewMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + _ASDisplayViewMethodOverrides overrides = _ASDisplayViewMethodOverrideNone; + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canBecomeFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideCanBecomeFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(becomeFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideBecomeFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canResignFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideCanResignFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(resignFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideResignFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(isFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideIsFirstResponder; + } + return overrides; +} + +#pragma mark - _ASDisplayView + +@interface _ASDisplayView () + +// Keep the node alive while its view is active. If you create a view, add its layer to a layer hierarchy, then release +// the view, the layer retains the view to prevent a crash. This replicates this behaviour for the node abstraction. +@property (nonatomic) ASDisplayNode *keepalive_node; +@end + +@implementation _ASDisplayView +{ + BOOL _inHitTest; + BOOL _inPointInside; + + NSArray *_accessibilityElements; + CGRect _lastAccessibilityElementsFrame; + + _ASDisplayViewMethodOverrides _methodOverrides; +} + +#pragma mark - Class + ++ (void)initialize +{ + __unused Class initializeSelf = self; + IMP staticInitialize = imp_implementationWithBlock(^(_ASDisplayView *view) { + ASDisplayNodeAssert(view.class == initializeSelf, @"View class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", view.class, initializeSelf); + view->_methodOverrides = GetASDisplayViewMethodOverrides(view.class); + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); +} + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - NSObject Overrides + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + [self _initializeInstance]; + + return self; +} + +- (void)_initializeInstance +{ + [self _staticInitialize]; +} + +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + +// e.g. ; frame = ...> +- (NSString *)description +{ + NSMutableString *description = [[super description] mutableCopy]; + + ASDisplayNode *node = _asyncdisplaykit_node; + + if (node != nil) { + NSString *classString = [NSString stringWithFormat:@"%s-", object_getClassName(node)]; + [description replaceOccurrencesOfString:@"_ASDisplay" withString:classString options:kNilOptions range:NSMakeRange(0, description.length)]; + NSUInteger semicolon = [description rangeOfString:@";"].location; + if (semicolon != NSNotFound) { + NSString *nodeString = [NSString stringWithFormat:@"; node = %@", node]; + [description insertString:nodeString atIndex:semicolon]; + } + // Remove layer description – it never contains valuable info and it duplicates the node info. Noisy. + NSRange layerDescriptionRange = [description rangeOfString:@"; layer = <.*>" options:NSRegularExpressionSearch]; + if (layerDescriptionRange.location != NSNotFound) { + [description replaceCharactersInRange:layerDescriptionRange withString:@""]; + // Our regex will grab the closing angle bracket and I'm not clever enough to come up with a better one, so re-add it if needed. + if ([description hasSuffix:@">"] == NO) { + [description appendString:@">"]; + } + } + } + return description; +} + +#pragma mark - UIView Overrides + +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + BOOL visible = (newWindow != nil); + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + BOOL visible = (self.window != nil); + if (!visible && node.inHierarchy) { + [node __exitHierarchy]; + } +} + +- (void)willMoveToSuperview:(UIView *)newSuperview +{ + // Keep the node alive while the view is in a view hierarchy. This helps ensure that async-drawing views can always + // display their contents as long as they are visible somewhere, and aids in lifecycle management because the + // lifecycle of the node can be treated as the same as the lifecycle of the view (let the view hierarchy own the + // view). + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + UIView *currentSuperview = self.superview; + if (!currentSuperview && newSuperview) { + self.keepalive_node = node; + } + + if (newSuperview) { + ASDisplayNode *supernode = node.supernode; + BOOL supernodeLoaded = supernode.nodeLoaded; + ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for _ASDisplayView's supernode to be layer-backed."); + + BOOL needsSupernodeUpdate = NO; + + if (supernode) { + if (supernodeLoaded) { + if (supernode.layerBacked) { + // See comment in -didMoveToSuperview. This case should be avoided, but is possible with app-level coding errors. + needsSupernodeUpdate = (supernode.layer != newSuperview.layer); + } else { + // If we have a supernode, compensate for users directly messing with views by hitching up to any new supernode. + needsSupernodeUpdate = (supernode.view != newSuperview); + } + } else { + needsSupernodeUpdate = YES; + } + } else { + // If we have no supernode and we are now in a view hierarchy, check to see if we can hook up to a supernode. + needsSupernodeUpdate = (newSuperview != nil); + } + + if (needsSupernodeUpdate) { + // -removeFromSupernode is called by -addSubnode:, if it is needed. + // FIXME: Needs rethinking if automaticallyManagesSubnodes=YES + [newSuperview.asyncdisplaykit_node _addSubnode:node]; + } + } +} + +- (void)didMoveToSuperview +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + UIView *superview = self.superview; + if (superview == nil) { + // Clearing keepalive_node may cause deallocation of the node. In this case, __exitHierarchy may not have an opportunity (e.g. _node will be cleared + // by the time -didMoveToWindow occurs after this) to clear the Visible interfaceState, which we need to do before deallocation to meet an API guarantee. + if (node.inHierarchy) { + [node __exitHierarchy]; + } + self.keepalive_node = nil; + } + +#ifndef MINIMAL_ASDK +#if DEBUG + // This is only to help detect issues when a root-of-view-controller node is reused separately from its view controller. + // Avoid overhead in release. + if (superview && node.viewControllerRoot) { + UIViewController *vc = [node closestViewController]; + + ASDisplayNodeAssert(vc != nil && [vc isKindOfClass:[ASViewController class]] && ((ASViewController*)vc).node == node, @"This node was once used as a view controller's node. You should not reuse it without its view controller."); + } +#endif +#endif + + ASDisplayNode *supernode = node.supernode; + ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for superview's node to be layer-backed."); + + if (supernode) { + ASDisplayNodeAssertTrue(node.nodeLoaded); + BOOL supernodeLoaded = supernode.nodeLoaded; + BOOL needsSupernodeRemoval = NO; + + if (superview) { + // If our new superview is not the same as the supernode's view, or the supernode has no view, disconnect. + if (supernodeLoaded) { + if (supernode.layerBacked) { + // As asserted at the top, this shouldn't be possible, but in production with assertions disabled it can happen. + // We try to make such code behave as well as feasible because it's not that hard of an error to make if some deep + // child node of a layer-backed node happens to be view-backed, but it is not supported and should be avoided. + needsSupernodeRemoval = (supernode.layer != superview.layer); + } else { + needsSupernodeRemoval = (supernode.view != superview); + } + } else { + needsSupernodeRemoval = YES; + } + } else { + // If supernode is loaded but our superview is nil, the user likely manually removed us, so disconnect supernode. + // The unlikely alternative: we are in __unloadNode, with shouldRasterizeSubnodes just having been turned on. + // In the latter case, we don't want to disassemble the node hierarchy because all views are intentionally being destroyed. + BOOL nodeIsRasterized = ((node.hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized); + needsSupernodeRemoval = (supernodeLoaded && !nodeIsRasterized); + } + + if (needsSupernodeRemoval) { + // The node will only disconnect from its supernode, not removeFromSuperview, in this condition. + // FIXME: Needs rethinking if automaticallyManagesSubnodes=YES + [node _removeFromSupernode]; + } + } +} + +- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index { + [super insertSubview:view atIndex:index]; + +#ifndef ASDK_ACCESSIBILITY_DISABLE + self.accessibilityElements = nil; +#endif +} + +- (void)addSubview:(UIView *)view +{ + [super addSubview:view]; + +#ifndef ASDK_ACCESSIBILITY_DISABLE + self.accessibilityElements = nil; +#endif +} + +- (void)willRemoveSubview:(UIView *)subview +{ + [super willRemoveSubview:subview]; + +#ifndef ASDK_ACCESSIBILITY_DISABLE + self.accessibilityElements = nil; +#endif +} + +- (CGSize)sizeThatFits:(CGSize)size +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return node ? [node layoutThatFits:ASSizeRangeMake(size)].size : [super sizeThatFits:size]; +} + +- (void)setNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + // Standard implementation does not actually get to the layer, at least for views that don't implement drawRect:. + [self.layer setNeedsDisplay]; +} + +- (UIViewContentMode)contentMode +{ + return ASDisplayNodeUIContentModeFromCAContentsGravity(self.layer.contentsGravity); +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + ASDisplayNodeAssert(contentMode != UIViewContentModeRedraw, @"Don't do this. Use needsDisplayOnBoundsChange instead."); + + // Do our own mapping so as not to call super and muck up needsDisplayOnBoundsChange. If we're in a production build, fall back to resize if we see redraw + self.layer.contentsGravity = (contentMode != UIViewContentModeRedraw) ? ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode) : kCAGravityResize; +} + +- (void)setBounds:(CGRect)bounds +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super setBounds:bounds]; + node.threadSafeBounds = bounds; +} + +- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +{ + [super addGestureRecognizer:gestureRecognizer]; + [_asyncdisplaykit_node nodeViewDidAddGestureRecognizer]; +} + +#pragma mark - Event Handling + UIResponder Overrides +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesBegan) { + [node touchesBegan:touches withEvent:event]; + } else { + [super touchesBegan:touches withEvent:event]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesMoved) { + [node touchesMoved:touches withEvent:event]; + } else { + [super touchesMoved:touches withEvent:event]; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesEnded) { + [node touchesEnded:touches withEvent:event]; + } else { + [super touchesEnded:touches withEvent:event]; + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesCancelled) { + [node touchesCancelled:touches withEvent:event]; + } else { + [super touchesCancelled:touches withEvent:event]; + } +} + +- (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; +} + +- (void)__forwardTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; +} + +- (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; +} + +- (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + // REVIEW: We should optimize these types of messages by setting a boolean in the associated ASDisplayNode subclass if + // they actually override the method. Same goes for -pointInside:withEvent: below. Many UIKit classes use that + // pattern for meaningful reductions of message send overhead in hot code (especially event handling). + + // Set boolean so this method can be re-entrant. If the node subclass wants to default to / make use of UIView + // hitTest:, it will call it on the view, which is _ASDisplayView. After calling into the node, any additional calls + // should use the UIView implementation of hitTest: + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (!_inHitTest) { + _inHitTest = YES; + UIView *hitView = [node hitTest:point withEvent:event]; + _inHitTest = NO; + return hitView; + } else { + return [super hitTest:point withEvent:event]; + } +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + // See comments in -hitTest:withEvent: for the strategy here. + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (!_inPointInside) { + _inPointInside = YES; + BOOL result = [node pointInside:point withEvent:event]; + _inPointInside = NO; + return result; + } else { + return [super pointInside:point withEvent:event]; + } +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node gestureRecognizerShouldBegin:gestureRecognizer]; +} + +- (void)tintColorDidChange +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super tintColorDidChange]; + + [node tintColorDidChange]; +} + +#pragma mark UIResponder Handling + +#define IMPLEMENT_RESPONDER_METHOD(__sel, __nodeMethodOverride, __viewMethodOverride) \ +- (BOOL)__sel\ +{\ + ASDisplayNode *node = _asyncdisplaykit_node; /* Create strong reference to weak ivar. */ \ + /* Check if we can call through to ASDisplayNode subclass directly */ \ + if (node.methodOverrides & __nodeMethodOverride) { \ + return [node __sel]; \ + } else { \ + /* Prevent an infinite loop in here if [super __sel] was called on a \ + / _ASDisplayView subclass */ \ + if (self->_methodOverrides & __viewMethodOverride) { \ + /* Call through to views superclass as we expect super was called from the + _ASDisplayView subclass and a node subclass does not overwrite __sel */ \ + return [self __##__sel]; \ + } else { \ + /* Call through to internal node __sel method that will consider the view in responding */ \ + return [node __##__sel]; \ + } \ + } \ +}\ +/* All __ prefixed methods are called from ASDisplayNode to let the view decide in what UIResponder state they \ +are not overridden by a ASDisplayNode subclass */ \ +- (BOOL)__##__sel \ +{ \ + return [super __sel]; \ +} \ + +IMPLEMENT_RESPONDER_METHOD(canBecomeFirstResponder, + ASDisplayNodeMethodOverrideCanBecomeFirstResponder, + _ASDisplayViewMethodOverrideCanBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(becomeFirstResponder, + ASDisplayNodeMethodOverrideBecomeFirstResponder, + _ASDisplayViewMethodOverrideBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(canResignFirstResponder, + ASDisplayNodeMethodOverrideCanResignFirstResponder, + _ASDisplayViewMethodOverrideCanResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(resignFirstResponder, + ASDisplayNodeMethodOverrideResignFirstResponder, + _ASDisplayViewMethodOverrideResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(isFirstResponder, + ASDisplayNodeMethodOverrideIsFirstResponder, + _ASDisplayViewMethodOverrideIsFirstResponder); + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + // We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:. + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return ([super canPerformAction:action withSender:sender] || [node respondsToSelector:action]); +} + +- (void)layoutMarginsDidChange +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super layoutMarginsDidChange]; + + [node layoutMarginsDidChange]; +} + +- (void)safeAreaInsetsDidChange +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super safeAreaInsetsDidChange]; + + [node safeAreaInsetsDidChange]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + // Ideally, we would implement -targetForAction:withSender: and simply return the node where we don't respond personally. + // Unfortunately UIResponder's default implementation of -targetForAction:withSender: doesn't follow its own documentation. It doesn't call -targetForAction:withSender: up the responder chain when -canPerformAction:withSender: fails, but instead merely calls -canPerformAction:withSender: on itself and then up the chain. rdar://20111500. + // Consequently, to forward responder-chain actions to our node, we override -canPerformAction:withSender: (used by the chain) to indicate support for responder chain-driven actions that our node supports, and then provide the node as a forwarding target here. + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return node; +} + +#if TARGET_OS_TV +#pragma mark - tvOS +- (BOOL)canBecomeFocused +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node canBecomeFocused]; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node didUpdateFocusInContext:context withAnimationCoordinator:coordinator]; +} + +- (void)setNeedsFocusUpdate +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node setNeedsFocusUpdate]; +} + +- (void)updateFocusIfNeeded +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node updateFocusIfNeeded]; +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node shouldUpdateFocusInContext:context]; +} + +- (UIView *)preferredFocusedView +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node preferredFocusedView]; +} +#endif +@end diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASDisplayViewAccessiblity.h b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayViewAccessiblity.h new file mode 100644 index 0000000000..9d0bf0719a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayViewAccessiblity.h @@ -0,0 +1,17 @@ +// +// _ASDisplayViewAccessiblity.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +// WARNING: When dealing with accessibility elements, please use the `accessibilityElements` +// property instead of the older methods e.g. `accessibilityElementCount()`. While the older methods +// should still work as long as accessibility is enabled, this framework provides no guarantees on +// their correctness. For details, see +// https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements diff --git a/submodules/AsyncDisplayKit/Source/Details/_ASDisplayViewAccessiblity.mm b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayViewAccessiblity.mm new file mode 100644 index 0000000000..f43e96c3ee --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Details/_ASDisplayViewAccessiblity.mm @@ -0,0 +1,351 @@ +// +// _ASDisplayViewAccessiblity.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef ASDK_ACCESSIBILITY_DISABLE + +#import +#import +#import +#import +#import +#import +#import + +#import + +NS_INLINE UIAccessibilityTraits InteractiveAccessibilityTraitsMask() { + return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; +} + +#pragma mark - UIAccessibilityElement + +@protocol ASAccessibilityElementPositioning + +@property (nonatomic, readonly) CGRect accessibilityFrame; + +@end + +typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id, id); + +/// Sort accessiblity elements first by y and than by x origin. +static void SortAccessibilityElements(NSMutableArray *elements) +{ + ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + + static SortAccessibilityElementsComparator comparator = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + comparator = ^NSComparisonResult(id a, id b) { + CGPoint originA = a.accessibilityFrame.origin; + CGPoint originB = b.accessibilityFrame.origin; + if (originA.y == originB.y) { + if (originA.x == originB.x) { + return NSOrderedSame; + } + return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending; + } + return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending; + }; + }); + [elements sortUsingComparator:comparator]; +} + +@interface ASAccessibilityElement : UIAccessibilityElement + +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) ASDisplayNode *containerNode; + ++ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode; + +@end + +@implementation ASAccessibilityElement + ++ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode +{ + ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:container]; + accessibilityElement.node = node; + accessibilityElement.containerNode = containerNode; + accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier; + accessibilityElement.accessibilityLabel = node.accessibilityLabel; + accessibilityElement.accessibilityHint = node.accessibilityHint; + accessibilityElement.accessibilityValue = node.accessibilityValue; + accessibilityElement.accessibilityTraits = node.accessibilityTraits; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; + } +#endif + return accessibilityElement; +} + +- (CGRect)accessibilityFrame +{ + CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; + accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.accessibilityContainer); + return accessibilityFrame; +} + +@end + +#pragma mark - _ASDisplayView / UIAccessibilityContainer + +@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction + +@property (nonatomic) UIView *container; +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) ASDisplayNode *containerNode; + +@end + +@implementation ASAccessibilityCustomAction + +- (CGRect)accessibilityFrame +{ + CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; + accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.container); + return accessibilityFrame; +} + +@end + +/// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container +static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) +{ + ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + + ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) { + // For every subnode that is layer backed or it's supernode has subtree rasterization enabled + // we have to create a UIAccessibilityElement as no view for this node exists + if (currentNode != containerNode && currentNode.isAccessibilityElement) { + UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode containerNode:containerNode]; + [elements addObject:accessibilityElement]; + } + }); +} + +static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, UIView *view, NSMutableArray *elements) { + UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:container containerNode:container]; + + NSMutableArray *labeledNodes = [[NSMutableArray alloc] init]; + NSMutableArray *actions = [[NSMutableArray alloc] init]; + std::queue queue; + queue.push(container); + + // If the container does not have an accessibility label set, or if the label is meant for custom + // actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overriden + // value and do not perform the aggregation. + BOOL shouldAggregateSubnodeLabels = + (container.accessibilityLabel.length == 0) || + (container.accessibilityTraits & InteractiveAccessibilityTraitsMask()); + + ASDisplayNode *node = nil; + while (!queue.empty()) { + node = queue.front(); + queue.pop(); + + if (node != container && node.isAccessibilityContainer) { + CollectAccessibilityElementsForContainer(node, view, elements); + continue; + } + + if (node.accessibilityLabel.length > 0) { + if (node.accessibilityTraits & InteractiveAccessibilityTraitsMask()) { + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)]; + action.node = node; + action.containerNode = node.supernode; + action.container = node.supernode.view; + [actions addObject:action]; + } else if (node == container || shouldAggregateSubnodeLabels) { + // Even though not surfaced to UIKit, create a non-interactive element for purposes of building sorted aggregated label. + ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node containerNode:container]; + [labeledNodes addObject:nonInteractiveElement]; + } + } + + for (ASDisplayNode *subnode in node.subnodes) { + queue.push(subnode); + } + } + + SortAccessibilityElements(labeledNodes); + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; + NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; + [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (idx != 0) { + [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; + } + [attributedLabel appendAttributedString:(NSAttributedString *)obj]; + }]; + accessiblityElement.accessibilityAttributedLabel = attributedLabel; + } else +#endif + { + NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; + accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + } + + SortAccessibilityElements(actions); + accessiblityElement.accessibilityCustomActions = actions; + + [elements addObject:accessiblityElement]; +} + +/// Collect all accessibliity elements for a given view and view node +static void CollectAccessibilityElementsForView(UIView *view, NSMutableArray *elements) +{ + ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + + ASDisplayNode *node = view.asyncdisplaykit_node; + + static Class telegramListViewClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + telegramListViewClass = NSClassFromString(@"Display.ListView"); + }); + BOOL anySubNodeIsCollection = (nil != ASDisplayNodeFindFirstNode(node, + ^BOOL(ASDisplayNode *nodeToCheck) { + if (telegramListViewClass != nil && [nodeToCheck isKindOfClass:telegramListViewClass]) { + return true; + } + return false; + /*return ASDynamicCast(nodeToCheck, ASCollectionNode) != nil || + ASDynamicCast(nodeToCheck, ASTableNode) != nil;*/ + })); + + if (node.isAccessibilityContainer && !anySubNodeIsCollection) { + CollectAccessibilityElementsForContainer(node, view, elements); + return; + } + + // Handle rasterize case + if (node.rasterizesSubtree) { + CollectUIAccessibilityElementsForNode(node, node, view, elements); + return; + } + + for (ASDisplayNode *subnode in node.subnodes) { + if (subnode.isAccessibilityElement) { + + // An accessiblityElement can either be a UIView or a UIAccessibilityElement + if (subnode.isLayerBacked) { + // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node + UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:subnode containerNode:node]; + [elements addObject:accessiblityElement]; + } else { + // Accessiblity element is not layer backed just add the view as accessibility element + [elements addObject:subnode.view]; + } + } else if (subnode.isLayerBacked) { + // Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement + CollectUIAccessibilityElementsForNode(subnode, node, view, elements); + } else if ([subnode accessibilityElementCount] > 0) { + // UIView is itself a UIAccessibilityContainer just add it + [elements addObject:subnode.view]; + } + } +} + +@interface _ASDisplayView () { + NSArray *_accessibilityElements; +} + +@end + +@implementation _ASDisplayView (UIAccessibilityContainer) + +- (void)accessibilityElementDidBecomeFocused { + ASDisplayNode *viewNode = self.asyncdisplaykit_node; + if ([viewNode respondsToSelector:@selector(accessibilityElementDidBecomeFocused)]) { + [viewNode accessibilityElementDidBecomeFocused]; + } +} + +/*- (bool)accessibilityActivate { + ASDisplayNode *viewNode = self.asyncdisplaykit_node; + if ([viewNode respondsToSelector:@selector(accessibilityActivate)]) { + return [viewNode accessibilityActivate]; + } + return false; +}*/ + +#pragma mark - UIAccessibility + +- (void)setAccessibilityElements:(NSArray *)accessibilityElements +{ + ASDisplayNodeAssertMainThread(); + _accessibilityElements = nil; +} + +- (NSArray *)accessibilityElements +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNode *viewNode = self.asyncdisplaykit_node; + if (viewNode == nil) { + return @[]; + } + if (true || _accessibilityElements == nil) { + _accessibilityElements = [viewNode accessibilityElements]; + } + return _accessibilityElements; +} + +@end + +@implementation ASDisplayNode (AccessibilityInternal) + +- (NSArray *)accessibilityElements +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since node is not loaded"); + return @[]; + } + NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init]; + CollectAccessibilityElementsForView(self.view, accessibilityElements); + SortAccessibilityElements(accessibilityElements); + return accessibilityElements; +} + +@end + +@implementation _ASDisplayView (UIAccessibilityAction) + +- (BOOL)accessibilityActivate { + return [self.asyncdisplaykit_node accessibilityActivate]; +} + +- (void)accessibilityIncrement { + [self.asyncdisplaykit_node accessibilityIncrement]; +} + +- (void)accessibilityDecrement { + [self.asyncdisplaykit_node accessibilityDecrement]; +} + +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { + return [self.asyncdisplaykit_node accessibilityScroll:direction]; +} + +- (BOOL)accessibilityPerformEscape { + return [self.asyncdisplaykit_node accessibilityPerformEscape]; +} + +- (BOOL)accessibilityPerformMagicTap { + return [self.asyncdisplaykit_node accessibilityPerformMagicTap]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/IGListAdapter+AsyncDisplayKit.h b/submodules/AsyncDisplayKit/Source/IGListAdapter+AsyncDisplayKit.h new file mode 100644 index 0000000000..76b53c1933 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/IGListAdapter+AsyncDisplayKit.h @@ -0,0 +1,37 @@ +// +// IGListAdapter+AsyncDisplayKit.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_IG_LIST_KIT + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionNode; + +@interface IGListAdapter (AsyncDisplayKit) + +/** + * Connect this list adapter to the given collection node. + * + * @param collectionNode The collection node to drive with this list adapter. + * + * @note This method may only be called once per list adapter, + * and it must be called on the main thread. -[UIViewController init] + * is a good place to call it. This method does not retain the collection node. + */ +- (void)setASDKCollectionNode:(ASCollectionNode *)collectionNode; + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_IG_LIST_KIT diff --git a/submodules/AsyncDisplayKit/Source/IGListAdapter+AsyncDisplayKit.mm b/submodules/AsyncDisplayKit/Source/IGListAdapter+AsyncDisplayKit.mm new file mode 100644 index 0000000000..c3e81d3e33 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/IGListAdapter+AsyncDisplayKit.mm @@ -0,0 +1,53 @@ +// +// IGListAdapter+AsyncDisplayKit.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_IG_LIST_KIT + +#import +#import +#import +#import + +@implementation IGListAdapter (AsyncDisplayKit) + +- (void)setASDKCollectionNode:(ASCollectionNode *)collectionNode +{ + ASDisplayNodeAssertMainThread(); + + // Attempt to retrieve previous data source. + ASIGListAdapterBasedDataSource *dataSource = objc_getAssociatedObject(self, _cmd); + // Bomb if we already made one. + if (dataSource != nil) { + ASDisplayNodeFailAssert(@"Attempt to call %@ multiple times on the same list adapter. Not currently allowed!", NSStringFromSelector(_cmd)); + return; + } + + // Make a data source and retain it. + dataSource = [[ASIGListAdapterBasedDataSource alloc] initWithListAdapter:self collectionDelegate:collectionNode.delegate]; + objc_setAssociatedObject(self, _cmd, dataSource, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Attach the data source to the collection node. + collectionNode.dataSource = dataSource; + collectionNode.delegate = dataSource; + __weak IGListAdapter *weakSelf = self; + [collectionNode onDidLoad:^(__kindof ASCollectionNode * _Nonnull collectionNode) { +#if IG_LIST_COLLECTION_VIEW + // We manually set the superclass of ASCollectionView to IGListCollectionView at runtime if needed. + weakSelf.collectionView = (IGListCollectionView *)collectionNode.view; +#else + weakSelf.collectionView = collectionNode.view; +#endif + }]; +} + +@end + +#endif // AS_IG_LIST_KIT diff --git a/submodules/AsyncDisplayKit/Source/Info.plist b/submodules/AsyncDisplayKit/Source/Info.plist new file mode 100644 index 0000000000..fbe1e6b314 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutElement.h b/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutElement.h new file mode 100644 index 0000000000..109816de20 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutElement.h @@ -0,0 +1,26 @@ +// +// ASAbsoluteLayoutElement.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Layout options that can be defined for an ASLayoutElement being added to a ASAbsoluteLayoutSpec. + */ +@protocol ASAbsoluteLayoutElement + +/** + * @abstract The position of this object within its parent spec. + */ +@property (nonatomic) CGPoint layoutPosition; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutSpec.h new file mode 100644 index 0000000000..61bfbf8cd5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutSpec.h @@ -0,0 +1,45 @@ +// +// ASAbsoluteLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** How much space the spec will take up. */ +typedef NS_ENUM(NSInteger, ASAbsoluteLayoutSpecSizing) { + /** The spec will take up the maximum size possible. */ + ASAbsoluteLayoutSpecSizingDefault, + /** Computes a size for the spec that is the union of all childrens' frames. */ + ASAbsoluteLayoutSpecSizingSizeToFit, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + A layout spec that positions children at fixed positions. + */ +@interface ASAbsoluteLayoutSpec : ASLayoutSpec + +/** + How much space will the spec taken up + */ +@property (nonatomic) ASAbsoluteLayoutSpecSizing sizing; + +/** + @param sizing How much space the spec will take up + @param children Children to be positioned at fixed positions + */ ++ (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + @param children Children to be positioned at fixed positions + */ ++ (instancetype)absoluteLayoutSpecWithChildren:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutSpec.mm new file mode 100644 index 0000000000..18a3a3a51c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASAbsoluteLayoutSpec.mm @@ -0,0 +1,104 @@ +// +// ASAbsoluteLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import + +#pragma mark - ASAbsoluteLayoutSpec + +@implementation ASAbsoluteLayoutSpec + +#pragma mark - Class + ++ (instancetype)absoluteLayoutSpecWithChildren:(NSArray *)children NS_RETURNS_RETAINED +{ + return [[self alloc] initWithChildren:children]; +} + ++ (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children NS_RETURNS_RETAINED +{ + return [[self alloc] initWithSizing:sizing children:children]; +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + return [self initWithChildren:nil]; +} + +- (instancetype)initWithChildren:(NSArray *)children +{ + return [self initWithSizing:ASAbsoluteLayoutSpecSizingDefault children:children]; +} + +- (instancetype)initWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children +{ + if (!(self = [super init])) { + return nil; + } + + _sizing = sizing; + self.children = children; + + return self; +} + +#pragma mark - ASLayoutSpec + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + CGSize size = { + ASPointsValidForSize(constrainedSize.max.width) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.width, + ASPointsValidForSize(constrainedSize.max.height) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.height + }; + + NSArray *children = self.children; + ASLayout *rawSublayouts[children.count]; + int i = 0; + + for (id child in children) { + CGPoint layoutPosition = child.style.layoutPosition; + CGSize autoMaxSize = { + constrainedSize.max.width - layoutPosition.x, + constrainedSize.max.height - layoutPosition.y + }; + + const ASSizeRange childConstraint = ASLayoutElementSizeResolveAutoSize(child.style.size, size, {{0,0}, autoMaxSize}); + + ASLayout *sublayout = [child layoutThatFits:childConstraint parentSize:size]; + sublayout.position = layoutPosition; + rawSublayouts[i++] = sublayout; + } + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:i]; + + if (_sizing == ASAbsoluteLayoutSpecSizingSizeToFit || isnan(size.width)) { + size.width = constrainedSize.min.width; + for (ASLayout *sublayout in sublayouts) { + size.width = MAX(size.width, sublayout.position.x + sublayout.size.width); + } + } + + if (_sizing == ASAbsoluteLayoutSpecSizingSizeToFit || isnan(size.height)) { + size.height = constrainedSize.min.height; + for (ASLayout *sublayout in sublayouts) { + size.height = MAX(size.height, sublayout.position.y + sublayout.size.height); + } + } + + return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:sublayouts]; +} + +@end + diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASAsciiArtBoxCreator.h b/submodules/AsyncDisplayKit/Source/Layout/ASAsciiArtBoxCreator.h new file mode 100644 index 0000000000..83c7bec414 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASAsciiArtBoxCreator.h @@ -0,0 +1,60 @@ +// +// ASAsciiArtBoxCreator.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASLayoutElementAsciiArtProtocol +/** + * Returns an ascii-art representation of this object and its children. + * For example, an ASInsetSpec may return something like this: + * + * --ASInsetLayoutSpec-- + * | ASTextNode | + * --------------------- + */ +- (NSString *)asciiArtString; + +/** + * returns the name of this object that will display in the ascii art. Usually this can + * simply be NSStringFromClass([self class]). + */ +- (NSString *)asciiArtName; + +@end + +/** + * A that takes a parent and its children and renders as ascii art box. + */ +@interface ASAsciiArtBoxCreator : NSObject + +/** + * Renders an ascii art box with the children aligned horizontally + * Example: + * ------------ASStackLayoutSpec----------- + * | ASTextNode ASTextNode ASTextNode | + * ---------------------------------------- + */ ++ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent; + +/** + * Renders an ascii art box with the children aligned vertically. + * Example: + * --ASStackLayoutSpec-- + * | ASTextNode | + * | ASTextNode | + * | ASTextNode | + * --------------------- + */ ++ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASAsciiArtBoxCreator.mm b/submodules/AsyncDisplayKit/Source/Layout/ASAsciiArtBoxCreator.mm new file mode 100644 index 0000000000..78eb572ead --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASAsciiArtBoxCreator.mm @@ -0,0 +1,186 @@ +// +// ASAsciiArtBoxCreator.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +static const NSUInteger kDebugBoxPadding = 2; + +typedef NS_ENUM(NSUInteger, PIDebugBoxPaddingLocation) +{ + PIDebugBoxPaddingLocationFront, + PIDebugBoxPaddingLocationEnd, + PIDebugBoxPaddingLocationBoth +}; + +@interface NSString(PIDebugBox) + +@end + +@implementation NSString(PIDebugBox) + ++ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount NS_RETURNS_RETAINED +{ + NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[stringToRepeat length] * repeatCount]; + for (NSUInteger index = 0; index < repeatCount; index++) { + [string appendString:stringToRepeat]; + } + return [string copy]; +} + +- (NSString *)debugbox_stringByAddingPadding:(NSString *)padding count:(NSUInteger)count location:(PIDebugBoxPaddingLocation)location +{ + NSString *paddingString = [NSString debugbox_stringWithString:padding repeatedCount:count]; + switch (location) { + case PIDebugBoxPaddingLocationFront: + return [NSString stringWithFormat:@"%@%@", paddingString, self]; + case PIDebugBoxPaddingLocationEnd: + return [NSString stringWithFormat:@"%@%@", self, paddingString]; + case PIDebugBoxPaddingLocationBoth: + return [NSString stringWithFormat:@"%@%@%@", paddingString, self, paddingString]; + } + return [self copy]; +} + +@end + +@implementation ASAsciiArtBoxCreator + ++ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent +{ + if ([children count] == 0) { + return parent; + } + + NSMutableArray *childrenLines = [NSMutableArray array]; + + // split the children into lines + NSUInteger lineCountPerChild = 0; + for (NSString *child in children) { + NSArray *lines = [child componentsSeparatedByString:@"\n"]; + lineCountPerChild = MAX(lineCountPerChild, [lines count]); + } + + for (NSString *child in children) { + NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy]; + NSUInteger topPadding = ceil((CGFloat)(lineCountPerChild - [lines count])/2.0); + NSUInteger bottomPadding = (lineCountPerChild - [lines count])/2.0; + NSUInteger lineLength = [lines[0] length]; + + for (NSUInteger index = 0; index < topPadding; index++) { + [lines insertObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength] atIndex:0]; + } + for (NSUInteger index = 0; index < bottomPadding; index++) { + [lines addObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength]]; + } + [childrenLines addObject:lines]; + } + + NSMutableArray *concatenatedLines = [NSMutableArray array]; + NSString *padding = [NSString debugbox_stringWithString:@" " repeatedCount:kDebugBoxPadding]; + for (NSUInteger index = 0; index < lineCountPerChild; index++) { + NSMutableString *line = [[NSMutableString alloc] init]; + [line appendFormat:@"|%@",padding]; + for (NSArray *childLines in childrenLines) { + [line appendFormat:@"%@%@", childLines[index], padding]; + } + [line appendString:@"|"]; + [concatenatedLines addObject:line]; + } + + // surround the lines in a box + NSUInteger totalLineLength = [concatenatedLines[0] length]; + if (totalLineLength < [parent length]) { + NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - totalLineLength; + NSUInteger leftPadding = ceil((CGFloat)difference/2.0); + NSUInteger rightPadding = difference/2; + + NSString *leftString = [@"|" debugbox_stringByAddingPadding:@" " count:leftPadding location:PIDebugBoxPaddingLocationEnd]; + NSString *rightString = [@"|" debugbox_stringByAddingPadding:@" " count:rightPadding location:PIDebugBoxPaddingLocationFront]; + + NSMutableArray *paddedLines = [NSMutableArray array]; + for (NSString *line in concatenatedLines) { + NSString *paddedLine = [line stringByReplacingOccurrencesOfString:@"|" withString:leftString options:NSCaseInsensitiveSearch range:NSMakeRange(0, 1)]; + paddedLine = [paddedLine stringByReplacingOccurrencesOfString:@"|" withString:rightString options:NSCaseInsensitiveSearch range:NSMakeRange([paddedLine length] - 1, 1)]; + [paddedLines addObject:paddedLine]; + } + concatenatedLines = paddedLines; + // totalLineLength += difference; + } + concatenatedLines = [self appendTopAndBottomToBoxString:concatenatedLines parent:parent]; + return [concatenatedLines componentsJoinedByString:@"\n"]; + +} + ++ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent +{ + if ([children count] == 0) { + return parent; + } + + NSMutableArray *childrenLines = [NSMutableArray array]; + + NSUInteger maxChildLength = 0; + for (NSString *child in children) { + NSArray *lines = [child componentsSeparatedByString:@"\n"]; + maxChildLength = MAX(maxChildLength, [lines[0] length]); + } + + NSUInteger rightPadding = 0; + NSUInteger leftPadding = 0; + + if (maxChildLength < [parent length]) { + NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - maxChildLength; + leftPadding = ceil((CGFloat)difference/2.0); + rightPadding = difference/2; + } + + NSString *rightPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightPadding + kDebugBoxPadding]; + NSString *leftPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftPadding + kDebugBoxPadding]; + + for (NSString *child in children) { + NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy]; + + NSUInteger leftLinePadding = ceil((CGFloat)(maxChildLength - [lines[0] length])/2.0); + NSUInteger rightLinePadding = (maxChildLength - [lines[0] length])/2.0; + + for (NSString *line in lines) { + NSString *rightLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightLinePadding]; + rightLinePaddingString = [NSString stringWithFormat:@"%@%@|", rightLinePaddingString, rightPaddingString]; + + NSString *leftLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftLinePadding]; + leftLinePaddingString = [NSString stringWithFormat:@"|%@%@", leftLinePaddingString, leftPaddingString]; + + NSString *paddingLine = [NSString stringWithFormat:@"%@%@%@", leftLinePaddingString, line, rightLinePaddingString]; + [childrenLines addObject:paddingLine]; + } + } + + childrenLines = [self appendTopAndBottomToBoxString:childrenLines parent:parent]; + return [childrenLines componentsJoinedByString:@"\n"]; +} + ++ (NSMutableArray *)appendTopAndBottomToBoxString:(NSMutableArray *)boxStrings parent:(NSString *)parent +{ + NSUInteger totalLineLength = [boxStrings[0] length]; + [boxStrings addObject:[NSString debugbox_stringWithString:@"-" repeatedCount:totalLineLength]]; + + NSUInteger leftPadding = ceil(((CGFloat)(totalLineLength - [parent length]))/2.0); + NSUInteger rightPadding = (totalLineLength - [parent length])/2; + + NSString *topLine = [parent debugbox_stringByAddingPadding:@"-" count:leftPadding location:PIDebugBoxPaddingLocationFront]; + topLine = [topLine debugbox_stringByAddingPadding:@"-" count:rightPadding location:PIDebugBoxPaddingLocationEnd]; + [boxStrings insertObject:topLine atIndex:0]; + + return boxStrings; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASBackgroundLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASBackgroundLayoutSpec.h new file mode 100644 index 0000000000..032a240bbd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASBackgroundLayoutSpec.h @@ -0,0 +1,34 @@ +// +// ASBackgroundLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Lays out a single layoutElement child, then lays out a background layoutElement instance behind it stretched to its size. + */ +@interface ASBackgroundLayoutSpec : ASLayoutSpec + +/** + * Background layoutElement for this layout spec + */ +@property (nonatomic) id background; + +/** + * Creates and returns an ASBackgroundLayoutSpec object + * + * @param child A child that is laid out to determine the size of this spec. + * @param background A layoutElement object that is laid out behind the child. + */ ++ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASBackgroundLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASBackgroundLayoutSpec.mm new file mode 100644 index 0000000000..31dd75487a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASBackgroundLayoutSpec.mm @@ -0,0 +1,92 @@ +// +// ASBackgroundLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import + +static NSUInteger const kForegroundChildIndex = 0; +static NSUInteger const kBackgroundChildIndex = 1; + +@implementation ASBackgroundLayoutSpec + +#pragma mark - Class + ++ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background NS_RETURNS_RETAINED +{ + return [[self alloc] initWithChild:child background:background]; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithChild:(id)child background:(id)background +{ + if (!(self = [super init])) { + return nil; + } + self.child = child; + self.background = background; + return self; +} + +#pragma mark - ASLayoutSpec + +/** + * First layout the contents, then fit the background image. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASLayout *contentsLayout = [self.child layoutThatFits:constrainedSize parentSize:parentSize]; + + ASLayout *rawSublayouts[2]; + int i = 0; + if (self.background) { + // Size background to exactly the same size. + ASLayout *backgroundLayout = [self.background layoutThatFits:ASSizeRangeMake(contentsLayout.size) + parentSize:parentSize]; + backgroundLayout.position = CGPointZero; + rawSublayouts[i++] = backgroundLayout; + } + contentsLayout.position = CGPointZero; + rawSublayouts[i++] = contentsLayout; + + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:i]; + return [ASLayout layoutWithLayoutElement:self size:contentsLayout.size sublayouts:sublayouts]; +} + +#pragma mark - Background + +- (void)setChild:(id)child +{ + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + [super setChild:child atIndex:kForegroundChildIndex]; +} + +- (id)child +{ + return [super childAtIndex:kForegroundChildIndex]; +} + +- (void)setBackground:(id)background +{ + ASDisplayNodeAssertNotNil(background, @"Background cannot be nil"); + [super setChild:background atIndex:kBackgroundChildIndex]; +} + +- (id)background +{ + return [super childAtIndex:kBackgroundChildIndex]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASCenterLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASCenterLayoutSpec.h new file mode 100644 index 0000000000..dd1f99aa5a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASCenterLayoutSpec.h @@ -0,0 +1,70 @@ +// +// ASCenterLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * How the child is centered within the spec. + * + * The default option will position the child at {0,0} relatively to the layout bound. + * Swift: use [] for the default behavior. + */ +typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecCenteringOptions) { + /** The child is positioned in {0,0} relatively to the layout bounds */ + ASCenterLayoutSpecCenteringNone = 0, + /** The child is centered along the X axis */ + ASCenterLayoutSpecCenteringX = 1 << 0, + /** The child is centered along the Y axis */ + ASCenterLayoutSpecCenteringY = 1 << 1, + /** Convenience option to center both along the X and Y axis */ + ASCenterLayoutSpecCenteringXY = ASCenterLayoutSpecCenteringX | ASCenterLayoutSpecCenteringY +}; + +/** + * How much space the spec will take up. + * + * The default option will allow the spec to take up the maximum size possible. + * Swift: use [] for the default behavior. + */ +typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecSizingOptions) { + /** The spec will take up the maximum size possible */ + ASCenterLayoutSpecSizingOptionDefault = ASRelativeLayoutSpecSizingOptionDefault, + /** The spec will take up the minimum size possible along the X axis */ + ASCenterLayoutSpecSizingOptionMinimumX = ASRelativeLayoutSpecSizingOptionMinimumWidth, + /** The spec will take up the minimum size possible along the Y axis */ + ASCenterLayoutSpecSizingOptionMinimumY = ASRelativeLayoutSpecSizingOptionMinimumHeight, + /** Convenience option to take up the minimum size along both the X and Y axis */ + ASCenterLayoutSpecSizingOptionMinimumXY = ASRelativeLayoutSpecSizingOptionMinimumSize +}; + +NS_ASSUME_NONNULL_BEGIN + +/** Lays out a single layoutElement child and position it so that it is centered into the layout bounds. + * NOTE: ASRelativeLayoutSpec offers all of the capabilities of Center, and more. + * Check it out if you would like to be able to position the child at any corner or the middle of an edge. + */ +@interface ASCenterLayoutSpec : ASRelativeLayoutSpec + +@property (nonatomic) ASCenterLayoutSpecCenteringOptions centeringOptions; +@property (nonatomic) ASCenterLayoutSpecSizingOptions sizingOptions; + +/** + * Initializer. + * + * @param centeringOptions How the child is centered. + * @param sizingOptions How much space will be taken up. + * @param child The child to center. + */ ++ (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions + child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASCenterLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASCenterLayoutSpec.mm new file mode 100644 index 0000000000..107f317c0c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASCenterLayoutSpec.mm @@ -0,0 +1,76 @@ +// +// ASCenterLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@implementation ASCenterLayoutSpec +{ + ASCenterLayoutSpecCenteringOptions _centeringOptions; + ASCenterLayoutSpecSizingOptions _sizingOptions; +} + +- (instancetype)initWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions + child:(id)child; +{ + ASRelativeLayoutSpecPosition verticalPosition = [self verticalPositionFromCenteringOptions:centeringOptions]; + ASRelativeLayoutSpecPosition horizontalPosition = [self horizontalPositionFromCenteringOptions:centeringOptions]; + + if (!(self = [super initWithHorizontalPosition:horizontalPosition verticalPosition:verticalPosition sizingOption:sizingOptions child:child])) { + return nil; + } + _centeringOptions = centeringOptions; + _sizingOptions = sizingOptions; + return self; +} + ++ (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions + child:(id)child NS_RETURNS_RETAINED +{ + return [[self alloc] initWithCenteringOptions:centeringOptions sizingOptions:sizingOptions child:child]; +} + +- (void)setCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _centeringOptions = centeringOptions; + + [self setHorizontalPosition:[self horizontalPositionFromCenteringOptions:centeringOptions]]; + [self setVerticalPosition:[self verticalPositionFromCenteringOptions:centeringOptions]]; +} + +- (void)setSizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _sizingOptions = sizingOptions; + [self setSizingOption:sizingOptions]; +} + +- (ASRelativeLayoutSpecPosition)horizontalPositionFromCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions +{ + if ((centeringOptions & ASCenterLayoutSpecCenteringX) != 0) { + return ASRelativeLayoutSpecPositionCenter; + } else { + return ASRelativeLayoutSpecPositionNone; + } +} + +- (ASRelativeLayoutSpecPosition)verticalPositionFromCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions +{ + if ((centeringOptions & ASCenterLayoutSpecCenteringY) != 0) { + return ASRelativeLayoutSpecPositionCenter; + } else { + return ASRelativeLayoutSpecPositionNone; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASCornerLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASCornerLayoutSpec.h new file mode 100644 index 0000000000..799c9c28c3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASCornerLayoutSpec.h @@ -0,0 +1,75 @@ +// +// ASCornerLayoutSpec.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + The corner location for positioning corner element. + */ +typedef NS_ENUM(NSInteger, ASCornerLayoutLocation) { + ASCornerLayoutLocationTopLeft, + ASCornerLayoutLocationTopRight, + ASCornerLayoutLocationBottomLeft, + ASCornerLayoutLocationBottomRight, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + A layout spec that positions a corner element which relatives to the child element. + + @warning Both child element and corner element must have valid preferredSize for layout calculation. + */ +@interface ASCornerLayoutSpec : ASLayoutSpec + +/** + A layout spec that positions a corner element which relatives to the child element. + + @param child A child that is laid out to determine the size of this spec. + @param corner A layoutElement object that is laid out to a corner on the child. + @param location The corner position option. + @return An ASCornerLayoutSpec object with a given child and an layoutElement that act as corner. + */ +- (instancetype)initWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location AS_WARN_UNUSED_RESULT; + +/** + A layout spec that positions a corner element which relatives to the child element. + + @param child A child that is laid out to determine the size of this spec. + @param corner A layoutElement object that is laid out to a corner on the child. + @param location The corner position option. + @return An ASCornerLayoutSpec object with a given child and an layoutElement that act as corner. + */ ++ (instancetype)cornerLayoutSpecWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + A layoutElement object that is laid out to a corner on the child. + */ +@property (nonatomic) id corner; + +/** + The corner position option. + */ +@property (nonatomic) ASCornerLayoutLocation cornerLocation; + +/** + The point which offsets from the corner location. Use this property to make delta + distance from the default corner location. Default is CGPointZero. + */ +@property (nonatomic) CGPoint offset; + +/** + Whether should include corner element into layout size calculation. If included, + the layout size will be the union size of both child and corner; If not included, + the layout size will be only child's size. Default is NO. + */ +@property (nonatomic) BOOL wrapsCorner; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASCornerLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASCornerLayoutSpec.mm new file mode 100644 index 0000000000..6663b28935 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASCornerLayoutSpec.mm @@ -0,0 +1,165 @@ +// +// ASCornerLayoutSpec.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +CGPoint as_calculatedCornerOriginIn(CGRect baseFrame, CGSize cornerSize, ASCornerLayoutLocation cornerLocation, CGPoint offset) +{ + CGPoint cornerOrigin = CGPointZero; + CGPoint baseOrigin = baseFrame.origin; + CGSize baseSize = baseFrame.size; + + switch (cornerLocation) { + case ASCornerLayoutLocationTopLeft: + cornerOrigin.x = baseOrigin.x - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y - cornerSize.height / 2; + break; + case ASCornerLayoutLocationTopRight: + cornerOrigin.x = baseOrigin.x + baseSize.width - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y - cornerSize.height / 2; + break; + case ASCornerLayoutLocationBottomLeft: + cornerOrigin.x = baseOrigin.x - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y + baseSize.height - cornerSize.height / 2; + break; + case ASCornerLayoutLocationBottomRight: + cornerOrigin.x = baseOrigin.x + baseSize.width - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y + baseSize.height - cornerSize.height / 2; + break; + } + + cornerOrigin.x += offset.x; + cornerOrigin.y += offset.y; + + return cornerOrigin; +} + +static NSUInteger const kBaseChildIndex = 0; +static NSUInteger const kCornerChildIndex = 1; + +@interface ASCornerLayoutSpec() +@end + +@implementation ASCornerLayoutSpec + +- (instancetype)initWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location +{ + self = [super init]; + if (self) { + self.child = child; + self.corner = corner; + self.cornerLocation = location; + } + return self; +} + ++ (instancetype)cornerLayoutSpecWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location NS_RETURNS_RETAINED +{ + return [[self alloc] initWithChild:child corner:corner location:location]; +} + +#pragma mark - Children + +- (void)setChild:(id)child +{ + ASDisplayNodeAssertNotNil(child, @"Child shouldn't be nil."); + [super setChild:child atIndex:kBaseChildIndex]; +} + +- (id)child +{ + return [super childAtIndex:kBaseChildIndex]; +} + +- (void)setCorner:(id)corner +{ + ASDisplayNodeAssertNotNil(corner, @"Corner element cannot be nil."); + [super setChild:corner atIndex:kCornerChildIndex]; +} + +- (id)corner +{ + return [super childAtIndex:kCornerChildIndex]; +} + +#pragma mark - Calculation + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + CGSize size = { + ASPointsValidForSize(constrainedSize.max.width) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.width, + ASPointsValidForSize(constrainedSize.max.height) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.height + }; + + id child = self.child; + id corner = self.corner; + + // Element validation + [self _validateElement:child]; + [self _validateElement:corner]; + + CGRect childFrame = CGRectZero; + CGRect cornerFrame = CGRectZero; + + // Layout child + ASLayout *childLayout = [child layoutThatFits:constrainedSize parentSize:size]; + childFrame.size = childLayout.size; + + // Layout corner + ASLayout *cornerLayout = [corner layoutThatFits:constrainedSize parentSize:size]; + cornerFrame.size = cornerLayout.size; + + // Calculate corner's position + CGPoint relativePosition = as_calculatedCornerOriginIn(childFrame, cornerFrame.size, _cornerLocation, _offset); + + // Update corner's position + cornerFrame.origin = relativePosition; + + // Calculate size + CGRect frame = childFrame; + if (_wrapsCorner) { + frame = CGRectUnion(childFrame, cornerFrame); + frame.size = ASSizeRangeClamp(constrainedSize, frame.size); + } + + // Shift sublayouts' positions if they are off the bounds. + if (frame.origin.x != 0) { + CGFloat deltaX = frame.origin.x; + childFrame.origin.x -= deltaX; + cornerFrame.origin.x -= deltaX; + } + + if (frame.origin.y != 0) { + CGFloat deltaY = frame.origin.y; + childFrame.origin.y -= deltaY; + cornerFrame.origin.y -= deltaY; + } + + childLayout.position = childFrame.origin; + cornerLayout.position = cornerFrame.origin; + + return [ASLayout layoutWithLayoutElement:self size:frame.size sublayouts:@[childLayout, cornerLayout]]; +} + +- (void)_validateElement:(id )element +{ + // Validate non-nil element + if (element == nil) { + ASDisplayNodeAssertNotNil(element, @"[%@]: Must have a non-nil child/corner for layout calculation.", self.class); + } + // Validate preferredSize if needed + CGSize size = element.style.preferredSize; + if (!CGSizeEqualToSize(size, CGSizeZero) && !ASIsCGSizeValidForSize(size) && (size.width < 0 || (size.height < 0))) { + ASDisplayNodeFailAssert(@"[%@]: Should give a valid preferredSize value for %@ before corner's position calculation.", self.class, element); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASDimension.h b/submodules/AsyncDisplayKit/Source/Layout/ASDimension.h new file mode 100644 index 0000000000..0b3c6b7beb --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASDimension.h @@ -0,0 +1,315 @@ +// +// ASDimension.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - + +ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASPointsValidForLayout(CGFloat points) +{ + return ((isnormal(points) || points == 0.0) && points >= 0.0 && points < (CGFLOAT_MAX / 2.0)); +} + +ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForLayout(CGSize size) +{ + return (ASPointsValidForLayout(size.width) && ASPointsValidForLayout(size.height)); +} + +// Note we want YGUndefined (10E20) to be considered invalid, so we have picked a smaller number than CGFLOAT_MAX/2.0 +ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASPointsValidForSize(CGFloat points) +{ + return ((isnormal(points) || points == 0.0) && points >= 0.0 && points < 10000000.0); +} + +ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForSize(CGSize size) +{ + return (ASPointsValidForSize(size.width) && ASPointsValidForSize(size.height)); +} + +// Note we want YGUndefined (10E20) to be considered invalid, so we have picked a smaller number than CGFLOAT_MAX/2.0 +ASDISPLAYNODE_INLINE BOOL ASIsCGPositionPointsValidForLayout(CGFloat points) +{ + return ((isnormal(points) || points == 0.0) && points < 10000000.0); +} + +ASDISPLAYNODE_INLINE BOOL ASIsCGPositionValidForLayout(CGPoint point) +{ + return (ASIsCGPositionPointsValidForLayout(point.x) && ASIsCGPositionPointsValidForLayout(point.y)); +} + +ASDISPLAYNODE_INLINE BOOL ASIsCGRectValidForLayout(CGRect rect) +{ + return (ASIsCGPositionValidForLayout(rect.origin) && ASIsCGSizeValidForLayout(rect.size)); +} + +#pragma mark - ASDimension + +/** + * A dimension relative to constraints to be provided in the future. + * A ASDimension can be one of three types: + * + * "Auto" - This indicated "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. + * + * "Points" - Just a number. It will always resolve to exactly this amount. + * + * "Percent" - Multiplied to a provided parent amount to resolve a final amount. + */ +typedef NS_ENUM(NSInteger, ASDimensionUnit) { + /** This indicates "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. */ + ASDimensionUnitAuto, + /** Just a number. It will always resolve to exactly this amount. This is the default type. */ + ASDimensionUnitPoints, + /** Multiplied to a provided parent amount to resolve a final amount. */ + ASDimensionUnitFraction, +}; + +typedef struct { + ASDimensionUnit unit; + CGFloat value; +} ASDimension; + +/** + * Represents auto as ASDimension + */ +AS_EXTERN ASDimension const ASDimensionAuto; + +/** + * Returns a dimension with the specified type and value. + */ +ASOVERLOADABLE ASDISPLAYNODE_INLINE ASDimension ASDimensionMake(ASDimensionUnit unit, CGFloat value) +{ + if (unit == ASDimensionUnitAuto ) { + ASDisplayNodeCAssert(value == 0, @"ASDimension auto value must be 0."); + } else if (unit == ASDimensionUnitPoints) { + ASDisplayNodeCAssertPositiveReal(@"Points", value); + } else if (unit == ASDimensionUnitFraction) { + ASDisplayNodeCAssert( 0 <= value && value <= 1.0, @"ASDimension fraction value (%f) must be between 0 and 1.", value); + } + ASDimension dimension; + dimension.unit = unit; + dimension.value = value; + return dimension; +} + +/** + * Returns a dimension with the specified points value. + */ +ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMake(CGFloat points) +{ + return ASDimensionMake(ASDimensionUnitPoints, points); +} + +/** + * Returns a dimension by parsing the specified dimension string. + * Examples: ASDimensionMake(@"50%") = ASDimensionMake(ASDimensionUnitFraction, 0.5) + * ASDimensionMake(@"0.5pt") = ASDimensionMake(ASDimensionUnitPoints, 0.5) + */ +ASOVERLOADABLE AS_WARN_UNUSED_RESULT AS_EXTERN ASDimension ASDimensionMake(NSString *dimension); + +/** + * Returns a dimension with the specified points value. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMakeWithPoints(CGFloat points) +{ + ASDisplayNodeCAssertPositiveReal(@"Points", points); + return ASDimensionMake(ASDimensionUnitPoints, points); +} + +/** + * Returns a dimension with the specified fraction value. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMakeWithFraction(CGFloat fraction) +{ + ASDisplayNodeCAssert( 0 <= fraction && fraction <= 1.0, @"ASDimension fraction value (%f) must be between 0 and 1.", fraction); + return ASDimensionMake(ASDimensionUnitFraction, fraction); +} + +/** + * Returns whether two dimensions are equal. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASDimensionEqualToDimension(ASDimension lhs, ASDimension rhs) +{ + return (lhs.unit == rhs.unit && lhs.value == rhs.value); +} + +/** + * Returns a NSString representation of a dimension. + */ +AS_EXTERN AS_WARN_UNUSED_RESULT NSString *NSStringFromASDimension(ASDimension dimension); + +/** + * Resolve this dimension to a parent size. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT CGFloat ASDimensionResolve(ASDimension dimension, CGFloat parentSize, CGFloat autoSize) +{ + switch (dimension.unit) { + case ASDimensionUnitAuto: + return autoSize; + case ASDimensionUnitPoints: + return dimension.value; + case ASDimensionUnitFraction: + return dimension.value * parentSize; + } +} + +#pragma mark - ASLayoutSize + +/** + * Expresses a size with relative dimensions. Only used for calculations internally in ASDimension.h + */ +typedef struct { + ASDimension width; + ASDimension height; +} ASLayoutSize; + +AS_EXTERN ASLayoutSize const ASLayoutSizeAuto; + +/* + * Creates an ASLayoutSize with provided min and max dimensions. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASLayoutSize ASLayoutSizeMake(ASDimension width, ASDimension height) +{ + ASLayoutSize size; + size.width = width; + size.height = height; + return size; +} + +/** + * Resolve this relative size relative to a parent size. + */ +ASDISPLAYNODE_INLINE CGSize ASLayoutSizeResolveSize(ASLayoutSize layoutSize, CGSize parentSize, CGSize autoSize) +{ + return CGSizeMake(ASDimensionResolve(layoutSize.width, parentSize.width, autoSize.width), + ASDimensionResolve(layoutSize.height, parentSize.height, autoSize.height)); +} + +/* + * Returns a string representation of a relative size. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString *NSStringFromASLayoutSize(ASLayoutSize size) +{ + return [NSString stringWithFormat:@"{%@, %@}", + NSStringFromASDimension(size.width), + NSStringFromASDimension(size.height)]; +} + +#pragma mark - ASSizeRange + +/** + * Expresses an inclusive range of sizes. Used to provide a simple constraint to layout. + */ +typedef struct { + CGSize min; + CGSize max; +} ASSizeRange; + +/** + * A size range with all dimensions zero. + */ +AS_EXTERN ASSizeRange const ASSizeRangeZero; + +/** + * A size range from zero to infinity in both directions. + */ +AS_EXTERN ASSizeRange const ASSizeRangeUnconstrained; + +/** + * Returns whether a size range has > 0.1 max width and max height. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASSizeRangeHasSignificantArea(ASSizeRange sizeRange) +{ + static CGFloat const limit = 0.1f; + return (sizeRange.max.width > limit && sizeRange.max.height > limit); +} + +/** + * Creates an ASSizeRange with provided min and max size. + */ +ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) +{ + ASDisplayNodeCAssertPositiveReal(@"Range min width", min.width); + ASDisplayNodeCAssertPositiveReal(@"Range min height", min.height); + ASDisplayNodeCAssertInfOrPositiveReal(@"Range max width", max.width); + ASDisplayNodeCAssertInfOrPositiveReal(@"Range max height", max.height); + ASDisplayNodeCAssert(min.width <= max.width, + @"Range min width (%f) must not be larger than max width (%f).", min.width, max.width); + ASDisplayNodeCAssert(min.height <= max.height, + @"Range min height (%f) must not be larger than max height (%f).", min.height, max.height); + ASSizeRange sizeRange; + sizeRange.min = min; + sizeRange.max = max; + return sizeRange; +} + +/** + * Creates an ASSizeRange with provided size as both min and max. + */ +ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize exactSize) +{ + return ASSizeRangeMake(exactSize, exactSize); +} + +/** + * Clamps the provided CGSize between the [min, max] bounds of this ASSizeRange. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size) +{ + return CGSizeMake(MAX(sizeRange.min.width, MIN(sizeRange.max.width, size.width)), + MAX(sizeRange.min.height, MIN(sizeRange.max.height, size.height))); +} + +/** + * Intersects another size range. If the other size range does not overlap in either dimension, this size range + * "wins" by returning a single point within its own range that is closest to the non-overlapping range. + */ +AS_EXTERN AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange); + +/** + * Returns whether two size ranges are equal in min and max size. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASSizeRangeEqualToSizeRange(ASSizeRange lhs, ASSizeRange rhs) +{ + return CGSizeEqualToSize(lhs.min, rhs.min) && CGSizeEqualToSize(lhs.max, rhs.max); +} + +/** + * Returns a string representation of a size range + */ +AS_EXTERN AS_WARN_UNUSED_RESULT NSString *NSStringFromASSizeRange(ASSizeRange sizeRange); + +#if YOGA + +#pragma mark - ASEdgeInsets + +typedef struct { + ASDimension top; + ASDimension left; + ASDimension bottom; + ASDimension right; + ASDimension start; + ASDimension end; + ASDimension horizontal; + ASDimension vertical; + ASDimension all; +} ASEdgeInsets; + +AS_EXTERN ASEdgeInsets const ASEdgeInsetsZero; + +AS_EXTERN ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets); + +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASDimension.mm b/submodules/AsyncDisplayKit/Source/Layout/ASDimension.mm new file mode 100644 index 0000000000..d1a42462df --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASDimension.mm @@ -0,0 +1,125 @@ +// +// ASDimension.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +#pragma mark - ASDimension + +ASDimension const ASDimensionAuto = {ASDimensionUnitAuto, 0}; + +ASOVERLOADABLE ASDimension ASDimensionMake(NSString *dimension) +{ + if (dimension.length > 0) { + + // Handle points + if ([dimension hasSuffix:@"pt"]) { + return ASDimensionMake(ASDimensionUnitPoints, ASCGFloatFromString(dimension)); + } + + // Handle auto + if ([dimension isEqualToString:@"auto"]) { + return ASDimensionAuto; + } + + // Handle percent + if ([dimension hasSuffix:@"%"]) { + return ASDimensionMake(ASDimensionUnitFraction, (ASCGFloatFromString(dimension) / 100.0)); + } + } + + return ASDimensionAuto; +} + +NSString *NSStringFromASDimension(ASDimension dimension) +{ + switch (dimension.unit) { + case ASDimensionUnitPoints: + return [NSString stringWithFormat:@"%.0fpt", dimension.value]; + case ASDimensionUnitFraction: + return [NSString stringWithFormat:@"%.0f%%", dimension.value * 100.0]; + case ASDimensionUnitAuto: + return @"Auto"; + } +} + +#pragma mark - ASLayoutSize + +ASLayoutSize const ASLayoutSizeAuto = {ASDimensionAuto, ASDimensionAuto}; + +#pragma mark - ASSizeRange + +ASSizeRange const ASSizeRangeZero = {}; + +ASSizeRange const ASSizeRangeUnconstrained = { {0, 0}, { INFINITY, INFINITY }}; + +struct _Range { + CGFloat min; + CGFloat max; + + /** + Intersects another dimension range. If the other range does not overlap, this size range "wins" by returning a + single point within its own range that is closest to the non-overlapping range. + */ + _Range intersect(const _Range &other) const + { + CGFloat newMin = MAX(min, other.min); + CGFloat newMax = MIN(max, other.max); + if (newMin <= newMax) { + return {newMin, newMax}; + } else { + // No intersection. If we're before the other range, return our max; otherwise our min. + if (min < other.min) { + return {max, max}; + } else { + return {min, min}; + } + } + } +}; + +ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange) +{ + const auto w = _Range({sizeRange.min.width, sizeRange.max.width}).intersect({otherSizeRange.min.width, otherSizeRange.max.width}); + const auto h = _Range({sizeRange.min.height, sizeRange.max.height}).intersect({otherSizeRange.min.height, otherSizeRange.max.height}); + return {{w.min, h.min}, {w.max, h.max}}; +} + +NSString *NSStringFromASSizeRange(ASSizeRange sizeRange) +{ + // 17 field length copied from iOS 10.3 impl of NSStringFromCGSize. + if (CGSizeEqualToSize(sizeRange.min, sizeRange.max)) { + return [NSString stringWithFormat:@"{{%.*g, %.*g}}", + 17, sizeRange.min.width, + 17, sizeRange.min.height]; + } + return [NSString stringWithFormat:@"{{%.*g, %.*g}, {%.*g, %.*g}}", + 17, sizeRange.min.width, + 17, sizeRange.min.height, + 17, sizeRange.max.width, + 17, sizeRange.max.height]; +} + +#if YOGA +#pragma mark - Yoga - ASEdgeInsets +ASEdgeInsets const ASEdgeInsetsZero = {}; + +ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets) +{ + ASEdgeInsets asEdgeInsets = ASEdgeInsetsZero; + asEdgeInsets.top = ASDimensionMake(edgeInsets.top); + asEdgeInsets.left = ASDimensionMake(edgeInsets.left); + asEdgeInsets.bottom = ASDimensionMake(edgeInsets.bottom); + asEdgeInsets.right = ASDimensionMake(edgeInsets.right); + return asEdgeInsets; +} +#endif diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASDimensionInternal.h b/submodules/AsyncDisplayKit/Source/Layout/ASDimensionInternal.h new file mode 100644 index 0000000000..df8f05600c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASDimensionInternal.h @@ -0,0 +1,101 @@ +// +// ASDimensionInternal.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - ASLayoutElementSize + +/** + * A struct specifying a ASLayoutElement's size. Example: + * + * ASLayoutElementSize size = (ASLayoutElementSize){ + * .width = ASDimensionMakeWithFraction(0.25), + * .maxWidth = ASDimensionMakeWithPoints(200), + * .minHeight = ASDimensionMakeWithFraction(0.50) + * }; + * + * Description: + * + */ +typedef struct { + ASDimension width; + ASDimension height; + ASDimension minWidth; + ASDimension maxWidth; + ASDimension minHeight; + ASDimension maxHeight; +} ASLayoutElementSize; + +/** + * Returns an ASLayoutElementSize with default values. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASLayoutElementSize ASLayoutElementSizeMake() +{ + return (ASLayoutElementSize){ + .width = ASDimensionAuto, + .height = ASDimensionAuto, + .minWidth = ASDimensionAuto, + .maxWidth = ASDimensionAuto, + .minHeight = ASDimensionAuto, + .maxHeight = ASDimensionAuto + }; +} + +/** + * Returns an ASLayoutElementSize with the specified CGSize values as width and height. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASLayoutElementSize ASLayoutElementSizeMakeFromCGSize(CGSize size) +{ + ASLayoutElementSize s = ASLayoutElementSizeMake(); + s.width = ASDimensionMakeWithPoints(size.width); + s.height = ASDimensionMakeWithPoints(size.height); + return s; +} + +/** + * Returns whether two sizes are equal. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutElementSizeEqualToLayoutElementSize(ASLayoutElementSize lhs, ASLayoutElementSize rhs) +{ + return (ASDimensionEqualToDimension(lhs.width, rhs.width) + && ASDimensionEqualToDimension(lhs.height, rhs.height) + && ASDimensionEqualToDimension(lhs.minWidth, rhs.minWidth) + && ASDimensionEqualToDimension(lhs.maxWidth, rhs.maxWidth) + && ASDimensionEqualToDimension(lhs.minHeight, rhs.minHeight) + && ASDimensionEqualToDimension(lhs.maxHeight, rhs.maxHeight)); +} + +/** + * Returns a string formatted to contain the data from an ASLayoutElementSize. + */ +AS_EXTERN AS_WARN_UNUSED_RESULT NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size); + +/** + * Resolve the given size relative to a parent size and an auto size. + * From the given size uses width, height to resolve the exact size constraint, uses the minHeight and minWidth to + * resolve the min size constraint and the maxHeight and maxWidth to resolve the max size constraint. For every + * dimension with unit ASDimensionUnitAuto the given autoASSizeRange value will be used. + * Based on the calculated exact, min and max size constraints the final size range will be calculated. + */ +AS_EXTERN AS_WARN_UNUSED_RESULT ASSizeRange ASLayoutElementSizeResolveAutoSize(ASLayoutElementSize size, const CGSize parentSize, ASSizeRange autoASSizeRange); + +/** + * Resolve the given size to a parent size. Uses internally ASLayoutElementSizeResolveAutoSize with {INFINITY, INFINITY} as + * as autoASSizeRange. For more information look at ASLayoutElementSizeResolveAutoSize. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASLayoutElementSizeResolve(ASLayoutElementSize size, const CGSize parentSize) +{ + return ASLayoutElementSizeResolveAutoSize(size, parentSize, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); +} + + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASDimensionInternal.mm b/submodules/AsyncDisplayKit/Source/Layout/ASDimensionInternal.mm new file mode 100644 index 0000000000..8af3555252 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASDimensionInternal.mm @@ -0,0 +1,65 @@ +// +// ASDimensionInternal.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#pragma mark - ASLayoutElementSize + +NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size) +{ + return [NSString stringWithFormat: + @"", + NSStringFromASLayoutSize(ASLayoutSizeMake(size.width, size.height)), + NSStringFromASLayoutSize(ASLayoutSizeMake(size.minWidth, size.minHeight)), + NSStringFromASLayoutSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight))]; +} + +ASDISPLAYNODE_INLINE void ASLayoutElementSizeConstrain(CGFloat minVal, CGFloat exactVal, CGFloat maxVal, CGFloat *outMin, CGFloat *outMax) +{ + NSCAssert(!isnan(minVal), @"minVal must not be NaN"); + NSCAssert(!isnan(maxVal), @"maxVal must not be NaN"); + // Avoid use of min/max primitives since they're harder to reason + // about in the presence of NaN (in exactVal) + // Follow CSS: min overrides max overrides exact. + + // Begin with the min/max range + *outMin = minVal; + *outMax = maxVal; + if (maxVal <= minVal) { + // min overrides max and exactVal is irrelevant + *outMax = minVal; + return; + } + if (isnan(exactVal)) { + // no exact value, so leave as a min/max range + return; + } + if (exactVal > maxVal) { + // clip to max value + *outMin = maxVal; + } else if (exactVal < minVal) { + // clip to min value + *outMax = minVal; + } else { + // use exact value + *outMin = *outMax = exactVal; + } +} + +ASSizeRange ASLayoutElementSizeResolveAutoSize(ASLayoutElementSize size, const CGSize parentSize, ASSizeRange autoASSizeRange) +{ + CGSize resolvedExact = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.width, size.height), parentSize, {NAN, NAN}); + CGSize resolvedMin = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.minWidth, size.minHeight), parentSize, autoASSizeRange.min); + CGSize resolvedMax = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight), parentSize, autoASSizeRange.max); + + CGSize rangeMin, rangeMax; + ASLayoutElementSizeConstrain(resolvedMin.width, resolvedExact.width, resolvedMax.width, &rangeMin.width, &rangeMax.width); + ASLayoutElementSizeConstrain(resolvedMin.height, resolvedExact.height, resolvedMax.height, &rangeMin.height, &rangeMax.height); + return {rangeMin, rangeMax}; +} diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASInsetLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASInsetLayoutSpec.h new file mode 100644 index 0000000000..8e22fe9bd2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASInsetLayoutSpec.h @@ -0,0 +1,43 @@ +// +// ASInsetLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A layout spec that wraps another layoutElement child, applying insets around it. + + If the child has a size specified as a fraction, the fraction is resolved against this spec's parent + size **after** applying insets. + + @example ASOuterLayoutSpec contains an ASInsetLayoutSpec with an ASInnerLayoutSpec. Suppose that: + - ASOuterLayoutSpec is 200pt wide. + - ASInnerLayoutSpec specifies its width as 100%. + - The ASInsetLayoutSpec has insets of 10pt on every side. + ASInnerLayoutSpec will have size 180pt, not 200pt, because it receives a parent size that has been adjusted for insets. + + If you're familiar with CSS: ASInsetLayoutSpec's child behaves similarly to "box-sizing: border-box". + + An infinite inset is resolved as an inset equal to all remaining space after applying the other insets and child size. + @example An ASInsetLayoutSpec with an infinite left inset and 10px for all other edges will position it's child 10px from the right edge. + */ +@interface ASInsetLayoutSpec : ASLayoutSpec + +@property (nonatomic) UIEdgeInsets insets; + +/** + @param insets The amount of space to inset on each side. + @param child The wrapped child to inset. + */ ++ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASInsetLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASInsetLayoutSpec.mm new file mode 100644 index 0000000000..450480f1c4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASInsetLayoutSpec.mm @@ -0,0 +1,123 @@ +// +// ASInsetLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import + +@interface ASInsetLayoutSpec () +{ + UIEdgeInsets _insets; +} +@end + +/* Returns f if f is finite, substitute otherwise */ +static CGFloat finite(CGFloat f, CGFloat substitute) +{ + return isinf(f) ? substitute : f; +} + +/* Returns f if f is finite, 0 otherwise */ +static CGFloat finiteOrZero(CGFloat f) +{ + return finite(f, 0); +} + +/* Returns the inset required to center 'inner' in 'outer' */ +static CGFloat centerInset(CGFloat outer, CGFloat inner) +{ + return ASRoundPixelValue((outer - inner) / 2); +} + +@implementation ASInsetLayoutSpec + +- (instancetype)initWithInsets:(UIEdgeInsets)insets child:(id)child; +{ + if (!(self = [super init])) { + return nil; + } + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + _insets = insets; + [self setChild:child]; + return self; +} + ++ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child NS_RETURNS_RETAINED +{ + return [[self alloc] initWithInsets:insets child:child]; +} + +- (void)setInsets:(UIEdgeInsets)insets +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _insets = insets; +} + +/** + Inset will compute a new constrained size for it's child after applying insets and re-positioning + the child to respect the inset. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ + if (self.child == nil) { + ASDisplayNodeAssert(NO, @"Inset spec measured without a child. The spec will do nothing."); + return [ASLayout layoutWithLayoutElement:self size:CGSizeZero]; + } + + const CGFloat insetsX = (finiteOrZero(_insets.left) + finiteOrZero(_insets.right)); + const CGFloat insetsY = (finiteOrZero(_insets.top) + finiteOrZero(_insets.bottom)); + + // if either x-axis inset is infinite, let child be intrinsic width + const CGFloat minWidth = (isinf(_insets.left) || isinf(_insets.right)) ? 0 : constrainedSize.min.width; + // if either y-axis inset is infinite, let child be intrinsic height + const CGFloat minHeight = (isinf(_insets.top) || isinf(_insets.bottom)) ? 0 : constrainedSize.min.height; + + const ASSizeRange insetConstrainedSize = { + { + MAX(0, minWidth - insetsX), + MAX(0, minHeight - insetsY), + }, + { + MAX(0, constrainedSize.max.width - insetsX), + MAX(0, constrainedSize.max.height - insetsY), + } + }; + + const CGSize insetParentSize = { + MAX(0, parentSize.width - insetsX), + MAX(0, parentSize.height - insetsY) + }; + + ASLayout *sublayout = [self.child layoutThatFits:insetConstrainedSize parentSize:insetParentSize]; + + const CGSize computedSize = ASSizeRangeClamp(constrainedSize, { + finite(sublayout.size.width + _insets.left + _insets.right, constrainedSize.max.width), + finite(sublayout.size.height + _insets.top + _insets.bottom, constrainedSize.max.height), + }); + + const CGFloat x = finite(_insets.left, constrainedSize.max.width - + (finite(_insets.right, + centerInset(constrainedSize.max.width, sublayout.size.width)) + sublayout.size.width)); + + const CGFloat y = finite(_insets.top, + constrainedSize.max.height - + (finite(_insets.bottom, + centerInset(constrainedSize.max.height, sublayout.size.height)) + sublayout.size.height)); + + sublayout.position = CGPointMake(x, y); + + return [ASLayout layoutWithLayoutElement:self size:computedSize sublayouts:@[sublayout]]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayout+IGListKit.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayout+IGListKit.h new file mode 100644 index 0000000000..6f6753da2c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayout+IGListKit.h @@ -0,0 +1,15 @@ +// +// ASLayout+IGListKit.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#if AS_IG_LIST_KIT +#import +#import +@interface ASLayout(IGListKit) +@end + +#endif // AS_IG_LIST_KIT diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayout+IGListKit.mm b/submodules/AsyncDisplayKit/Source/Layout/ASLayout+IGListKit.mm new file mode 100644 index 0000000000..f9cc139e9c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayout+IGListKit.mm @@ -0,0 +1,30 @@ +// +// ASLayout+IGListKit.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#import +#if AS_IG_LIST_KIT +#import "ASLayout+IGListKit.h" + +@interface ASLayout() { +@public + id _layoutElement; +} +@end + +@implementation ASLayout(IGListKit) + +- (id )diffIdentifier +{ + return self->_layoutElement; +} + +- (BOOL)isEqualToDiffableObject:(id )other +{ + return [self isEqual:other]; +} +@end +#endif // AS_IG_LIST_KIT diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayout.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayout.h new file mode 100644 index 0000000000..0e40574d07 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayout.h @@ -0,0 +1,157 @@ +// +// ASLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN CGPoint const ASPointNull; // {NAN, NAN} + +AS_EXTERN BOOL ASPointIsNull(CGPoint point); + +AS_EXTERN NSString *const ASThreadDictMaxConstraintSizeKey; + +/** + * Safely calculates the layout of the given root layoutElement by guarding against nil nodes. + * @param rootLayoutElement The root node to calculate the layout for. + * @param sizeRange The size range to calculate the root layout within. + */ +AS_EXTERN ASLayout *ASCalculateRootLayout(id rootLayoutElement, const ASSizeRange sizeRange); + +/** + * Safely computes the layout of the given node by guarding against nil nodes. + * @param layoutElement The layout element to calculate the layout for. + * @param sizeRange The size range to calculate the node layout within. + * @param parentSize The parent size of the node to calculate the layout for. + */ +AS_EXTERN ASLayout *ASCalculateLayout(idlayoutElement, const ASSizeRange sizeRange, const CGSize parentSize); + +/** + * A node in the layout tree that represents the size and position of the object that created it (ASLayoutElement). + */ +@interface ASLayout : NSObject + +/** + * The underlying object described by this layout + */ +@property (nonatomic, weak, readonly) id layoutElement; + +/** + * The type of ASLayoutElement that created this layout + */ +@property (nonatomic, readonly) ASLayoutElementType type; + +/** + * Size of the current layout + */ +@property (nonatomic, readonly) CGSize size; + +/** + * Position in parent. Default to ASPointNull. + * + * @discussion When being used as a sublayout, this property must not equal ASPointNull. + */ +@property (nonatomic, readonly) CGPoint position; + +/** + * Array of ASLayouts. Each must have a valid non-null position. + */ +@property (nonatomic, copy, readonly) NSArray *sublayouts; + +/** + * The frame for the given element, or CGRectNull if + * the element is not a direct descendent of this layout. + */ +- (CGRect)frameForElement:(id)layoutElement; + +/** + * @abstract Returns a valid frame for the current layout computed with the size and position. + * @discussion Clamps the layout's origin or position to 0 if any of the calculated values are infinite. + */ +@property (nonatomic, readonly) CGRect frame; + +/** + * Designated initializer + */ +- (instancetype)initWithLayoutElement:(id)layoutElement + size:(CGSize)size + position:(CGPoint)position + sublayouts:(nullable NSArray *)sublayouts NS_DESIGNATED_INITIALIZER; + +/** + * Convenience class initializer for layout construction. + * + * @param layoutElement The backing ASLayoutElement object. + * @param size The size of this layout. + * @param position The position of this layout within its parent (if available). + * @param sublayouts Sublayouts belong to the new layout. + */ ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size + position:(CGPoint)position + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * Convenience initializer that has CGPointNull position. + * Best used by ASDisplayNode subclasses that are manually creating a layout for -calculateLayoutThatFits:, + * or for ASLayoutSpec subclasses that are referencing the "self" level in the layout tree, + * or for creating a sublayout of which the position is yet to be determined. + * + * @param layoutElement The backing ASLayoutElement object. + * @param size The size of this layout. + * @param sublayouts Sublayouts belong to the new layout. + */ ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * Convenience that has CGPointNull position and no sublayouts. + * Best used for creating a layout that has no sublayouts, and is either a root one + * or a sublayout of which the position is yet to be determined. + * + * @param layoutElement The backing ASLayoutElement object. + * @param size The size of this layout. + */ ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * Traverses the existing layout tree and generates a new tree that represents only ASDisplayNode layouts + */ +- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)new NS_UNAVAILABLE; + +@end + +#pragma mark - Debugging + +@interface ASLayout (Debugging) + +/** + * Set to YES to tell all ASLayout instances to retain their sublayout elements. Defaults to NO. + * See `-retainSublayoutLayoutElements` to control this per-instance. + * + * Note: Weaver relies on this API. + */ +@property (class) BOOL shouldRetainSublayoutLayoutElements; + +/** + * Recrusively output the description of the layout tree. + */ +- (NSString *)recursiveDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayout.mm b/submodules/AsyncDisplayKit/Source/Layout/ASLayout.mm new file mode 100644 index 0000000000..055daa88f5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayout.mm @@ -0,0 +1,378 @@ +// +// ASLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import + +#import +#import +#import + +NSString *const ASThreadDictMaxConstraintSizeKey = @"kASThreadDictMaxConstraintSizeKey"; + +CGPoint const ASPointNull = {NAN, NAN}; + +BOOL ASPointIsNull(CGPoint point) +{ + return isnan(point.x) && isnan(point.y); +} + +/** + * Creates an defined number of " |" indent blocks for the recursive description. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents) +{ + NSMutableString *description = [NSMutableString string]; + for (NSUInteger i = 0; i < indents; i++) { + [description appendString:@" |"]; + } + if (indents > 0) { + [description appendString:@" "]; + } + return description; +} + +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout) +{ + return layout.type == ASLayoutElementTypeDisplayNode; +} + +@interface ASLayout () +{ + ASLayoutElementType _layoutElementType; + std::atomic_bool _retainSublayoutElements; +} +@end + +@implementation ASLayout + +@dynamic frame, type; + +static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(NO); + ++ (void)setShouldRetainSublayoutLayoutElements:(BOOL)shouldRetain +{ + static_retainsSublayoutLayoutElements.store(shouldRetain); +} + ++ (BOOL)shouldRetainSublayoutLayoutElements +{ + return static_retainsSublayoutLayoutElements.load(); +} + +- (instancetype)initWithLayoutElement:(id)layoutElement + size:(CGSize)size + position:(CGPoint)position + sublayouts:(nullable NSArray *)sublayouts +{ + NSParameterAssert(layoutElement); + + self = [super init]; + if (self) { +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + for (ASLayout *sublayout in sublayouts) { + ASDisplayNodeAssert(ASPointIsNull(sublayout.position) == NO, @"Invalid position is not allowed in sublayout."); + } +#endif + + _layoutElement = layoutElement; + + // Read this now to avoid @c weak overhead later. + _layoutElementType = layoutElement.layoutElementType; + + if (!ASIsCGSizeValidForSize(size)) { + ASDisplayNodeFailAssert(@"layoutSize is invalid and unsafe to provide to Core Animation! Release configurations will force to 0, 0. Size = %@, node = %@", NSStringFromCGSize(size), layoutElement); + size = CGSizeZero; + } else { + size = CGSizeMake(ASCeilPixelValue(size.width), ASCeilPixelValue(size.height)); + } + _size = size; + + if (ASPointIsNull(position) == NO) { + _position = ASCeilPointValues(position); + } else { + _position = position; + } + + _sublayouts = [sublayouts copy] ?: @[]; + + if ([ASLayout shouldRetainSublayoutLayoutElements]) { + [self retainSublayoutElements]; + } + } + + return self; +} + +#pragma mark - Class Constructors + ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size + position:(CGPoint)position + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED +{ + return [[self alloc] initWithLayoutElement:layoutElement + size:size + position:position + sublayouts:sublayouts]; +} + ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED +{ + return [self layoutWithLayoutElement:layoutElement + size:size + position:ASPointNull + sublayouts:sublayouts]; +} + ++ (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size NS_RETURNS_RETAINED +{ + return [self layoutWithLayoutElement:layoutElement + size:size + position:ASPointNull + sublayouts:nil]; +} + +- (void)dealloc +{ + if (_retainSublayoutElements.load()) { + for (ASLayout *sublayout in _sublayouts) { + // We retained this, so there's no risk of it deallocating on us. + if (CFTypeRef cfElement = (__bridge CFTypeRef)sublayout->_layoutElement) { + CFRelease(cfElement); + } + } + } +} + +#pragma mark - Sublayout Elements Caching + +- (void)retainSublayoutElements +{ + if (_retainSublayoutElements.exchange(true)) { + return; + } + + for (ASLayout *sublayout in _sublayouts) { + // CFBridgingRetain atomically casts and retains. We need the atomicity. + CFBridgingRetain(sublayout->_layoutElement); + } +} + +#pragma mark - Layout Flattening + +- (BOOL)isFlattened +{ + // A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts. + if (!ASPointIsNull(_position)) { + return NO; + } + + for (ASLayout *sublayout in _sublayouts) { + if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout->_sublayouts.count > 0) { + return NO; + } + } + + return YES; +} + +- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED +{ + if ([self isFlattened]) { + // All flattened layouts must retain sublayout elements until they are applied. + [self retainSublayoutElements]; + return self; + } + + struct Context { + unowned ASLayout *layout; + CGPoint absolutePosition; + }; + + // Queue used to keep track of sublayouts while traversing this layout in a DFS fashion. + std::deque queue; + for (ASLayout *sublayout in _sublayouts) { + queue.push_back({sublayout, sublayout.position}); + } + + std::vector flattenedSublayouts; + + while (!queue.empty()) { + const Context context = std::move(queue.front()); + queue.pop_front(); + + unowned ASLayout *layout = context.layout; + // Direct ivar access to avoid retain/release, use existing +1. + const NSUInteger sublayoutsCount = layout->_sublayouts.count; + const CGPoint absolutePosition = context.absolutePosition; + + if (ASLayoutIsDisplayNodeType(layout)) { + if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) { + // Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position. + const auto newLayout = [ASLayout layoutWithLayoutElement:layout->_layoutElement + size:layout.size + position:absolutePosition + sublayouts:@[]]; + flattenedSublayouts.push_back(newLayout); + } else { + flattenedSublayouts.push_back(layout); + } + } else if (sublayoutsCount > 0) { + // Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue. + unowned ASLayout *rawSublayouts[sublayoutsCount]; + [layout->_sublayouts getObjects:rawSublayouts range:NSMakeRange(0, sublayoutsCount)]; + for (NSInteger i = sublayoutsCount - 1; i >= 0; i--) { + queue.push_front({rawSublayouts[i], absolutePosition + rawSublayouts[i].position}); + } + } + } + + NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()]; + // flattenedSublayouts is now all nils. + + ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array]; + // All flattened layouts must retain sublayout elements until they are applied. + [layout retainSublayoutElements]; + return layout; +} + +#pragma mark - Equality Checking + +- (BOOL)isEqual:(id)object +{ + if (self == object) return YES; + + ASLayout *layout = ASDynamicCast(object, ASLayout); + if (layout == nil) { + return NO; + } + + if (!CGSizeEqualToSize(_size, layout.size)) return NO; + + if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position)) + || CGPointEqualToPoint(self.position, layout.position))) return NO; + if (_layoutElement != layout.layoutElement) return NO; + + if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) { + return NO; + } + + return YES; +} + +#pragma mark - Accessors + +- (ASLayoutElementType)type +{ + return _layoutElementType; +} + +- (CGRect)frameForElement:(id)layoutElement +{ + for (ASLayout *l in _sublayouts) { + if (l->_layoutElement == layoutElement) { + return l.frame; + } + } + return CGRectNull; +} + +- (CGRect)frame +{ + CGRect subnodeFrame = CGRectZero; + CGPoint adjustedOrigin = _position; + if (isfinite(adjustedOrigin.x) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid position"); + adjustedOrigin.x = 0; + } + if (isfinite(adjustedOrigin.y) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid position"); + adjustedOrigin.y = 0; + } + subnodeFrame.origin = adjustedOrigin; + + CGSize adjustedSize = _size; + if (isfinite(adjustedSize.width) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid size"); + adjustedSize.width = 0; + } + if (isfinite(adjustedSize.height) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid position"); + adjustedSize.height = 0; + } + subnodeFrame.size = adjustedSize; + + return subnodeFrame; +} + +#pragma mark - Description + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"size" : [NSValue valueWithCGSize:self.size] }]; + + if (id layoutElement = self.layoutElement) { + [result addObject:@{ @"layoutElement" : layoutElement }]; + } + + const auto pos = self.position; + if (!ASPointIsNull(pos)) { + [result addObject:@{ @"position" : [NSValue valueWithCGPoint:pos] }]; + } + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)recursiveDescription +{ + return [self _recursiveDescriptionForLayout:self level:0]; +} + +- (NSString *)_recursiveDescriptionForLayout:(ASLayout *)layout level:(NSUInteger)level +{ + NSMutableString *description = [NSMutableString string]; + [description appendString:descriptionIndents(level)]; + [description appendString:[layout description]]; + for (ASLayout *sublayout in layout.sublayouts) { + [description appendString:@"\n"]; + [description appendString:[self _recursiveDescriptionForLayout:sublayout level:level + 1]]; + } + return description; +} + +@end + +ASLayout *ASCalculateLayout(id layoutElement, const ASSizeRange sizeRange, const CGSize parentSize) +{ + NSCParameterAssert(layoutElement != nil); + + return [layoutElement layoutThatFits:sizeRange parentSize:parentSize]; +} + +ASLayout *ASCalculateRootLayout(id rootLayoutElement, const ASSizeRange sizeRange) +{ + ASLayout *layout = ASCalculateLayout(rootLayoutElement, sizeRange, sizeRange.max); + // Here could specific verfication happen + return layout; +} diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.h new file mode 100644 index 0000000000..c4ba689cfc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.h @@ -0,0 +1,306 @@ +// +// ASLayoutElement.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import + +@class ASLayout; +@class ASLayoutSpec; +@protocol ASLayoutElementStylability; + +@protocol ASTraitEnvironment; + +NS_ASSUME_NONNULL_BEGIN + +/** A constant that indicates that the parent's size is not yet determined in a given dimension. */ +AS_EXTERN CGFloat const ASLayoutElementParentDimensionUndefined; + +/** A constant that indicates that the parent's size is not yet determined in either dimension. */ +AS_EXTERN CGSize const ASLayoutElementParentSizeUndefined; + +/** Type of ASLayoutElement */ +typedef NS_ENUM(NSUInteger, ASLayoutElementType) { + ASLayoutElementTypeLayoutSpec, + ASLayoutElementTypeDisplayNode +}; + +#pragma mark - ASLayoutElement + +/** + * The ASLayoutElement protocol declares a method for measuring the layout of an object. A layout + * is defined by an ASLayout return value, and must specify 1) the size (but not position) of the + * layoutElement object, and 2) the size and position of all of its immediate child objects. The tree + * recursion is driven by parents requesting layouts from their children in order to determine their + * size, followed by the parents setting the position of the children once the size is known + * + * The protocol also implements a "family" of LayoutElement protocols. These protocols contain layout + * options that can be used for specific layout specs. For example, ASStackLayoutSpec has options + * defining how a layoutElement should shrink or grow based upon available space. + * + * These layout options are all stored in an ASLayoutOptions class (that is defined in ASLayoutElementPrivate). + * Generally you needn't worry about the layout options class, as the layoutElement protocols allow all direct + * access to the options via convenience properties. If you are creating custom layout spec, then you can + * extend the backing layout options class to accommodate any new layout options. + */ +@protocol ASLayoutElement + +#pragma mark - Getter + +/** + * @abstract Returns type of layoutElement + */ +@property (nonatomic, readonly) ASLayoutElementType layoutElementType; + +/** + * @abstract A size constraint that should apply to this ASLayoutElement. + */ +@property (nonatomic, readonly) ASLayoutElementStyle *style; + +/** + * @abstract Returns all children of an object which class conforms to the ASLayoutElement protocol + */ +- (nullable NSArray> *)sublayoutElements; + +#pragma mark - Calculate layout + +/** + * @abstract Asks the node to return a layout based on given size range. + * + * @param constrainedSize The minimum and maximum sizes the receiver should fit in. + * + * @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used). + * + * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the + * constraint and the result. + * + * @warning Subclasses must not override this; it caches results from -calculateLayoutThatFits:. Calling this method may + * be expensive if result is not cached. + * + * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] + */ +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize; + +/** + * Call this on children layoutElements to compute their layouts within your implementation of -calculateLayoutThatFits:. + * + * @warning You may not override this method. Override -calculateLayoutThatFits: instead. + * @warning In almost all cases, prefer the use of ASCalculateLayout in ASLayout + * + * @param constrainedSize Specifies a minimum and maximum size. The receiver must choose a size that is in this range. + * @param parentSize The parent node's size. If the parent component does not have a final size in a given dimension, + * then it should be passed as ASLayoutElementParentDimensionUndefined (for example, if the parent's width + * depends on the child's size). + * + * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the + * constraint and the result. + * + * @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used). + */ +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize; + +/** + * Override this method to compute your layoutElement's layout. + * + * @discussion Why do you need to override -calculateLayoutThatFits: instead of -layoutThatFits:parentSize:? + * The base implementation of -layoutThatFits:parentSize: does the following for you: + * 1. First, it uses the parentSize parameter to resolve the nodes's size (the one assigned to the size property). + * 2. Then, it intersects the resolved size with the constrainedSize parameter. If the two don't intersect, + * constrainedSize wins. This allows a component to always override its childrens' sizes when computing its layout. + * (The analogy for UIView: you might return a certain size from -sizeThatFits:, but a parent view can always override + * that size and set your frame to any size.) + * 3. It caches it result for reuse + * + * @param constrainedSize A min and max size. This is computed as described in the description. The ASLayout you + * return MUST have a size between these two sizes. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize; + +/** + * In certain advanced cases, you may want to override this method. Overriding this method allows you to receive the + * layoutElement's size, parentSize, and constrained size. With these values you could calculate the final constrained size + * and call -calculateLayoutThatFits: with the result. + * + * @warning Overriding this method should be done VERY rarely. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize; + +- (BOOL)implementsLayoutMethod; + +@end + +#pragma mark - ASLayoutElementStyle + +AS_EXTERN NSString * const ASLayoutElementStyleWidthProperty; +AS_EXTERN NSString * const ASLayoutElementStyleMinWidthProperty; +AS_EXTERN NSString * const ASLayoutElementStyleMaxWidthProperty; + +AS_EXTERN NSString * const ASLayoutElementStyleHeightProperty; +AS_EXTERN NSString * const ASLayoutElementStyleMinHeightProperty; +AS_EXTERN NSString * const ASLayoutElementStyleMaxHeightProperty; + +AS_EXTERN NSString * const ASLayoutElementStyleSpacingBeforeProperty; +AS_EXTERN NSString * const ASLayoutElementStyleSpacingAfterProperty; +AS_EXTERN NSString * const ASLayoutElementStyleFlexGrowProperty; +AS_EXTERN NSString * const ASLayoutElementStyleFlexShrinkProperty; +AS_EXTERN NSString * const ASLayoutElementStyleFlexBasisProperty; +AS_EXTERN NSString * const ASLayoutElementStyleAlignSelfProperty; +AS_EXTERN NSString * const ASLayoutElementStyleAscenderProperty; +AS_EXTERN NSString * const ASLayoutElementStyleDescenderProperty; + +AS_EXTERN NSString * const ASLayoutElementStyleLayoutPositionProperty; + +@protocol ASLayoutElementStyleDelegate +- (void)style:(__kindof ASLayoutElementStyle *)style propertyDidChange:(NSString *)propertyName; +@end + +@interface ASLayoutElementStyle : NSObject + +/** + * @abstract Initializes the layoutElement style with a specified delegate + */ +- (instancetype)initWithDelegate:(id)delegate; + +/** + * @abstract The object that acts as the delegate of the style. + * + * @discussion The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained. + */ +@property (nullable, nonatomic, weak, readonly) id delegate; + + +#pragma mark - Sizing + +/** + * @abstract The width property specifies the width of the content area of an ASLayoutElement. + * The minWidth and maxWidth properties override width. + * Defaults to ASDimensionAuto + */ +@property (nonatomic) ASDimension width; + +/** + * @abstract The height property specifies the height of the content area of an ASLayoutElement + * The minHeight and maxHeight properties override height. + * Defaults to ASDimensionAuto + */ +@property (nonatomic) ASDimension height; + +/** + * @abstract The minHeight property is used to set the minimum height of a given element. It prevents the used value + * of the height property from becoming smaller than the value specified for minHeight. + * The value of minHeight overrides both maxHeight and height. + * Defaults to ASDimensionAuto + */ +@property (nonatomic) ASDimension minHeight; + +/** + * @abstract The maxHeight property is used to set the maximum height of an element. It prevents the used value of the + * height property from becoming larger than the value specified for maxHeight. + * The value of maxHeight overrides height, but minHeight overrides maxHeight. + * Defaults to ASDimensionAuto + */ +@property (nonatomic) ASDimension maxHeight; + +/** + * @abstract The minWidth property is used to set the minimum width of a given element. It prevents the used value of + * the width property from becoming smaller than the value specified for minWidth. + * The value of minWidth overrides both maxWidth and width. + * Defaults to ASDimensionAuto + */ +@property (nonatomic) ASDimension minWidth; + +/** + * @abstract The maxWidth property is used to set the maximum width of a given element. It prevents the used value of + * the width property from becoming larger than the value specified for maxWidth. + * The value of maxWidth overrides width, but minWidth overrides maxWidth. + * Defaults to ASDimensionAuto + */ +@property (nonatomic) ASDimension maxWidth; + +#pragma mark - ASLayoutElementStyleSizeHelpers + +/** + * @abstract Provides a suggested size for a layout element. If the optional minSize or maxSize are provided, + * and the preferredSize exceeds these, the minSize or maxSize will be enforced. If this optional value is not + * provided, the layout element’s size will default to it’s intrinsic content size provided calculateSizeThatFits: + * + * @discussion This method is optional, but one of either preferredSize or preferredLayoutSize is required + * for nodes that either have no intrinsic content size or + * should be laid out at a different size than its intrinsic content size. For example, this property could be + * set on an ASImageNode to display at a size different from the underlying image size. + * + * @warning Calling the getter when the size's width or height are relative will cause an assert. + */ +@property (nonatomic) CGSize preferredSize; + + /** + * @abstract An optional property that provides a minimum size bound for a layout element. If provided, this restriction will + * always be enforced. If a parent layout element’s minimum size is smaller than its child’s minimum size, the child’s + * minimum size will be enforced and its size will extend out of the layout spec’s. + * + * @discussion For example, if you set a preferred relative width of 50% and a minimum width of 200 points on an + * element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, + * since 160 pts is lower than the minimum width of 200 pts, the minimum width would be used. + */ +@property (nonatomic) CGSize minSize; +- (CGSize)minSize UNAVAILABLE_ATTRIBUTE; + +/** + * @abstract An optional property that provides a maximum size bound for a layout element. If provided, this restriction will + * always be enforced. If a child layout element’s maximum size is smaller than its parent, the child’s maximum size will + * be enforced and its size will extend out of the layout spec’s. + * + * @discussion For example, if you set a preferred relative width of 50% and a maximum width of 120 points on an + * element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, + * since 160 pts is higher than the maximum width of 120 pts, the maximum width would be used. + */ +@property (nonatomic) CGSize maxSize; +- (CGSize)maxSize UNAVAILABLE_ATTRIBUTE; + +/** + * @abstract Provides a suggested RELATIVE size for a layout element. An ASLayoutSize uses percentages rather + * than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minLayoutSize or + * maxLayoutSize are provided, and the preferredLayoutSize exceeds these, the minLayoutSize or maxLayoutSize + * will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size + * provided calculateSizeThatFits: + */ +@property (nonatomic) ASLayoutSize preferredLayoutSize; + +/** + * @abstract An optional property that provides a minimum RELATIVE size bound for a layout element. If provided, this + * restriction will always be enforced. If a parent layout element’s minimum relative size is smaller than its child’s minimum + * relative size, the child’s minimum relative size will be enforced and its size will extend out of the layout spec’s. + */ +@property (nonatomic) ASLayoutSize minLayoutSize; + +/** + * @abstract An optional property that provides a maximum RELATIVE size bound for a layout element. If provided, this + * restriction will always be enforced. If a parent layout element’s maximum relative size is smaller than its child’s maximum + * relative size, the child’s maximum relative size will be enforced and its size will extend out of the layout spec’s. + */ +@property (nonatomic) ASLayoutSize maxLayoutSize; + +@end + +#pragma mark - ASLayoutElementStylability + +@protocol ASLayoutElementStylability + +- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.mm b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.mm new file mode 100644 index 0000000000..887f609e99 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.mm @@ -0,0 +1,843 @@ +// +// ASLayoutElement.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import + +#import +#include + +using AS::MutexLocker; + +#if YOGA + #import YOGA_HEADER_PATH + #import +#endif + +#pragma mark - ASLayoutElementContext + +@implementation ASLayoutElementContext + +- (instancetype)init +{ + if (self = [super init]) { + _transitionID = ASLayoutElementContextDefaultTransitionID; + } + return self; +} + +@end + +CGFloat const ASLayoutElementParentDimensionUndefined = NAN; +CGSize const ASLayoutElementParentSizeUndefined = {ASLayoutElementParentDimensionUndefined, ASLayoutElementParentDimensionUndefined}; + +int32_t const ASLayoutElementContextInvalidTransitionID = 0; +int32_t const ASLayoutElementContextDefaultTransitionID = ASLayoutElementContextInvalidTransitionID + 1; + +#if AS_TLS_AVAILABLE + +static _Thread_local __unsafe_unretained ASLayoutElementContext *tls_context; + +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(tls_context, @"Nested ASLayoutElementContexts aren't supported."); + + tls_context = (__bridge ASLayoutElementContext *)(__bridge_retained CFTypeRef)context; +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + return tls_context; +} + +void ASLayoutElementPopContext() +{ + ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!"); + CFRelease((__bridge CFTypeRef)tls_context); + tls_context = nil; +} + +#else + +static pthread_key_t ASLayoutElementContextKey() { + static pthread_key_t k; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_key_create(&k, NULL); + }); + return k; +} +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(pthread_getspecific(ASLayoutElementContextKey()), @"Nested ASLayoutElementContexts aren't supported."); + + const auto cfCtx = (__bridge_retained CFTypeRef)context; + pthread_setspecific(ASLayoutElementContextKey(), cfCtx); +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + const auto ctxPtr = pthread_getspecific(ASLayoutElementContextKey()); + return (__bridge ASLayoutElementContext *)ctxPtr; +} + +void ASLayoutElementPopContext() +{ + const auto ctx = (CFTypeRef)pthread_getspecific(ASLayoutElementContextKey()); + ASDisplayNodeCAssertNotNil(ctx, @"Attempt to pop context when there wasn't a context!"); + CFRelease(ctx); + pthread_setspecific(ASLayoutElementContextKey(), NULL); +} + +#endif // AS_TLS_AVAILABLE + +#pragma mark - ASLayoutElementStyle + +NSString * const ASLayoutElementStyleWidthProperty = @"ASLayoutElementStyleWidthProperty"; +NSString * const ASLayoutElementStyleMinWidthProperty = @"ASLayoutElementStyleMinWidthProperty"; +NSString * const ASLayoutElementStyleMaxWidthProperty = @"ASLayoutElementStyleMaxWidthProperty"; + +NSString * const ASLayoutElementStyleHeightProperty = @"ASLayoutElementStyleHeightProperty"; +NSString * const ASLayoutElementStyleMinHeightProperty = @"ASLayoutElementStyleMinHeightProperty"; +NSString * const ASLayoutElementStyleMaxHeightProperty = @"ASLayoutElementStyleMaxHeightProperty"; + +NSString * const ASLayoutElementStyleSpacingBeforeProperty = @"ASLayoutElementStyleSpacingBeforeProperty"; +NSString * const ASLayoutElementStyleSpacingAfterProperty = @"ASLayoutElementStyleSpacingAfterProperty"; +NSString * const ASLayoutElementStyleFlexGrowProperty = @"ASLayoutElementStyleFlexGrowProperty"; +NSString * const ASLayoutElementStyleFlexShrinkProperty = @"ASLayoutElementStyleFlexShrinkProperty"; +NSString * const ASLayoutElementStyleFlexBasisProperty = @"ASLayoutElementStyleFlexBasisProperty"; +NSString * const ASLayoutElementStyleAlignSelfProperty = @"ASLayoutElementStyleAlignSelfProperty"; +NSString * const ASLayoutElementStyleAscenderProperty = @"ASLayoutElementStyleAscenderProperty"; +NSString * const ASLayoutElementStyleDescenderProperty = @"ASLayoutElementStyleDescenderProperty"; + +NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementStyleLayoutPositionProperty"; + +#if YOGA +NSString * const ASYogaFlexWrapProperty = @"ASLayoutElementStyleLayoutFlexWrapProperty"; +NSString * const ASYogaFlexDirectionProperty = @"ASYogaFlexDirectionProperty"; +NSString * const ASYogaDirectionProperty = @"ASYogaDirectionProperty"; +NSString * const ASYogaSpacingProperty = @"ASYogaSpacingProperty"; +NSString * const ASYogaJustifyContentProperty = @"ASYogaJustifyContentProperty"; +NSString * const ASYogaAlignItemsProperty = @"ASYogaAlignItemsProperty"; +NSString * const ASYogaPositionTypeProperty = @"ASYogaPositionTypeProperty"; +NSString * const ASYogaPositionProperty = @"ASYogaPositionProperty"; +NSString * const ASYogaMarginProperty = @"ASYogaMarginProperty"; +NSString * const ASYogaPaddingProperty = @"ASYogaPaddingProperty"; +NSString * const ASYogaBorderProperty = @"ASYogaBorderProperty"; +NSString * const ASYogaAspectRatioProperty = @"ASYogaAspectRatioProperty"; +#endif + +#define ASLayoutElementStyleSetSizeWithScope(x) \ + __instanceLock__.lock(); \ + ASLayoutElementSize newSize = _size.load(); \ + { x } \ + _size.store(newSize); \ + __instanceLock__.unlock(); + +#define ASLayoutElementStyleCallDelegate(propertyName)\ +do {\ + [self propertyDidChange:propertyName];\ + [_delegate style:self propertyDidChange:propertyName];\ +} while(0) + +@implementation ASLayoutElementStyle { + AS::RecursiveMutex __instanceLock__; + ASLayoutElementStyleExtensions _extensions; + + std::atomic _size; + std::atomic _spacingBefore; + std::atomic _spacingAfter; + std::atomic _flexGrow; + std::atomic _flexShrink; + std::atomic _flexBasis; + std::atomic _alignSelf; + std::atomic _ascender; + std::atomic _descender; + std::atomic _layoutPosition; + +#if YOGA + YGNodeRef _yogaNode; + std::atomic _flexWrap; + std::atomic _flexDirection; + std::atomic _direction; + std::atomic _justifyContent; + std::atomic _alignItems; + std::atomic _positionType; + std::atomic _position; + std::atomic _margin; + std::atomic _padding; + std::atomic _border; + std::atomic _aspectRatio; + ASStackLayoutAlignItems _parentAlignStyle; +#endif +} + +@dynamic width, height, minWidth, maxWidth, minHeight, maxHeight; +@dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize; + +#pragma mark - Lifecycle + +- (instancetype)initWithDelegate:(id)delegate +{ + self = [self init]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _size = ASLayoutElementSizeMake(); +#if YOGA + _parentAlignStyle = ASStackLayoutAlignItemsNotSet; +#endif + } + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__) + +#pragma mark - ASLayoutElementStyleSize + +- (ASLayoutElementSize)size +{ + return _size.load(); +} + +- (void)setSize:(ASLayoutElementSize)size +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize = size; + }); + // No CallDelegate method as ASLayoutElementSize is currently internal. +} + +#pragma mark - ASLayoutElementStyleSizeForwarding + +- (ASDimension)width +{ + return _size.load().width; +} + +- (void)setWidth:(ASDimension)width +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = width; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); +} + +- (ASDimension)height +{ + return _size.load().height; +} + +- (void)setHeight:(ASDimension)height +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.height = height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (ASDimension)minWidth +{ + return _size.load().minWidth; +} + +- (void)setMinWidth:(ASDimension)minWidth +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = minWidth; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); +} + +- (ASDimension)maxWidth +{ + return _size.load().maxWidth; +} + +- (void)setMaxWidth:(ASDimension)maxWidth +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = maxWidth; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); +} + +- (ASDimension)minHeight +{ + return _size.load().minHeight; +} + +- (void)setMinHeight:(ASDimension)minHeight +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minHeight = minHeight; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (ASDimension)maxHeight +{ + return _size.load().maxHeight; +} + +- (void)setMaxHeight:(ASDimension)maxHeight +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxHeight = maxHeight; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + + +#pragma mark - ASLayoutElementStyleSizeHelpers + +- (void)setPreferredSize:(CGSize)preferredSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = ASDimensionMakeWithPoints(preferredSize.width); + newSize.height = ASDimensionMakeWithPoints(preferredSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (CGSize)preferredSize +{ + ASLayoutElementSize size = _size.load(); + if (size.width.unit == ASDimensionUnitFraction) { + NSCAssert(NO, @"Cannot get preferredSize of element with fractional width. Width: %@.", NSStringFromASDimension(size.width)); + return CGSizeZero; + } + + if (size.height.unit == ASDimensionUnitFraction) { + NSCAssert(NO, @"Cannot get preferredSize of element with fractional height. Height: %@.", NSStringFromASDimension(size.height)); + return CGSizeZero; + } + + return CGSizeMake(size.width.value, size.height.value); +} + +- (void)setMinSize:(CGSize)minSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = ASDimensionMakeWithPoints(minSize.width); + newSize.minHeight = ASDimensionMakeWithPoints(minSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (void)setMaxSize:(CGSize)maxSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = ASDimensionMakeWithPoints(maxSize.width); + newSize.maxHeight = ASDimensionMakeWithPoints(maxSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + +- (ASLayoutSize)preferredLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.width, size.height); +} + +- (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = preferredLayoutSize.width; + newSize.height = preferredLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (ASLayoutSize)minLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.minWidth, size.minHeight); +} + +- (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = minLayoutSize.width; + newSize.minHeight = minLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (ASLayoutSize)maxLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.maxWidth, size.maxHeight); +} + +- (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = maxLayoutSize.width; + newSize.maxHeight = maxLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + +#pragma mark - ASStackLayoutElement + +- (void)setSpacingBefore:(CGFloat)spacingBefore +{ + _spacingBefore.store(spacingBefore); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty); +} + +- (CGFloat)spacingBefore +{ + return _spacingBefore.load(); +} + +- (void)setSpacingAfter:(CGFloat)spacingAfter +{ + _spacingAfter.store(spacingAfter); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty); +} + +- (CGFloat)spacingAfter +{ + return _spacingAfter.load(); +} + +- (void)setFlexGrow:(CGFloat)flexGrow +{ + _flexGrow.store(flexGrow); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty); +} + +- (CGFloat)flexGrow +{ + return _flexGrow.load(); +} + +- (void)setFlexShrink:(CGFloat)flexShrink +{ + _flexShrink.store(flexShrink); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty); +} + +- (CGFloat)flexShrink +{ + return _flexShrink.load(); +} + +- (void)setFlexBasis:(ASDimension)flexBasis +{ + _flexBasis.store(flexBasis); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty); +} + +- (ASDimension)flexBasis +{ + return _flexBasis.load(); +} + +- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf +{ + _alignSelf.store(alignSelf); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty); +} + +- (ASStackLayoutAlignSelf)alignSelf +{ + return _alignSelf.load(); +} + +- (void)setAscender:(CGFloat)ascender +{ + _ascender.store(ascender); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty); +} + +- (CGFloat)ascender +{ + return _ascender.load(); +} + +- (void)setDescender:(CGFloat)descender +{ + _descender.store(descender); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty); +} + +- (CGFloat)descender +{ + return _descender.load(); +} + +#pragma mark - ASAbsoluteLayoutElement + +- (void)setLayoutPosition:(CGPoint)layoutPosition +{ + _layoutPosition.store(layoutPosition); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty); +} + +- (CGPoint)layoutPosition +{ + return _layoutPosition.load(); +} + +#pragma mark - Extensions + +- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Setting index outside of max bool extensions space"); + + MutexLocker l(__instanceLock__); + _extensions.boolExtensions[idx] = value; +} + +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\ +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Accessing index outside of max bool extensions space"); + + MutexLocker l(__instanceLock__); + return _extensions.boolExtensions[idx]; +} + +- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Setting index outside of max integer extensions space"); + + MutexLocker l(__instanceLock__); + _extensions.integerExtensions[idx] = value; +} + +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Accessing index outside of max integer extensions space"); + + MutexLocker l(__instanceLock__); + return _extensions.integerExtensions[idx]; +} + +- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Setting index outside of max edge insets extensions space"); + + MutexLocker l(__instanceLock__); + _extensions.edgeInsetsExtensions[idx] = value; +} + +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Accessing index outside of max edge insets extensions space"); + + MutexLocker l(__instanceLock__); + return _extensions.edgeInsetsExtensions[idx]; +} + +#pragma mark - Debugging + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if ((self.minLayoutSize.width.unit != ASDimensionUnitAuto || + self.minLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"minLayoutSize" : NSStringFromASLayoutSize(self.minLayoutSize) }]; + } + + if ((self.preferredLayoutSize.width.unit != ASDimensionUnitAuto || + self.preferredLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"preferredSize" : NSStringFromASLayoutSize(self.preferredLayoutSize) }]; + } + + if ((self.maxLayoutSize.width.unit != ASDimensionUnitAuto || + self.maxLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"maxLayoutSize" : NSStringFromASLayoutSize(self.maxLayoutSize) }]; + } + + if (self.alignSelf != ASStackLayoutAlignSelfAuto) { + [result addObject:@{ @"alignSelf" : [@[@"ASStackLayoutAlignSelfAuto", + @"ASStackLayoutAlignSelfStart", + @"ASStackLayoutAlignSelfEnd", + @"ASStackLayoutAlignSelfCenter", + @"ASStackLayoutAlignSelfStretch"] objectAtIndex:self.alignSelf] }]; + } + + if (self.ascender != 0) { + [result addObject:@{ @"ascender" : @(self.ascender) }]; + } + + if (self.descender != 0) { + [result addObject:@{ @"descender" : @(self.descender) }]; + } + + if (ASDimensionEqualToDimension(self.flexBasis, ASDimensionAuto) == NO) { + [result addObject:@{ @"flexBasis" : NSStringFromASDimension(self.flexBasis) }]; + } + + if (self.flexGrow != 0) { + [result addObject:@{ @"flexGrow" : @(self.flexGrow) }]; + } + + if (self.flexShrink != 0) { + [result addObject:@{ @"flexShrink" : @(self.flexShrink) }]; + } + + if (self.spacingAfter != 0) { + [result addObject:@{ @"spacingAfter" : @(self.spacingAfter) }]; + } + + if (self.spacingBefore != 0) { + [result addObject:@{ @"spacingBefore" : @(self.spacingBefore) }]; + } + + if (CGPointEqualToPoint(self.layoutPosition, CGPointZero) == NO) { + [result addObject:@{ @"layoutPosition" : [NSValue valueWithCGPoint:self.layoutPosition] }]; + } + + return result; +} + +- (void)propertyDidChange:(NSString *)propertyName +{ +#if YOGA + /* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT + void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow); + void YGNodeStyleSetFlex(YGNodeRef node, float flex); + */ + + if (_yogaNode == NULL) { + return; + } + // Because the NSStrings used to identify each property are const, use efficient pointer comparison. + if (propertyName == ASLayoutElementStyleWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Width, self.width); + } + else if (propertyName == ASLayoutElementStyleMinWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinWidth, self.minWidth); + } + else if (propertyName == ASLayoutElementStyleMaxWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxWidth, self.maxWidth); + } + else if (propertyName == ASLayoutElementStyleHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Height, self.height); + } + else if (propertyName == ASLayoutElementStyleMinHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinHeight, self.minHeight); + } + else if (propertyName == ASLayoutElementStyleMaxHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxHeight, self.maxHeight); + } + else if (propertyName == ASLayoutElementStyleFlexGrowProperty) { + YGNodeStyleSetFlexGrow(_yogaNode, self.flexGrow); + } + else if (propertyName == ASLayoutElementStyleFlexShrinkProperty) { + YGNodeStyleSetFlexShrink(_yogaNode, self.flexShrink); + } + else if (propertyName == ASLayoutElementStyleFlexBasisProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, self.flexBasis); + } + else if (propertyName == ASLayoutElementStyleAlignSelfProperty) { + YGNodeStyleSetAlignSelf(_yogaNode, yogaAlignSelf(self.alignSelf)); + } + else if (propertyName == ASYogaFlexWrapProperty) { + YGNodeStyleSetFlexWrap(_yogaNode, self.flexWrap); + } + else if (propertyName == ASYogaFlexDirectionProperty) { + YGNodeStyleSetFlexDirection(_yogaNode, yogaFlexDirection(self.flexDirection)); + } + else if (propertyName == ASYogaDirectionProperty) { + YGNodeStyleSetDirection(_yogaNode, self.direction); + } + else if (propertyName == ASYogaJustifyContentProperty) { + YGNodeStyleSetJustifyContent(_yogaNode, yogaJustifyContent(self.justifyContent)); + } + else if (propertyName == ASYogaAlignItemsProperty) { + ASStackLayoutAlignItems alignItems = self.alignItems; + if (alignItems != ASStackLayoutAlignItemsNotSet) { + YGNodeStyleSetAlignItems(_yogaNode, yogaAlignItems(alignItems)); + } + } + else if (propertyName == ASYogaPositionTypeProperty) { + YGNodeStyleSetPositionType(_yogaNode, self.positionType); + } + else if (propertyName == ASYogaPositionProperty) { + ASEdgeInsets position = self.position; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaMarginProperty) { + ASEdgeInsets margin = self.margin; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaPaddingProperty) { + ASEdgeInsets padding = self.padding; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaBorderProperty) { + ASEdgeInsets border = self.border; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_FLOAT_WITH_EDGE(_yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaAspectRatioProperty) { + CGFloat aspectRatio = self.aspectRatio; + if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) { + YGNodeStyleSetAspectRatio(_yogaNode, aspectRatio); + } + } +#endif +} + +#pragma mark - Yoga Flexbox Properties + +#if YOGA + ++ (void)initialize +{ + [super initialize]; + YGConfigSetPointScaleFactor(YGConfigGetDefault(), ASScreenScale()); + // Yoga recommends using Web Defaults for all new projects. This will be enabled for Texture very soon. + //YGConfigSetUseWebDefaults(YGConfigGetDefault(), true); +} + +- (YGNodeRef)yogaNode +{ + return _yogaNode; +} + +- (YGNodeRef)yogaNodeCreateIfNeeded +{ + if (_yogaNode == NULL) { + _yogaNode = YGNodeNew(); + } + return _yogaNode; +} + +- (void)destroyYogaNode +{ + if (_yogaNode != NULL) { + // Release the __bridge_retained Context object. + ASLayoutElementYogaUpdateMeasureFunc(_yogaNode, nil); + YGNodeFree(_yogaNode); + _yogaNode = NULL; + } +} + +- (void)dealloc +{ + [self destroyYogaNode]; +} + +- (YGWrap)flexWrap { return _flexWrap.load(); } +- (ASStackLayoutDirection)flexDirection { return _flexDirection.load(); } +- (YGDirection)direction { return _direction.load(); } +- (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); } +- (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); } +- (YGPositionType)positionType { return _positionType.load(); } +- (ASEdgeInsets)position { return _position.load(); } +- (ASEdgeInsets)margin { return _margin.load(); } +- (ASEdgeInsets)padding { return _padding.load(); } +- (ASEdgeInsets)border { return _border.load(); } +- (CGFloat)aspectRatio { return _aspectRatio.load(); } +// private (ASLayoutElementStylePrivate.h) +- (ASStackLayoutAlignItems)parentAlignStyle { + return _parentAlignStyle; +} + +- (void)setFlexWrap:(YGWrap)flexWrap { + _flexWrap.store(flexWrap); + ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty); +} +- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { + _flexDirection.store(flexDirection); + ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty); +} +- (void)setDirection:(YGDirection)direction { + _direction.store(direction); + ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty); +} +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { + _justifyContent.store(justify); + ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty); +} +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { + _alignItems.store(alignItems); + ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty); +} +- (void)setPositionType:(YGPositionType)positionType { + _positionType.store(positionType); + ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty); +} +- (void)setPosition:(ASEdgeInsets)position { + _position.store(position); + ASLayoutElementStyleCallDelegate(ASYogaPositionProperty); +} +- (void)setMargin:(ASEdgeInsets)margin { + _margin.store(margin); + ASLayoutElementStyleCallDelegate(ASYogaMarginProperty); +} +- (void)setPadding:(ASEdgeInsets)padding { + _padding.store(padding); + ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty); +} +- (void)setBorder:(ASEdgeInsets)border { + _border.store(border); + ASLayoutElementStyleCallDelegate(ASYogaBorderProperty); +} +- (void)setAspectRatio:(CGFloat)aspectRatio { + _aspectRatio.store(aspectRatio); + ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty); +} +// private (ASLayoutElementStylePrivate.h) +- (void)setParentAlignStyle:(ASStackLayoutAlignItems)style { + _parentAlignStyle = style; +} + +#endif /* YOGA */ + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.mm.orig b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.mm.orig new file mode 100644 index 0000000000..3a17293bc9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElement.mm.orig @@ -0,0 +1,869 @@ +// +// ASLayoutElement.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import + +#import +#include + +#if YOGA + #import YOGA_HEADER_PATH + #import +#endif + +#pragma mark - ASLayoutElementContext + +@implementation ASLayoutElementContext + +- (instancetype)init +{ + if (self = [super init]) { + _transitionID = ASLayoutElementContextDefaultTransitionID; + } + return self; +} + +@end + +CGFloat const ASLayoutElementParentDimensionUndefined = NAN; +CGSize const ASLayoutElementParentSizeUndefined = {ASLayoutElementParentDimensionUndefined, ASLayoutElementParentDimensionUndefined}; + +int32_t const ASLayoutElementContextInvalidTransitionID = 0; +int32_t const ASLayoutElementContextDefaultTransitionID = ASLayoutElementContextInvalidTransitionID + 1; + +<<<<<<< HEAD +#ifdef MINIMAL_ASDK +static ASLayoutElementContext *mainThreadTlsContext = nil; + +static ASLayoutElementContext *get_tls_context() { + if ([NSThread isMainThread]) { + return mainThreadTlsContext; + } else { + return [NSThread currentThread].threadDictionary[@"ASDK_tls_context"]; + } +} + +static void set_tls_context(ASLayoutElementContext *value) { + if ([NSThread isMainThread]) { + mainThreadTlsContext = value; + } else { + if (value != nil) { + [NSThread currentThread].threadDictionary[@"ASDK_tls_context"] = value; + } else { + [[NSThread currentThread].threadDictionary removeObjectForKey:@"ASDK_tls_context"]; + } + } +} +#else +======= +#if AS_TLS_AVAILABLE + +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +static _Thread_local __unsafe_unretained ASLayoutElementContext *tls_context; +#endif + +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ +#ifdef MINIMAL_ASDK + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(get_tls_context(), @"Nested ASLayoutElementContexts aren't supported."); + + ; + set_tls_context(context); +#else + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(tls_context, @"Nested ASLayoutElementContexts aren't supported."); + + tls_context = (__bridge ASLayoutElementContext *)(__bridge_retained CFTypeRef)context; +#endif +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + return get_tls_context(); +} + +void ASLayoutElementPopContext() +{ +#ifdef MINIMAL_ASDK + ASDisplayNodeCAssertNotNil(get_tls_context(), @"Attempt to pop context when there wasn't a context!"); + set_tls_context(nil); +#else + ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!"); + CFRelease((__bridge CFTypeRef)tls_context); + tls_context = nil; +#endif +} + +#else + +static pthread_key_t ASLayoutElementContextKey() { + static pthread_key_t k; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_key_create(&k, NULL); + }); + return k; +} +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(pthread_getspecific(ASLayoutElementContextKey()), @"Nested ASLayoutElementContexts aren't supported."); + + let cfCtx = (__bridge_retained CFTypeRef)context; + pthread_setspecific(ASLayoutElementContextKey(), cfCtx); +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + let ctxPtr = pthread_getspecific(ASLayoutElementContextKey()); + return (__bridge ASLayoutElementContext *)ctxPtr; +} + +void ASLayoutElementPopContext() +{ + let ctx = (CFTypeRef)pthread_getspecific(ASLayoutElementContextKey()); + ASDisplayNodeCAssertNotNil(ctx, @"Attempt to pop context when there wasn't a context!"); + CFRelease(ctx); + pthread_setspecific(ASLayoutElementContextKey(), NULL); +} + +#endif // AS_TLS_AVAILABLE + +#pragma mark - ASLayoutElementStyle + +NSString * const ASLayoutElementStyleWidthProperty = @"ASLayoutElementStyleWidthProperty"; +NSString * const ASLayoutElementStyleMinWidthProperty = @"ASLayoutElementStyleMinWidthProperty"; +NSString * const ASLayoutElementStyleMaxWidthProperty = @"ASLayoutElementStyleMaxWidthProperty"; + +NSString * const ASLayoutElementStyleHeightProperty = @"ASLayoutElementStyleHeightProperty"; +NSString * const ASLayoutElementStyleMinHeightProperty = @"ASLayoutElementStyleMinHeightProperty"; +NSString * const ASLayoutElementStyleMaxHeightProperty = @"ASLayoutElementStyleMaxHeightProperty"; + +NSString * const ASLayoutElementStyleSpacingBeforeProperty = @"ASLayoutElementStyleSpacingBeforeProperty"; +NSString * const ASLayoutElementStyleSpacingAfterProperty = @"ASLayoutElementStyleSpacingAfterProperty"; +NSString * const ASLayoutElementStyleFlexGrowProperty = @"ASLayoutElementStyleFlexGrowProperty"; +NSString * const ASLayoutElementStyleFlexShrinkProperty = @"ASLayoutElementStyleFlexShrinkProperty"; +NSString * const ASLayoutElementStyleFlexBasisProperty = @"ASLayoutElementStyleFlexBasisProperty"; +NSString * const ASLayoutElementStyleAlignSelfProperty = @"ASLayoutElementStyleAlignSelfProperty"; +NSString * const ASLayoutElementStyleAscenderProperty = @"ASLayoutElementStyleAscenderProperty"; +NSString * const ASLayoutElementStyleDescenderProperty = @"ASLayoutElementStyleDescenderProperty"; + +NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementStyleLayoutPositionProperty"; + +#if YOGA +NSString * const ASYogaFlexWrapProperty = @"ASLayoutElementStyleLayoutFlexWrapProperty"; +NSString * const ASYogaFlexDirectionProperty = @"ASYogaFlexDirectionProperty"; +NSString * const ASYogaDirectionProperty = @"ASYogaDirectionProperty"; +NSString * const ASYogaSpacingProperty = @"ASYogaSpacingProperty"; +NSString * const ASYogaJustifyContentProperty = @"ASYogaJustifyContentProperty"; +NSString * const ASYogaAlignItemsProperty = @"ASYogaAlignItemsProperty"; +NSString * const ASYogaPositionTypeProperty = @"ASYogaPositionTypeProperty"; +NSString * const ASYogaPositionProperty = @"ASYogaPositionProperty"; +NSString * const ASYogaMarginProperty = @"ASYogaMarginProperty"; +NSString * const ASYogaPaddingProperty = @"ASYogaPaddingProperty"; +NSString * const ASYogaBorderProperty = @"ASYogaBorderProperty"; +NSString * const ASYogaAspectRatioProperty = @"ASYogaAspectRatioProperty"; +#endif + +#define ASLayoutElementStyleSetSizeWithScope(x) \ + __instanceLock__.lock(); \ + ASLayoutElementSize newSize = _size.load(); \ + { x } \ + _size.store(newSize); \ + __instanceLock__.unlock(); + +#define ASLayoutElementStyleCallDelegate(propertyName)\ +do {\ + [self propertyDidChange:propertyName];\ + [_delegate style:self propertyDidChange:propertyName];\ +} while(0) + +@implementation ASLayoutElementStyle { + ASDN::RecursiveMutex __instanceLock__; + ASLayoutElementStyleExtensions _extensions; + + std::atomic _size; + std::atomic _spacingBefore; + std::atomic _spacingAfter; + std::atomic _flexGrow; + std::atomic _flexShrink; + std::atomic _flexBasis; + std::atomic _alignSelf; + std::atomic _ascender; + std::atomic _descender; + std::atomic _layoutPosition; + +#if YOGA + YGNodeRef _yogaNode; + std::atomic _flexWrap; + std::atomic _flexDirection; + std::atomic _direction; + std::atomic _justifyContent; + std::atomic _alignItems; + std::atomic _positionType; + std::atomic _position; + std::atomic _margin; + std::atomic _padding; + std::atomic _border; + std::atomic _aspectRatio; +#endif +} + +@dynamic width, height, minWidth, maxWidth, minHeight, maxHeight; +@dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize; + +#pragma mark - Lifecycle + +- (instancetype)initWithDelegate:(id)delegate +{ + self = [self init]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _size = ASLayoutElementSizeMake(); + } + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__) + +#pragma mark - ASLayoutElementStyleSize + +- (ASLayoutElementSize)size +{ + return _size.load(); +} + +- (void)setSize:(ASLayoutElementSize)size +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize = size; + }); + // No CallDelegate method as ASLayoutElementSize is currently internal. +} + +#pragma mark - ASLayoutElementStyleSizeForwarding + +- (ASDimension)width +{ + return _size.load().width; +} + +- (void)setWidth:(ASDimension)width +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = width; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); +} + +- (ASDimension)height +{ + return _size.load().height; +} + +- (void)setHeight:(ASDimension)height +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.height = height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (ASDimension)minWidth +{ + return _size.load().minWidth; +} + +- (void)setMinWidth:(ASDimension)minWidth +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = minWidth; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); +} + +- (ASDimension)maxWidth +{ + return _size.load().maxWidth; +} + +- (void)setMaxWidth:(ASDimension)maxWidth +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = maxWidth; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); +} + +- (ASDimension)minHeight +{ + return _size.load().minHeight; +} + +- (void)setMinHeight:(ASDimension)minHeight +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minHeight = minHeight; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (ASDimension)maxHeight +{ + return _size.load().maxHeight; +} + +- (void)setMaxHeight:(ASDimension)maxHeight +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxHeight = maxHeight; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + + +#pragma mark - ASLayoutElementStyleSizeHelpers + +- (void)setPreferredSize:(CGSize)preferredSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = ASDimensionMakeWithPoints(preferredSize.width); + newSize.height = ASDimensionMakeWithPoints(preferredSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (CGSize)preferredSize +{ + ASLayoutElementSize size = _size.load(); + if (size.width.unit == ASDimensionUnitFraction) { + NSCAssert(NO, @"Cannot get preferredSize of element with fractional width. Width: %@.", NSStringFromASDimension(size.width)); + return CGSizeZero; + } + + if (size.height.unit == ASDimensionUnitFraction) { + NSCAssert(NO, @"Cannot get preferredSize of element with fractional height. Height: %@.", NSStringFromASDimension(size.height)); + return CGSizeZero; + } + + return CGSizeMake(size.width.value, size.height.value); +} + +- (void)setMinSize:(CGSize)minSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = ASDimensionMakeWithPoints(minSize.width); + newSize.minHeight = ASDimensionMakeWithPoints(minSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (void)setMaxSize:(CGSize)maxSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = ASDimensionMakeWithPoints(maxSize.width); + newSize.maxHeight = ASDimensionMakeWithPoints(maxSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + +- (ASLayoutSize)preferredLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.width, size.height); +} + +- (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = preferredLayoutSize.width; + newSize.height = preferredLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (ASLayoutSize)minLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.minWidth, size.minHeight); +} + +- (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = minLayoutSize.width; + newSize.minHeight = minLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (ASLayoutSize)maxLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.maxWidth, size.maxHeight); +} + +- (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = maxLayoutSize.width; + newSize.maxHeight = maxLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + +#pragma mark - ASStackLayoutElement + +- (void)setSpacingBefore:(CGFloat)spacingBefore +{ + _spacingBefore.store(spacingBefore); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty); +} + +- (CGFloat)spacingBefore +{ + return _spacingBefore.load(); +} + +- (void)setSpacingAfter:(CGFloat)spacingAfter +{ + _spacingAfter.store(spacingAfter); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty); +} + +- (CGFloat)spacingAfter +{ + return _spacingAfter.load(); +} + +- (void)setFlexGrow:(CGFloat)flexGrow +{ + _flexGrow.store(flexGrow); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty); +} + +- (CGFloat)flexGrow +{ + return _flexGrow.load(); +} + +- (void)setFlexShrink:(CGFloat)flexShrink +{ + _flexShrink.store(flexShrink); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty); +} + +- (CGFloat)flexShrink +{ + return _flexShrink.load(); +} + +- (void)setFlexBasis:(ASDimension)flexBasis +{ + _flexBasis.store(flexBasis); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty); +} + +- (ASDimension)flexBasis +{ + return _flexBasis.load(); +} + +- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf +{ + _alignSelf.store(alignSelf); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty); +} + +- (ASStackLayoutAlignSelf)alignSelf +{ + return _alignSelf.load(); +} + +- (void)setAscender:(CGFloat)ascender +{ + _ascender.store(ascender); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty); +} + +- (CGFloat)ascender +{ + return _ascender.load(); +} + +- (void)setDescender:(CGFloat)descender +{ + _descender.store(descender); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty); +} + +- (CGFloat)descender +{ + return _descender.load(); +} + +#pragma mark - ASAbsoluteLayoutElement + +- (void)setLayoutPosition:(CGPoint)layoutPosition +{ + _layoutPosition.store(layoutPosition); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty); +} + +- (CGPoint)layoutPosition +{ + return _layoutPosition.load(); +} + +#pragma mark - Extensions + +- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Setting index outside of max bool extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + _extensions.boolExtensions[idx] = value; +} + +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\ +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Accessing index outside of max bool extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + return _extensions.boolExtensions[idx]; +} + +- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Setting index outside of max integer extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + _extensions.integerExtensions[idx] = value; +} + +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Accessing index outside of max integer extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + return _extensions.integerExtensions[idx]; +} + +- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Setting index outside of max edge insets extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + _extensions.edgeInsetsExtensions[idx] = value; +} + +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Accessing index outside of max edge insets extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + return _extensions.edgeInsetsExtensions[idx]; +} + +#pragma mark - Debugging + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if ((self.minLayoutSize.width.unit != ASDimensionUnitAuto || + self.minLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"minLayoutSize" : NSStringFromASLayoutSize(self.minLayoutSize) }]; + } + + if ((self.preferredLayoutSize.width.unit != ASDimensionUnitAuto || + self.preferredLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"preferredSize" : NSStringFromASLayoutSize(self.preferredLayoutSize) }]; + } + + if ((self.maxLayoutSize.width.unit != ASDimensionUnitAuto || + self.maxLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"maxLayoutSize" : NSStringFromASLayoutSize(self.maxLayoutSize) }]; + } + + if (self.alignSelf != ASStackLayoutAlignSelfAuto) { + [result addObject:@{ @"alignSelf" : [@[@"ASStackLayoutAlignSelfAuto", + @"ASStackLayoutAlignSelfStart", + @"ASStackLayoutAlignSelfEnd", + @"ASStackLayoutAlignSelfCenter", + @"ASStackLayoutAlignSelfStretch"] objectAtIndex:self.alignSelf] }]; + } + + if (self.ascender != 0) { + [result addObject:@{ @"ascender" : @(self.ascender) }]; + } + + if (self.descender != 0) { + [result addObject:@{ @"descender" : @(self.descender) }]; + } + + if (ASDimensionEqualToDimension(self.flexBasis, ASDimensionAuto) == NO) { + [result addObject:@{ @"flexBasis" : NSStringFromASDimension(self.flexBasis) }]; + } + + if (self.flexGrow != 0) { + [result addObject:@{ @"flexGrow" : @(self.flexGrow) }]; + } + + if (self.flexShrink != 0) { + [result addObject:@{ @"flexShrink" : @(self.flexShrink) }]; + } + + if (self.spacingAfter != 0) { + [result addObject:@{ @"spacingAfter" : @(self.spacingAfter) }]; + } + + if (self.spacingBefore != 0) { + [result addObject:@{ @"spacingBefore" : @(self.spacingBefore) }]; + } + + if (CGPointEqualToPoint(self.layoutPosition, CGPointZero) == NO) { + [result addObject:@{ @"layoutPosition" : [NSValue valueWithCGPoint:self.layoutPosition] }]; + } + + return result; +} + +- (void)propertyDidChange:(NSString *)propertyName +{ +#if YOGA + /* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT + void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow); + void YGNodeStyleSetFlex(YGNodeRef node, float flex); + */ + + if (_yogaNode == NULL) { + return; + } + // Because the NSStrings used to identify each property are const, use efficient pointer comparison. + if (propertyName == ASLayoutElementStyleWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Width, self.width); + } + else if (propertyName == ASLayoutElementStyleMinWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinWidth, self.minWidth); + } + else if (propertyName == ASLayoutElementStyleMaxWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxWidth, self.maxWidth); + } + else if (propertyName == ASLayoutElementStyleHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Height, self.height); + } + else if (propertyName == ASLayoutElementStyleMinHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinHeight, self.minHeight); + } + else if (propertyName == ASLayoutElementStyleMaxHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxHeight, self.maxHeight); + } + else if (propertyName == ASLayoutElementStyleFlexGrowProperty) { + YGNodeStyleSetFlexGrow(_yogaNode, self.flexGrow); + } + else if (propertyName == ASLayoutElementStyleFlexShrinkProperty) { + YGNodeStyleSetFlexShrink(_yogaNode, self.flexShrink); + } + else if (propertyName == ASLayoutElementStyleFlexBasisProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, self.flexBasis); + } + else if (propertyName == ASLayoutElementStyleAlignSelfProperty) { + YGNodeStyleSetAlignSelf(_yogaNode, yogaAlignSelf(self.alignSelf)); + } + else if (propertyName == ASYogaFlexWrapProperty) { + YGNodeStyleSetFlexWrap(_yogaNode, self.flexWrap); + } + else if (propertyName == ASYogaFlexDirectionProperty) { + YGNodeStyleSetFlexDirection(_yogaNode, yogaFlexDirection(self.flexDirection)); + } + else if (propertyName == ASYogaDirectionProperty) { + YGNodeStyleSetDirection(_yogaNode, self.direction); + } + else if (propertyName == ASYogaJustifyContentProperty) { + YGNodeStyleSetJustifyContent(_yogaNode, yogaJustifyContent(self.justifyContent)); + } + else if (propertyName == ASYogaAlignItemsProperty) { + ASStackLayoutAlignItems alignItems = self.alignItems; + if (alignItems != ASStackLayoutAlignItemsNotSet) { + YGNodeStyleSetAlignItems(_yogaNode, yogaAlignItems(alignItems)); + } + } + else if (propertyName == ASYogaPositionTypeProperty) { + YGNodeStyleSetPositionType(_yogaNode, self.positionType); + } + else if (propertyName == ASYogaPositionProperty) { + ASEdgeInsets position = self.position; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaMarginProperty) { + ASEdgeInsets margin = self.margin; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaPaddingProperty) { + ASEdgeInsets padding = self.padding; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaBorderProperty) { + ASEdgeInsets border = self.border; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_FLOAT_WITH_EDGE(_yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaAspectRatioProperty) { + CGFloat aspectRatio = self.aspectRatio; + if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) { + YGNodeStyleSetAspectRatio(_yogaNode, aspectRatio); + } + } +#endif +} + +#pragma mark - Yoga Flexbox Properties + +#if YOGA + ++ (void)initialize +{ + [super initialize]; + YGConfigSetPointScaleFactor(YGConfigGetDefault(), ASScreenScale()); + // Yoga recommends using Web Defaults for all new projects. This will be enabled for Texture very soon. + //YGConfigSetUseWebDefaults(YGConfigGetDefault(), true); +} + +- (YGNodeRef)yogaNode +{ + return _yogaNode; +} + +- (YGNodeRef)yogaNodeCreateIfNeeded +{ + if (_yogaNode == NULL) { + _yogaNode = YGNodeNew(); + } + return _yogaNode; +} + +- (void)destroyYogaNode +{ + if (_yogaNode != NULL) { + // Release the __bridge_retained Context object. + ASLayoutElementYogaUpdateMeasureFunc(_yogaNode, nil); + YGNodeFree(_yogaNode); + _yogaNode = NULL; + } +} + +- (void)dealloc +{ + [self destroyYogaNode]; +} + +- (YGWrap)flexWrap { return _flexWrap.load(); } +- (ASStackLayoutDirection)flexDirection { return _flexDirection.load(); } +- (YGDirection)direction { return _direction.load(); } +- (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); } +- (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); } +- (YGPositionType)positionType { return _positionType.load(); } +- (ASEdgeInsets)position { return _position.load(); } +- (ASEdgeInsets)margin { return _margin.load(); } +- (ASEdgeInsets)padding { return _padding.load(); } +- (ASEdgeInsets)border { return _border.load(); } +- (CGFloat)aspectRatio { return _aspectRatio.load(); } + +- (void)setFlexWrap:(YGWrap)flexWrap { + _flexWrap.store(flexWrap); + ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty); +} +- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { + _flexDirection.store(flexDirection); + ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty); +} +- (void)setDirection:(YGDirection)direction { + _direction.store(direction); + ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty); +} +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { + _justifyContent.store(justify); + ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty); +} +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { + _alignItems.store(alignItems); + ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty); +} +- (void)setPositionType:(YGPositionType)positionType { + _positionType.store(positionType); + ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty); +} +- (void)setPosition:(ASEdgeInsets)position { + _position.store(position); + ASLayoutElementStyleCallDelegate(ASYogaPositionProperty); +} +- (void)setMargin:(ASEdgeInsets)margin { + _margin.store(margin); + ASLayoutElementStyleCallDelegate(ASYogaMarginProperty); +} +- (void)setPadding:(ASEdgeInsets)padding { + _padding.store(padding); + ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty); +} +- (void)setBorder:(ASEdgeInsets)border { + _border.store(border); + ASLayoutElementStyleCallDelegate(ASYogaBorderProperty); +} +- (void)setAspectRatio:(CGFloat)aspectRatio { + _aspectRatio.store(aspectRatio); + ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty); +} + +#endif /* YOGA */ + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElementExtensibility.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElementExtensibility.h new file mode 100644 index 0000000000..f46b63e812 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElementExtensibility.h @@ -0,0 +1,106 @@ +// +// ASLayoutElementExtensibility.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#pragma mark - ASLayoutElementExtensibility + +@protocol ASLayoutElementExtensibility + +// The maximum number of extended values per type are defined in ASEnvironment.h above the ASEnvironmentStateExtensions +// struct definition. If you try to set a value at an index after the maximum it will throw an assertion. + +- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx; +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx; + +- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx; +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx; + +- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx; +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx; + +@end + +#pragma mark - Dynamic Properties + +/** + * Unbox NSNumber based on the type + */ +#define ASDK_UNBOX_NUMBER(NUMBER, PROPERTY_TYPE) \ +const char *objCType = [NUMBER objCType]; \ +if (strcmp(objCType, @encode(BOOL)) == 0) { \ + return (PROPERTY_TYPE)[obj boolValue]; \ +} else if (strcmp(objCType, @encode(int)) == 0) { \ + return (PROPERTY_TYPE)[obj intValue]; \ +} else if (strcmp(objCType, @encode(NSInteger)) == 0) { \ + return (PROPERTY_TYPE)[obj integerValue]; \ +} else if (strcmp(objCType, @encode(NSUInteger)) == 0) { \ + return (PROPERTY_TYPE)[obj unsignedIntegerValue]; \ +} else if (strcmp(objCType, @encode(CGFloat)) == 0) { \ + return (PROPERTY_TYPE)[obj floatValue]; \ +} else { \ + NSAssert(NO, @"Data type not supported"); \ +} \ + +/** + * Define a NSObject property + */ +#define ASDK_STYLE_PROP_OBJ(PROPERTY_TYPE, PROPERTY_NAME, SETTER_NAME) \ +@dynamic PROPERTY_NAME; \ +- (PROPERTY_TYPE)PROPERTY_NAME \ +{ \ + return (PROPERTY_TYPE)objc_getAssociatedObject(self, @selector(PROPERTY_NAME)); \ +} \ +\ +- (void)SETTER_NAME:(PROPERTY_TYPE)PROPERTY_NAME \ +{ \ + objc_setAssociatedObject(self, @selector(PROPERTY_NAME), PROPERTY_NAME, OBJC_ASSOCIATION_RETAIN); \ +} \ + +/** + * Define an primitive property + */ +#define ASDK_STYLE_PROP_PRIM(PROPERTY_TYPE, PROPERTY_NAME, SETTER_NAME, DEFAULT_VALUE) \ +@dynamic PROPERTY_NAME; \ +- (PROPERTY_TYPE)PROPERTY_NAME \ +{ \ + id obj = objc_getAssociatedObject(self, @selector(PROPERTY_NAME)); \ + \ + if (obj != nil) { \ + ASDK_UNBOX_NUMBER(obj, PROPERTY_TYPE); \ + } \ + \ + return DEFAULT_VALUE;\ +} \ +\ +- (void)SETTER_NAME:(PROPERTY_TYPE)PROPERTY_NAME \ +{ \ + objc_setAssociatedObject(self, @selector(PROPERTY_NAME), @(PROPERTY_NAME), OBJC_ASSOCIATION_RETAIN); \ +} \ + +/** + * Define an structure property + */ +#define ASDK_STYLE_PROP_STR(PROPERTY_TYPE, PROPERTY_NAME, SETTER_NAME, DEFAULT_STRUCT) \ +@dynamic PROPERTY_NAME; \ +- (PROPERTY_TYPE)PROPERTY_NAME \ +{ \ + id obj = objc_getAssociatedObject(self, @selector(PROPERTY_NAME)); \ + if (obj == nil) { \ + return DEFAULT_STRUCT; \ + } \ + PROPERTY_TYPE PROPERTY_NAME; [obj getValue:&PROPERTY_NAME]; return PROPERTY_NAME; \ +} \ +\ +- (void)SETTER_NAME:(PROPERTY_TYPE)PROPERTY_NAME \ +{ \ + objc_setAssociatedObject(self, @selector(PROPERTY_NAME), [NSValue value:&PROPERTY_NAME withObjCType:@encode(PROPERTY_TYPE)], OBJC_ASSOCIATION_RETAIN_NONATOMIC);\ +} \ diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElementPrivate.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElementPrivate.h new file mode 100644 index 0000000000..cf699f42a2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutElementPrivate.h @@ -0,0 +1,106 @@ +// +// ASLayoutElementPrivate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@protocol ASLayoutElement; +@class ASLayoutElementStyle; + +#pragma mark - ASLayoutElementContext + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASLayoutElementContext : NSObject +@property (nonatomic) int32_t transitionID; +@end + +AS_EXTERN int32_t const ASLayoutElementContextInvalidTransitionID; + +AS_EXTERN int32_t const ASLayoutElementContextDefaultTransitionID; + +// Does not currently support nesting – there must be no current context. +AS_EXTERN void ASLayoutElementPushContext(ASLayoutElementContext * context); + +AS_EXTERN ASLayoutElementContext * _Nullable ASLayoutElementGetCurrentContext(void); + +AS_EXTERN void ASLayoutElementPopContext(void); + +NS_ASSUME_NONNULL_END + +#pragma mark - ASLayoutElementLayoutDefaults + +#define ASLayoutElementLayoutCalculationDefaults \ +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize\ +{\ + return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];\ +}\ +\ +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize\ +{\ + return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize];\ +}\ +\ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize\ + restrictedToSize:(ASLayoutElementSize)size\ + relativeToParentSize:(CGSize)parentSize\ +{\ + const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, ASLayoutElementSizeResolve(self.style.size, parentSize));\ + return [self calculateLayoutThatFits:resolvedRange];\ +}\ + + +#pragma mark - ASLayoutElementExtensibility + +// Provides extension points for elments that comply to ASLayoutElement like ASLayoutSpec to add additional +// properties besides the default one provided in ASLayoutElementStyle + +static const int kMaxLayoutElementBoolExtensions = 1; +static const int kMaxLayoutElementStateIntegerExtensions = 4; +static const int kMaxLayoutElementStateEdgeInsetExtensions = 1; + +typedef struct ASLayoutElementStyleExtensions { + // Values to store extensions + BOOL boolExtensions[kMaxLayoutElementBoolExtensions]; + NSInteger integerExtensions[kMaxLayoutElementStateIntegerExtensions]; + UIEdgeInsets edgeInsetsExtensions[kMaxLayoutElementStateEdgeInsetExtensions]; +} ASLayoutElementStyleExtensions; + +#define ASLayoutElementStyleExtensibilityForwarding \ +- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx\ +{\ + [self.style setLayoutOptionExtensionBool:value atIndex:idx];\ +}\ +\ +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\ +{\ + return [self.style layoutOptionExtensionBoolAtIndex:idx];\ +}\ +\ +- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx\ +{\ + [self.style setLayoutOptionExtensionInteger:value atIndex:idx];\ +}\ +\ +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx\ +{\ + return [self.style layoutOptionExtensionIntegerAtIndex:idx];\ +}\ +\ +- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx\ +{\ + [self.style setLayoutOptionExtensionEdgeInsets:value atIndex:idx];\ +}\ +\ +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx\ +{\ + return [self.style layoutOptionExtensionEdgeInsetsAtIndex:idx];\ +}\ + diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec+Subclasses.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec+Subclasses.h new file mode 100644 index 0000000000..34bf6e069f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec+Subclasses.h @@ -0,0 +1,59 @@ +// +// ASLayoutSpec+Subclasses.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASLayoutElement; + +@interface ASLayoutSpec (Subclassing) + +/** + * Adds a child with the given identifier to this layout spec. + * + * @param child A child to be added. + * + * @param index An index associated with the child. + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, + * only require a single child. + * + * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) + * a subclass can use the setChild method to set the "primary" child. It should then use this method + * to set any other required children. Ideally a subclass would hide this from the user, and use the + * setChild:forIndex: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild + * property that behind the scenes is calling setChild:forIndex:. + */ +- (void)setChild:(id)child atIndex:(NSUInteger)index; + +/** + * Returns the child added to this layout spec using the given index. + * + * @param index An identifier associated with the the child. + */ +- (nullable id)childAtIndex:(NSUInteger)index; + +@end + +@interface ASLayout () + +/** + * Position in parent. Default to CGPointNull. + * + * @discussion When being used as a sublayout, this property must not equal CGPointNull. + */ +@property (nonatomic) CGPoint position; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec+Subclasses.mm b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec+Subclasses.mm new file mode 100644 index 0000000000..17ff53e6c5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec+Subclasses.mm @@ -0,0 +1,87 @@ +// +// ASLayoutSpec+Subclasses.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#pragma mark - ASNullLayoutSpec + +@interface ASNullLayoutSpec : ASLayoutSpec +- (instancetype)init NS_UNAVAILABLE; ++ (ASNullLayoutSpec *)null; +@end + +@implementation ASNullLayoutSpec : ASLayoutSpec + ++ (ASNullLayoutSpec *)null +{ + static ASNullLayoutSpec *sharedNullLayoutSpec = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedNullLayoutSpec = [[self alloc] init]; + }); + return sharedNullLayoutSpec; +} + +- (BOOL)isMutable +{ + return NO; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + return [ASLayout layoutWithLayoutElement:self size:CGSizeZero]; +} + +@end + + +#pragma mark - ASLayoutSpec (Subclassing) + +@implementation ASLayoutSpec (Subclassing) + +#pragma mark - Child with index + +- (void)setChild:(id)child atIndex:(NSUInteger)index +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + + id layoutElement = child ?: [ASNullLayoutSpec null]; + + if (child) { + if (_childrenArray.count < index) { + // Fill up the array with null objects until the index + NSInteger i = _childrenArray.count; + while (i < index) { + _childrenArray[i] = [ASNullLayoutSpec null]; + i++; + } + } + } + + // Replace object at the given index with the layoutElement + _childrenArray[index] = layoutElement; +} + +- (id)childAtIndex:(NSUInteger)index +{ + id layoutElement = nil; + if (index < _childrenArray.count) { + layoutElement = _childrenArray[index]; + } + + // Null layoutElement should not be accessed + ASDisplayNodeAssert(layoutElement != [ASNullLayoutSpec null], @"Access child at index without set a child at that index"); + + return layoutElement; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec.h new file mode 100644 index 0000000000..c7aa01c386 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec.h @@ -0,0 +1,100 @@ +// +// ASLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A layout spec is an immutable object that describes a layout, loosely inspired by React. + */ +@interface ASLayoutSpec : NSObject + +/** + * Creation of a layout spec should only happen by a user in layoutSpecThatFits:. During that method, a + * layout spec can be created and mutated. Once it is passed back to ASDK, the isMutable flag will be + * set to NO and any further mutations will cause an assert. + */ +@property (nonatomic) BOOL isMutable; + +/** + * First child within the children's array. + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, + * only require a single child. + * + * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) + * a subclass should use this method to set the "primary" child. It can then use setChild:atIndex: + * to set any other required children. Ideally a subclass would hide this from the user, and use the + * setChild:atIndex: internally. For example, ASBackgroundLayoutSpec exposes a "background" + * property that behind the scenes is calling setChild:atIndex:. + */ +@property (nullable, nonatomic) id child; + +/** + * An array of ASLayoutElement children + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * reponsibility of holding on to the spec children. Some layout specs, like ASStackLayoutSpec, + * can take an unknown number of children. In this case, the this method should be used. + * For good measure, in these layout specs it probably makes sense to define + * setChild: and setChild:forIdentifier: methods to do something appropriate or to assert. + */ +@property (nullable, nonatomic) NSArray> *children; + +@end + +/** + * An ASLayoutSpec subclass that can wrap one or more ASLayoutElement and calculates the layout based on the + * sizes of the children. If multiple children are provided the size of the biggest child will be used to for + * size of this layout spec. + */ +@interface ASWrapperLayoutSpec : ASLayoutSpec + +/* + * Returns an ASWrapperLayoutSpec object with the given layoutElement as child. + */ ++ (instancetype)wrapperWithLayoutElement:(id)layoutElement NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/* + * Returns an ASWrapperLayoutSpec object with the given layoutElements as children. + */ ++ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/* + * Returns an ASWrapperLayoutSpec object initialized with the given layoutElement as child. + */ +- (instancetype)initWithLayoutElement:(id)layoutElement AS_WARN_UNUSED_RESULT; + +/* + * Returns an ASWrapperLayoutSpec object initialized with the given layoutElements as children. + */ +- (instancetype)initWithLayoutElements:(NSArray> *)layoutElements AS_WARN_UNUSED_RESULT; + +/* + * Init not available for ASWrapperLayoutSpec + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +@interface ASLayoutSpec (Debugging) +/** + * Used by other layout specs to create ascii art debug strings + */ ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction; ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec.mm new file mode 100644 index 0000000000..8da22d3d01 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASLayoutSpec.mm @@ -0,0 +1,338 @@ +// +// ASLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +#import +#import +#import +#import +#import + +#import +#import +#import + +@implementation ASLayoutSpec + +// Dynamic properties for ASLayoutElements +@dynamic layoutElementType; +@synthesize debugName = _debugName; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _isMutable = YES; + _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + _childrenArray = [[NSMutableArray alloc] init]; + + return self; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeLayoutSpec; +} + +- (BOOL)canLayoutAsynchronous +{ + return YES; +} + +- (BOOL)implementsLayoutMethod +{ + return YES; +} + +#pragma mark - Style + +- (ASLayoutElementStyle *)style +{ + AS::MutexLocker l(__instanceLock__); + if (_style == nil) { + _style = [[ASLayoutElementStyle alloc] init]; + } + return _style; +} + +- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock +{ + styleBlock(self.style); + return self; +} + +#pragma mark - Layout + +ASLayoutElementLayoutCalculationDefaults + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + return [ASLayout layoutWithLayoutElement:self size:constrainedSize.min]; +} + +#pragma mark - Child + +- (void)setChild:(id)child +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_childrenArray.count < 2, @"This layout spec does not support more than one child. Use the setChildren: or the setChild:AtIndex: API"); + + if (child) { + _childrenArray[0] = child; + } else { + if (_childrenArray.count) { + [_childrenArray removeObjectAtIndex:0]; + } + } +} + +- (id)child +{ + ASDisplayNodeAssert(_childrenArray.count < 2, @"This layout spec does not support more than one child. Use the setChildren: or the setChild:AtIndex: API"); + + return _childrenArray.firstObject; +} + +#pragma mark - Children + +- (void)setChildren:(NSArray> *)children +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + for (id child in children) { + ASDisplayNodeAssert([child conformsToProtocol:NSProtocolFromString(@"ASLayoutElement")], @"Child %@ of spec %@ is not an ASLayoutElement!", child, self); + } +#endif + [_childrenArray setArray:children]; +} + +- (nullable NSArray> *)children +{ + return [_childrenArray copy]; +} + +- (NSArray> *)sublayoutElements +{ + return [_childrenArray copy]; +} + +#pragma mark - NSFastEnumeration + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len +{ + return [_childrenArray countByEnumeratingWithState:state objects:buffer count:len]; +} + +#pragma mark - ASTraitEnvironment + +- (ASTraitCollection *)asyncTraitCollection +{ + AS::MutexLocker l(__instanceLock__); + return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; +} + +ASPrimitiveTraitCollectionDefaults + +#pragma mark - ASLayoutElementStyleExtensibility + +ASLayoutElementStyleExtensibilityForwarding + +#pragma mark - ASDescriptionProvider + +- (NSMutableArray *)propertiesForDescription +{ + const auto result = [NSMutableArray array]; + if (NSArray *children = self.children) { + // Use tiny descriptions because these trees can get nested very deep. + const auto tinyDescriptions = ASArrayByFlatMapping(children, id object, ASObjectDescriptionMakeTiny(object)); + [result addObject:@{ @"children": tinyDescriptions }]; + } + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +#pragma mark - Framework Private + +#if AS_DEDUPE_LAYOUT_SPEC_TREE +- (nullable NSHashTable> *)findDuplicatedElementsInSubtree +{ + NSHashTable *result = nil; + NSUInteger count = 0; + [self _findDuplicatedElementsInSubtreeWithWorkingSet:[NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality] workingCount:&count result:&result]; + return result; +} + +/** + * This method is extremely performance-sensitive, so we do some strange things. + * + * @param workingSet A working set of elements for use in the recursion. + * @param workingCount The current count of the set for use in the recursion. + * @param result The set into which to put the result. This initially points to @c nil to save time if no duplicates exist. + */ +- (void)_findDuplicatedElementsInSubtreeWithWorkingSet:(NSHashTable> *)workingSet workingCount:(NSUInteger *)workingCount result:(NSHashTable> * _Nullable *)result +{ + Class layoutSpecClass = [ASLayoutSpec class]; + + for (id child in self) { + // Add the object into the set. + [workingSet addObject:child]; + + // Check that addObject: caused the count to increase. + // This is faster than using containsObject. + NSUInteger oldCount = *workingCount; + NSUInteger newCount = workingSet.count; + BOOL objectAlreadyExisted = (newCount != oldCount + 1); + if (objectAlreadyExisted) { + if (*result == nil) { + *result = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + } + [*result addObject:child]; + } else { + *workingCount = newCount; + // If child is a layout spec we haven't visited, recurse its children. + if ([child isKindOfClass:layoutSpecClass]) { + [(ASLayoutSpec *)child _findDuplicatedElementsInSubtreeWithWorkingSet:workingSet workingCount:workingCount result:result]; + } + } + } +} +#endif + +#pragma mark - Debugging + +- (NSString *)debugName +{ + AS::MutexLocker l(__instanceLock__); + return _debugName; +} + +- (void)setDebugName:(NSString *)debugName +{ + AS::MutexLocker l(__instanceLock__); + if (!ASObjectIsEqual(_debugName, debugName)) { + _debugName = [debugName copy]; + } +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + NSArray *children = self.children.count < 2 && self.child ? @[self.child] : self.children; + return [ASLayoutSpec asciiArtStringForChildren:children parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + NSMutableString *result = [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; + if (_debugName) { + [result appendFormat:@" (%@)", _debugName]; + } + return result; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__) + +@end + +#pragma mark - ASWrapperLayoutSpec + +@implementation ASWrapperLayoutSpec + ++ (instancetype)wrapperWithLayoutElement:(id)layoutElement NS_RETURNS_RETAINED +{ + return [[self alloc] initWithLayoutElement:layoutElement]; +} + +- (instancetype)initWithLayoutElement:(id)layoutElement +{ + self = [super init]; + if (self) { + self.child = layoutElement; + } + return self; +} + ++ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements NS_RETURNS_RETAINED +{ + return [[self alloc] initWithLayoutElements:layoutElements]; +} + +- (instancetype)initWithLayoutElements:(NSArray> *)layoutElements +{ + self = [super init]; + if (self) { + self.children = layoutElements; + } + return self; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + NSArray *children = self.children; + const auto count = children.count; + ASLayout *rawSublayouts[count]; + int i = 0; + + CGSize size = constrainedSize.min; + for (id child in children) { + ASLayout *sublayout = [child layoutThatFits:constrainedSize parentSize:constrainedSize.max]; + sublayout.position = CGPointZero; + + size.width = MAX(size.width, sublayout.size.width); + size.height = MAX(size.height, sublayout.size.height); + + rawSublayouts[i++] = sublayout; + } + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:i]; + return [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; +} + +@end + +#pragma mark - ASLayoutSpec (Debugging) + +@implementation ASLayoutSpec (Debugging) + +#pragma mark - ASCII Art Helpers + ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction +{ + NSMutableArray *childStrings = [NSMutableArray array]; + for (id layoutChild in children) { + NSString *childString = [layoutChild asciiArtString]; + if (childString) { + [childStrings addObject:childString]; + } + } + if (direction == ASStackLayoutDirectionHorizontal) { + return [ASAsciiArtBoxCreator horizontalBoxStringForChildren:childStrings parent:parentName]; + } + return [ASAsciiArtBoxCreator verticalBoxStringForChildren:childStrings parent:parentName]; +} + ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName +{ + return [self asciiArtStringForChildren:children parentName:parentName direction:ASStackLayoutDirectionHorizontal]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASOverlayLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASOverlayLayoutSpec.h new file mode 100644 index 0000000000..a29a72b750 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASOverlayLayoutSpec.h @@ -0,0 +1,34 @@ +// +// ASOverlayLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + This layout spec lays out a single layoutElement child and then overlays a layoutElement object on top of it streched to its size + */ +@interface ASOverlayLayoutSpec : ASLayoutSpec + +/** + * Overlay layoutElement of this layout spec + */ +@property (nonatomic) id overlay; + +/** + * Creates and returns an ASOverlayLayoutSpec object with a given child and an layoutElement that act as overlay. + * + * @param child A child that is laid out to determine the size of this spec. + * @param overlay A layoutElement object that is laid out over the child. + */ ++ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASOverlayLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASOverlayLayoutSpec.mm new file mode 100644 index 0000000000..27ffbeb8c7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASOverlayLayoutSpec.mm @@ -0,0 +1,88 @@ +// +// ASOverlayLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +static NSUInteger const kUnderlayChildIndex = 0; +static NSUInteger const kOverlayChildIndex = 1; + +@implementation ASOverlayLayoutSpec + +#pragma mark - Class + ++ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay NS_RETURNS_RETAINED +{ + return [[self alloc] initWithChild:child overlay:overlay]; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithChild:(id)child overlay:(id)overlay +{ + if (!(self = [super init])) { + return nil; + } + self.child = child; + self.overlay = overlay; + return self; +} + +#pragma mark - Setter / Getter + +- (void)setChild:(id)child +{ + ASDisplayNodeAssertNotNil(child, @"Child that will be overlayed on shouldn't be nil"); + [super setChild:child atIndex:kUnderlayChildIndex]; +} + +- (id)child +{ + return [super childAtIndex:kUnderlayChildIndex]; +} + +- (void)setOverlay:(id)overlay +{ + ASDisplayNodeAssertNotNil(overlay, @"Overlay cannot be nil"); + [super setChild:overlay atIndex:kOverlayChildIndex]; +} + +- (id)overlay +{ + return [super childAtIndex:kOverlayChildIndex]; +} + +#pragma mark - ASLayoutSpec + +/** + First layout the contents, then fit the overlay on top of it. + */ +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASLayout *contentsLayout = [self.child layoutThatFits:constrainedSize parentSize:parentSize]; + contentsLayout.position = CGPointZero; + ASLayout *rawSublayouts[2]; + int i = 0; + rawSublayouts[i++] = contentsLayout; + if (self.overlay) { + ASLayout *overlayLayout = [self.overlay layoutThatFits:ASSizeRangeMake(contentsLayout.size) + parentSize:contentsLayout.size]; + overlayLayout.position = CGPointZero; + rawSublayouts[i++] = overlayLayout; + } + + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:i]; + return [ASLayout layoutWithLayoutElement:self size:contentsLayout.size sublayouts:sublayouts]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASRatioLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASRatioLayoutSpec.h new file mode 100644 index 0000000000..b70e6fe7fb --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASRatioLayoutSpec.h @@ -0,0 +1,42 @@ +// +// ASRatioLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASLayoutElement; + +/** + Ratio layout spec + For when the content should respect a certain inherent ratio but can be scaled (think photos or videos) + The ratio passed is the ratio of height / width you expect + + For a ratio 0.5, the spec will have a flat rectangle shape + _ _ _ _ + | | + |_ _ _ _| + + For a ratio 2.0, the spec will be twice as tall as it is wide + _ _ + | | + | | + | | + |_ _| + + **/ +@interface ASRatioLayoutSpec : ASLayoutSpec + +@property (nonatomic) CGFloat ratio; + ++ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASRatioLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASRatioLayoutSpec.mm new file mode 100644 index 0000000000..1580b2a60c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASRatioLayoutSpec.mm @@ -0,0 +1,103 @@ +// +// ASRatioLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +#import + +#import +#import + +#pragma mark - ASRatioLayoutSpec + +@implementation ASRatioLayoutSpec +{ + CGFloat _ratio; +} + +#pragma mark - Lifecycle + ++ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child NS_RETURNS_RETAINED +{ + return [[self alloc] initWithRatio:ratio child:child]; +} + +- (instancetype)initWithRatio:(CGFloat)ratio child:(id)child; +{ + if (!(self = [super init])) { + return nil; + } + + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + ASDisplayNodeAssert(ratio > 0, @"Ratio should be strictly positive, but received %f", ratio); + _ratio = ratio; + self.child = child; + + return self; +} + +#pragma mark - Setter / Getter + +- (void)setRatio:(CGFloat)ratio +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _ratio = ratio; +} + +#pragma mark - ASLayoutElement + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + std::vector sizeOptions; + + if (ASPointsValidForSize(constrainedSize.max.width)) { + sizeOptions.push_back(ASSizeRangeClamp(constrainedSize, { + constrainedSize.max.width, + ASFloorPixelValue(_ratio * constrainedSize.max.width) + })); + } + + if (ASPointsValidForSize(constrainedSize.max.height)) { + sizeOptions.push_back(ASSizeRangeClamp(constrainedSize, { + ASFloorPixelValue(constrainedSize.max.height / _ratio), + constrainedSize.max.height + })); + } + + // Choose the size closest to the desired ratio. + const auto &bestSize = std::max_element(sizeOptions.begin(), sizeOptions.end(), [&](const CGSize &a, const CGSize &b){ + return std::fabs((a.height / a.width) - _ratio) > std::fabs((b.height / b.width) - _ratio); + }); + + // If there is no max size in *either* dimension, we can't apply the ratio, so just pass our size range through. + const ASSizeRange childRange = (bestSize == sizeOptions.end()) ? constrainedSize : ASSizeRangeIntersect(constrainedSize, ASSizeRangeMake(*bestSize, *bestSize)); + const CGSize parentSize = (bestSize == sizeOptions.end()) ? ASLayoutElementParentSizeUndefined : *bestSize; + ASLayout *sublayout = [self.child layoutThatFits:childRange parentSize:parentSize]; + sublayout.position = CGPointZero; + return [ASLayout layoutWithLayoutElement:self size:sublayout.size sublayouts:@[sublayout]]; +} + +@end + +#pragma mark - ASRatioLayoutSpec (Debugging) + +@implementation ASRatioLayoutSpec (Debugging) + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtName +{ + return [NSString stringWithFormat:@"%@ (%.1f)", NSStringFromClass([self class]), self.ratio]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASRelativeLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASRelativeLayoutSpec.h new file mode 100644 index 0000000000..9b260d9ccc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASRelativeLayoutSpec.h @@ -0,0 +1,87 @@ +// +// ASRelativeLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * How the child is positioned within the spec. + * + * The default option will position the child at point 0. + * Swift: use [] for the default behavior. + */ +typedef NS_ENUM(NSUInteger, ASRelativeLayoutSpecPosition) { + /** The child is positioned at point 0 */ + ASRelativeLayoutSpecPositionNone = 0, + /** The child is positioned at point 0 relatively to the layout axis (ie left / top most) */ + ASRelativeLayoutSpecPositionStart = 1, + /** The child is centered along the specified axis */ + ASRelativeLayoutSpecPositionCenter = 2, + /** The child is positioned at the maximum point of the layout axis (ie right / bottom most) */ + ASRelativeLayoutSpecPositionEnd = 3, +}; + +/** + * How much space the spec will take up. + * + * The default option will allow the spec to take up the maximum size possible. + * Swift: use [] for the default behavior. + */ +typedef NS_OPTIONS(NSUInteger, ASRelativeLayoutSpecSizingOption) { + /** The spec will take up the maximum size possible */ + ASRelativeLayoutSpecSizingOptionDefault, + /** The spec will take up the minimum size possible along the X axis */ + ASRelativeLayoutSpecSizingOptionMinimumWidth = 1 << 0, + /** The spec will take up the minimum size possible along the Y axis */ + ASRelativeLayoutSpecSizingOptionMinimumHeight = 1 << 1, + /** Convenience option to take up the minimum size along both the X and Y axis */ + ASRelativeLayoutSpecSizingOptionMinimumSize = ASRelativeLayoutSpecSizingOptionMinimumWidth | ASRelativeLayoutSpecSizingOptionMinimumHeight, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** Lays out a single layoutElement child and positions it within the layout bounds according to vertical and horizontal positional specifiers. + * Can position the child at any of the 4 corners, or the middle of any of the 4 edges, as well as the center - similar to "9-part" image areas. + */ +@interface ASRelativeLayoutSpec : ASLayoutSpec + +// You may create a spec with alloc / init, then set any non-default properties; or use a convenience initialize that accepts all properties. +@property (nonatomic) ASRelativeLayoutSpecPosition horizontalPosition; +@property (nonatomic) ASRelativeLayoutSpecPosition verticalPosition; +@property (nonatomic) ASRelativeLayoutSpecSizingOption sizingOption; + +/*! + * @discussion convenience constructor for a ASRelativeLayoutSpec + * @param horizontalPosition how to position the item on the horizontal (x) axis + * @param verticalPosition how to position the item on the vertical (y) axis + * @param sizingOption how much size to take up + * @param child the child to layout + * @return a configured ASRelativeLayoutSpec + */ ++ (instancetype)relativePositionLayoutSpecWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition + verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition + sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption + child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/*! + * @discussion convenience initializer for a ASRelativeLayoutSpec + * @param horizontalPosition how to position the item on the horizontal (x) axis + * @param verticalPosition how to position the item on the vertical (y) axis + * @param sizingOption how much size to take up + * @param child the child to layout + * @return a configured ASRelativeLayoutSpec + */ +- (instancetype)initWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition + verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition + sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption + child:(id)child; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASRelativeLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASRelativeLayoutSpec.mm new file mode 100644 index 0000000000..f096ce51d4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASRelativeLayoutSpec.mm @@ -0,0 +1,104 @@ +// +// ASRelativeLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +@implementation ASRelativeLayoutSpec + +- (instancetype)initWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption child:(id)child +{ + if (!(self = [super init])) { + return nil; + } + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + _horizontalPosition = horizontalPosition; + _verticalPosition = verticalPosition; + _sizingOption = sizingOption; + [self setChild:child]; + return self; +} + ++ (instancetype)relativePositionLayoutSpecWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption child:(id)child NS_RETURNS_RETAINED +{ + return [[self alloc] initWithHorizontalPosition:horizontalPosition verticalPosition:verticalPosition sizingOption:sizingOption child:child]; +} + +- (void)setHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _horizontalPosition = horizontalPosition; +} + +- (void)setVerticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition { + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _verticalPosition = verticalPosition; +} + +- (void)setSizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _sizingOption = sizingOption; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + // If we have a finite size in any direction, pass this so that the child can resolve percentages against it. + // Otherwise pass ASLayoutElementParentDimensionUndefined as the size will depend on the content + CGSize size = { + ASPointsValidForSize(constrainedSize.max.width) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.width, + ASPointsValidForSize(constrainedSize.max.height) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.height + }; + + // Layout the child + const CGSize minChildSize = { + (_horizontalPosition != ASRelativeLayoutSpecPositionNone) ? 0 : constrainedSize.min.width, + (_verticalPosition != ASRelativeLayoutSpecPositionNone) ? 0 : constrainedSize.min.height, + }; + ASLayout *sublayout = [self.child layoutThatFits:ASSizeRangeMake(minChildSize, constrainedSize.max) parentSize:size]; + + // If we have an undetermined height or width, use the child size to define the layout size + size = ASSizeRangeClamp(constrainedSize, { + isfinite(size.width) == NO ? sublayout.size.width : size.width, + isfinite(size.height) == NO ? sublayout.size.height : size.height + }); + + // If minimum size options are set, attempt to shrink the size to the size of the child + size = ASSizeRangeClamp(constrainedSize, { + MIN(size.width, (_sizingOption & ASRelativeLayoutSpecSizingOptionMinimumWidth) != 0 ? sublayout.size.width : size.width), + MIN(size.height, (_sizingOption & ASRelativeLayoutSpecSizingOptionMinimumHeight) != 0 ? sublayout.size.height : size.height) + }); + + // Compute the position for the child on each axis according to layout parameters + CGFloat xPosition = [self proportionOfAxisForAxisPosition:_horizontalPosition]; + CGFloat yPosition = [self proportionOfAxisForAxisPosition:_verticalPosition]; + + sublayout.position = { + ASRoundPixelValue((size.width - sublayout.size.width) * xPosition), + ASRoundPixelValue((size.height - sublayout.size.height) * yPosition) + }; + + return [ASLayout layoutWithLayoutElement:self size:size sublayouts:@[sublayout]]; +} + +- (CGFloat)proportionOfAxisForAxisPosition:(ASRelativeLayoutSpecPosition)position +{ + if (position == ASRelativeLayoutSpecPositionCenter) { + return 0.5f; + } else if (position == ASRelativeLayoutSpecPositionEnd) { + return 1.0f; + } else { + return 0.0f; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutDefines.h b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutDefines.h new file mode 100644 index 0000000000..8a86be1b5a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutDefines.h @@ -0,0 +1,142 @@ +// +// ASStackLayoutDefines.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** The direction children are stacked in */ +typedef NS_ENUM(NSUInteger, ASStackLayoutDirection) { + /** Children are stacked vertically */ + ASStackLayoutDirectionVertical, + /** Children are stacked horizontally */ + ASStackLayoutDirectionHorizontal, +}; + +/** If no children are flexible, how should this spec justify its children in the available space? */ +typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent) { + /** + On overflow, children overflow out of this spec's bounds on the right/bottom side. + On underflow, children are left/top-aligned within this spec's bounds. + */ + ASStackLayoutJustifyContentStart, + /** + On overflow, children are centered and overflow on both sides. + On underflow, children are centered within this spec's bounds in the stacking direction. + */ + ASStackLayoutJustifyContentCenter, + /** + On overflow, children overflow out of this spec's bounds on the left/top side. + On underflow, children are right/bottom-aligned within this spec's bounds. + */ + ASStackLayoutJustifyContentEnd, + /** + On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentStart. + Otherwise, the starting edge of the first child is at the starting edge of the stack, + the ending edge of the last child is at the ending edge of the stack, and the remaining children + are distributed so that the spacing between any two adjacent ones is the same. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last 2 children). + */ + ASStackLayoutJustifyContentSpaceBetween, + /** + On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentCenter. + Otherwise, children are distributed such that the spacing between any two adjacent ones is the same, + and the spacing between the first/last child and the stack edges is half the size of the spacing between children. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last child and the stack ending edge). + */ + ASStackLayoutJustifyContentSpaceAround +}; + +/** Orientation of children along cross axis */ +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignItems) { + /** Align children to start of cross axis */ + ASStackLayoutAlignItemsStart, + /** Align children with end of cross axis */ + ASStackLayoutAlignItemsEnd, + /** Center children on cross axis */ + ASStackLayoutAlignItemsCenter, + /** Expand children to fill cross axis */ + ASStackLayoutAlignItemsStretch, + /** Children align to their first baseline. Only available for horizontal stack spec */ + ASStackLayoutAlignItemsBaselineFirst, + /** Children align to their last baseline. Only available for horizontal stack spec */ + ASStackLayoutAlignItemsBaselineLast, + ASStackLayoutAlignItemsNotSet +}; + +/** + Each child may override their parent stack's cross axis alignment. + @see ASStackLayoutAlignItems + */ +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { + /** Inherit alignment value from containing stack. */ + ASStackLayoutAlignSelfAuto, + /** Align to start of cross axis */ + ASStackLayoutAlignSelfStart, + /** Align with end of cross axis */ + ASStackLayoutAlignSelfEnd, + /** Center on cross axis */ + ASStackLayoutAlignSelfCenter, + /** Expand to fill cross axis */ + ASStackLayoutAlignSelfStretch, +}; + +/** Whether children are stacked into a single or multiple lines. */ +typedef NS_ENUM(NSUInteger, ASStackLayoutFlexWrap) { + ASStackLayoutFlexWrapNoWrap, + ASStackLayoutFlexWrapWrap, +}; + +/** Orientation of lines along cross axis if there are multiple lines. */ +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignContent) { + ASStackLayoutAlignContentStart, + ASStackLayoutAlignContentCenter, + ASStackLayoutAlignContentEnd, + ASStackLayoutAlignContentSpaceBetween, + ASStackLayoutAlignContentSpaceAround, + ASStackLayoutAlignContentStretch, +}; + +/** Orientation of children along horizontal axis */ +typedef NS_ENUM(NSUInteger, ASHorizontalAlignment) { + /** No alignment specified. Default value */ + ASHorizontalAlignmentNone, + /** Left aligned */ + ASHorizontalAlignmentLeft, + /** Center aligned */ + ASHorizontalAlignmentMiddle, + /** Right aligned */ + ASHorizontalAlignmentRight, + + // After 2.0 has landed, we'll add ASDISPLAYNODE_DEPRECATED here - for now, avoid triggering errors for projects with -Werror + /** @deprecated Use ASHorizontalAlignmentLeft instead */ + ASAlignmentLeft ASDISPLAYNODE_DEPRECATED = ASHorizontalAlignmentLeft, + /** @deprecated Use ASHorizontalAlignmentMiddle instead */ + ASAlignmentMiddle ASDISPLAYNODE_DEPRECATED = ASHorizontalAlignmentMiddle, + /** @deprecated Use ASHorizontalAlignmentRight instead */ + ASAlignmentRight ASDISPLAYNODE_DEPRECATED = ASHorizontalAlignmentRight, +}; + +/** Orientation of children along vertical axis */ +typedef NS_ENUM(NSUInteger, ASVerticalAlignment) { + /** No alignment specified. Default value */ + ASVerticalAlignmentNone, + /** Top aligned */ + ASVerticalAlignmentTop, + /** Center aligned */ + ASVerticalAlignmentCenter, + /** Bottom aligned */ + ASVerticalAlignmentBottom, + + // After 2.0 has landed, we'll add ASDISPLAYNODE_DEPRECATED here - for now, avoid triggering errors for projects with -Werror + /** @deprecated Use ASVerticalAlignmentTop instead */ + ASAlignmentTop ASDISPLAYNODE_DEPRECATED = ASVerticalAlignmentTop, + /** @deprecated Use ASVerticalAlignmentCenter instead */ + ASAlignmentCenter ASDISPLAYNODE_DEPRECATED = ASVerticalAlignmentCenter, + /** @deprecated Use ASVerticalAlignmentBottom instead */ + ASAlignmentBottom ASDISPLAYNODE_DEPRECATED = ASVerticalAlignmentBottom, +}; diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutElement.h b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutElement.h new file mode 100644 index 0000000000..46b263b4ff --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutElement.h @@ -0,0 +1,73 @@ +// +// ASStackLayoutElement.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Layout options that can be defined for an ASLayoutElement being added to a ASStackLayoutSpec. + */ +@protocol ASStackLayoutElement + +/** + * @abstract Additional space to place before this object in the stacking direction. + * Used when attached to a stack layout. + */ +@property (nonatomic) CGFloat spacingBefore; + +/** + * @abstract Additional space to place after this object in the stacking direction. + * Used when attached to a stack layout. + */ +@property (nonatomic) CGFloat spacingAfter; + +/** + * @abstract If the sum of childrens' stack dimensions is less than the minimum size, how much should this component grow? + * This value represents the "flex grow factor" and determines how much this component should grow in relation to any + * other flexible children. + */ +@property (nonatomic) CGFloat flexGrow; + +/** + * @abstract If the sum of childrens' stack dimensions is greater than the maximum size, how much should this component shrink? + * This value represents the "flex shrink factor" and determines how much this component should shink in relation to + * other flexible children. + */ +@property (nonatomic) CGFloat flexShrink; + +/** + * @abstract Specifies the initial size in the stack dimension for this object. + * Defaults to ASDimensionAuto. + * Used when attached to a stack layout. + */ +@property (nonatomic) ASDimension flexBasis; + +/** + * @abstract Orientation of the object along cross axis, overriding alignItems. + * Defaults to ASStackLayoutAlignSelfAuto. + * Used when attached to a stack layout. + */ +@property (nonatomic) ASStackLayoutAlignSelf alignSelf; + +/** + * @abstract Used for baseline alignment. The distance from the top of the object to its baseline. + */ +@property (nonatomic) CGFloat ascender; + +/** + * @abstract Used for baseline alignment. The distance from the baseline of the object to its bottom. + */ +@property (nonatomic) CGFloat descender; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutSpec.h b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutSpec.h new file mode 100644 index 0000000000..535758ac7b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutSpec.h @@ -0,0 +1,133 @@ +// +// ASStackLayoutSpec.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A simple layout spec that stacks a list of children vertically or horizontally. + + - All children are initially laid out with the an infinite available size in the stacking direction. + - In the other direction, this spec's constraint is passed. + - The children's sizes are summed in the stacking direction. + - If this sum is less than this spec's minimum size in stacking direction, children with flexGrow are flexed. + - If it is greater than this spec's maximum size in the stacking direction, children with flexShrink are flexed. + - If, even after flexing, the sum is still greater than this spec's maximum size in the stacking direction, + justifyContent determines how children are laid out. + + For example: + + - Suppose stacking direction is Vertical, min-width=100, max-width=300, min-height=200, max-height=500. + - All children are laid out with min-width=100, max-width=300, min-height=0, max-height=INFINITY. + - If the sum of the childrens' heights is less than 200, children with flexGrow are flexed larger. + - If the sum of the childrens' heights is greater than 500, children with flexShrink are flexed smaller. + Each child is shrunk by `((sum of heights) - 500)/(number of flexShrink-able children)`. + - If the sum of the childrens' heights is greater than 500 even after flexShrink-able children are flexed, + justifyContent determines how children are laid out. + */ +@interface ASStackLayoutSpec : ASLayoutSpec + +/** + Specifies the direction children are stacked in. If horizontalAlignment and verticalAlignment were set, + they will be resolved again, causing justifyContent and alignItems to be updated accordingly + */ +@property (nonatomic) ASStackLayoutDirection direction; +/** The amount of space between each child. */ +@property (nonatomic) CGFloat spacing; +/** + Specifies how children are aligned horizontally. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties + */ +@property (nonatomic) ASHorizontalAlignment horizontalAlignment; +/** + Specifies how children are aligned vertically. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties + */ +@property (nonatomic) ASVerticalAlignment verticalAlignment; +/** The amount of space between each child. Defaults to ASStackLayoutJustifyContentStart */ +@property (nonatomic) ASStackLayoutJustifyContent justifyContent; +/** Orientation of children along cross axis. Defaults to ASStackLayoutAlignItemsStretch */ +@property (nonatomic) ASStackLayoutAlignItems alignItems; +/** Whether children are stacked into a single or multiple lines. Defaults to single line (ASStackLayoutFlexWrapNoWrap) */ +@property (nonatomic) ASStackLayoutFlexWrap flexWrap; +/** Orientation of lines along cross axis if there are multiple lines. Defaults to ASStackLayoutAlignContentStart */ +@property (nonatomic) ASStackLayoutAlignContent alignContent; +/** If the stack spreads on multiple lines using flexWrap, the amount of space between lines. */ +@property (nonatomic) CGFloat lineSpacing; +/** Whether this stack can dispatch to other threads, regardless of which thread it's running on */ +@property (nonatomic, getter=isConcurrent) BOOL concurrent; + +- (instancetype)init; + +/** + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis + @param children ASLayoutElement children to be positioned. + */ ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis + @param flexWrap Whether children are stacked into a single or multiple lines + @param alignContent Orientation of lines along cross axis if there are multiple lines + @param children ASLayoutElement children to be positioned. + */ ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + flexWrap:(ASStackLayoutFlexWrap)flexWrap + alignContent:(ASStackLayoutAlignContent)alignContent + children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis + @param flexWrap Whether children are stacked into a single or multiple lines + @param alignContent Orientation of lines along cross axis if there are multiple lines + @param lineSpacing The spacing between lines + @param children ASLayoutElement children to be positioned. + */ ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + flexWrap:(ASStackLayoutFlexWrap)flexWrap + alignContent:(ASStackLayoutAlignContent)alignContent + lineSpacing:(CGFloat)lineSpacing + children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * @return A stack layout spec with direction of ASStackLayoutDirectionVertical + **/ ++ (instancetype)verticalStackLayoutSpec NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * @return A stack layout spec with direction of ASStackLayoutDirectionHorizontal + **/ ++ (instancetype)horizontalStackLayoutSpec NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutSpec.mm new file mode 100644 index 0000000000..c7c9f8dd21 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASStackLayoutSpec.mm @@ -0,0 +1,212 @@ +// +// ASStackLayoutSpec.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +@implementation ASStackLayoutSpec + +- (instancetype)init +{ + return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart lineSpacing:0.0 children:nil]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children NS_RETURNS_RETAINED +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart lineSpacing: 0.0 children:children]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children NS_RETURNS_RETAINED +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent lineSpacing:0.0 children:children]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing children:(NSArray> *)children NS_RETURNS_RETAINED +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent lineSpacing:lineSpacing children:children]; +} + ++ (instancetype)verticalStackLayoutSpec NS_RETURNS_RETAINED +{ + ASStackLayoutSpec *stackLayoutSpec = [[self alloc] init]; + stackLayoutSpec.direction = ASStackLayoutDirectionVertical; + return stackLayoutSpec; +} + ++ (instancetype)horizontalStackLayoutSpec NS_RETURNS_RETAINED +{ + ASStackLayoutSpec *stackLayoutSpec = [[self alloc] init]; + stackLayoutSpec.direction = ASStackLayoutDirectionHorizontal; + return stackLayoutSpec; +} + +- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing children:(NSArray *)children +{ + if (!(self = [super init])) { + return nil; + } + _direction = direction; + _spacing = spacing; + _horizontalAlignment = ASHorizontalAlignmentNone; + _verticalAlignment = ASVerticalAlignmentNone; + _alignItems = alignItems; + _justifyContent = justifyContent; + _flexWrap = flexWrap; + _alignContent = alignContent; + _lineSpacing = lineSpacing; + + [self setChildren:children]; + return self; +} + +- (void)setDirection:(ASStackLayoutDirection)direction +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_direction != direction) { + _direction = direction; + [self resolveHorizontalAlignment]; + [self resolveVerticalAlignment]; + } +} + +- (void)setHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_horizontalAlignment != horizontalAlignment) { + _horizontalAlignment = horizontalAlignment; + [self resolveHorizontalAlignment]; + } +} + +- (void)setVerticalAlignment:(ASVerticalAlignment)verticalAlignment +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_verticalAlignment != verticalAlignment) { + _verticalAlignment = verticalAlignment; + [self resolveVerticalAlignment]; + } +} + +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used"); + ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used"); + _alignItems = alignItems; +} + +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justifyContent +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used"); + ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used"); + _justifyContent = justifyContent; +} + +- (void)setSpacing:(CGFloat)spacing +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _spacing = spacing; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + NSArray *children = self.children; + if (children.count == 0) { + return [ASLayout layoutWithLayoutElement:self size:constrainedSize.min]; + } + + as_activity_scope_verbose(as_activity_create("Calculate stack layout", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASLayoutLog(), "Stack layout %@", self); + // Accessing the style and size property is pretty costly we create layout spec children we use to figure + // out the layout for each child + const auto stackChildren = AS::map(children, [&](const id child) -> ASStackLayoutSpecChild { + ASLayoutElementStyle *style = child.style; + return {child, style, style.size}; + }); + + const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .flexWrap = _flexWrap, .alignContent = _alignContent, .lineSpacing = _lineSpacing}; + + const auto unpositionedLayout = ASStackUnpositionedLayout::compute(stackChildren, style, constrainedSize, _concurrent); + const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, style, constrainedSize); + + if (style.direction == ASStackLayoutDirectionVertical) { + self.style.ascender = stackChildren.front().style.ascender; + self.style.descender = stackChildren.back().style.descender; + } + + ASLayout *rawSublayouts[positionedLayout.items.size()]; + int i = 0; + for (const auto &item : positionedLayout.items) { + rawSublayouts[i++] = item.layout; + } + + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:i]; + return [ASLayout layoutWithLayoutElement:self size:positionedLayout.size sublayouts:sublayouts]; +} + +- (void)resolveHorizontalAlignment +{ + if (_direction == ASStackLayoutDirectionHorizontal) { + _justifyContent = justifyContent(_horizontalAlignment, _justifyContent); + } else { + _alignItems = alignment(_horizontalAlignment, _alignItems); + } +} + +- (void)resolveVerticalAlignment +{ + if (_direction == ASStackLayoutDirectionHorizontal) { + _alignItems = alignment(_verticalAlignment, _alignItems); + } else { + _justifyContent = justifyContent(_verticalAlignment, _justifyContent); + } +} + +- (NSMutableArray *)propertiesForDescription +{ + auto result = [super propertiesForDescription]; + + // Add our direction + switch (self.direction) { + case ASStackLayoutDirectionVertical: + [result insertObject:@{ (id)kCFNull: @"vertical" } atIndex:0]; + break; + case ASStackLayoutDirectionHorizontal: + [result insertObject:@{ (id)kCFNull: @"horizontal" } atIndex:0]; + break; + } + + return result; +} + +@end + +@implementation ASStackLayoutSpec (Debugging) + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:self.children parentName:[self asciiArtName] direction:self.direction]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASYogaUtilities.h b/submodules/AsyncDisplayKit/Source/Layout/ASYogaUtilities.h new file mode 100644 index 0000000000..c3d1d8233c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASYogaUtilities.h @@ -0,0 +1,78 @@ +// +// ASYogaUtilities.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA /* YOGA */ + +#import +#import +#import + +// Should pass a string literal, not an NSString as the first argument to ASYogaLog +#define ASYogaLog(x, ...) as_log_verbose(ASLayoutLog(), x, ##__VA_ARGS__); + +@interface ASDisplayNode (YogaHelpers) + ++ (ASDisplayNode *)yogaNode; ++ (ASDisplayNode *)yogaSpacerNode; ++ (ASDisplayNode *)yogaVerticalStack; ++ (ASDisplayNode *)yogaHorizontalStack; + +@end + +// pre-order, depth-first +AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)); + +#pragma mark - Yoga Type Conversion Helpers + +AS_EXTERN YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems); +AS_EXTERN YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent); +AS_EXTERN YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf); +AS_EXTERN YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction); +AS_EXTERN float yogaFloatForCGFloat(CGFloat value); +AS_EXTERN float yogaDimensionToPoints(ASDimension dimension); +AS_EXTERN float yogaDimensionToPercent(ASDimension dimension); +AS_EXTERN ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets); + +AS_EXTERN void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id layoutElement); +AS_EXTERN float ASLayoutElementYogaBaselineFunc(YGNodeRef yogaNode, const float width, const float height); +AS_EXTERN YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, + float width, YGMeasureMode widthMode, + float height, YGMeasureMode heightMode); + +#pragma mark - Yoga Style Setter Helpers + +#define YGNODE_STYLE_SET_DIMENSION(yogaNode, property, dimension) \ + if (dimension.unit == ASDimensionUnitPoints) { \ + YGNodeStyleSet##property(yogaNode, yogaDimensionToPoints(dimension)); \ + } else if (dimension.unit == ASDimensionUnitFraction) { \ + YGNodeStyleSet##property##Percent(yogaNode, yogaDimensionToPercent(dimension)); \ + } else { \ + YGNodeStyleSet##property(yogaNode, YGUndefined); \ + }\ + +#define YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, property, dimension, edge) \ + if (dimension.unit == ASDimensionUnitPoints) { \ + YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \ + } else if (dimension.unit == ASDimensionUnitFraction) { \ + YGNodeStyleSet##property##Percent(yogaNode, edge, yogaDimensionToPercent(dimension)); \ + } else { \ + YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ + } \ + +#define YGNODE_STYLE_SET_FLOAT_WITH_EDGE(yogaNode, property, dimension, edge) \ + if (dimension.unit == ASDimensionUnitPoints) { \ + YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \ + } else if (dimension.unit == ASDimensionUnitFraction) { \ + ASDisplayNodeAssert(NO, @"Unexpected Fraction value in applying ##property## values to YGNode"); \ + } else { \ + YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ + } \ + +#endif /* YOGA */ diff --git a/submodules/AsyncDisplayKit/Source/Layout/ASYogaUtilities.mm b/submodules/AsyncDisplayKit/Source/Layout/ASYogaUtilities.mm new file mode 100644 index 0000000000..f0602ed4bd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Layout/ASYogaUtilities.mm @@ -0,0 +1,240 @@ +// +// ASYogaUtilities.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#if YOGA /* YOGA */ + +@implementation ASDisplayNode (YogaHelpers) + ++ (ASDisplayNode *)yogaNode +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + [node.style yogaNodeCreateIfNeeded]; + return node; +} + ++ (ASDisplayNode *)yogaSpacerNode +{ + ASDisplayNode *node = [ASDisplayNode yogaNode]; + node.style.flexGrow = 1.0f; + return node; +} + ++ (ASDisplayNode *)yogaVerticalStack +{ + ASDisplayNode *node = [self yogaNode]; + node.style.flexDirection = ASStackLayoutDirectionVertical; + return node; +} + ++ (ASDisplayNode *)yogaHorizontalStack +{ + ASDisplayNode *node = [self yogaNode]; + node.style.flexDirection = ASStackLayoutDirectionHorizontal; + return node; +} + +@end + +void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) +{ + if (node == nil) { + return; + } + block(node); + // We use the accessor here despite the copy, because the block may modify the yoga tree e.g. + // replacing a node. + for (ASDisplayNode *child in node.yogaChildren) { + ASDisplayNodePerformBlockOnEveryYogaChild(child, block); + } +} + +#pragma mark - Yoga Type Conversion Helpers + +YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems) +{ + switch (alignItems) { + case ASStackLayoutAlignItemsNotSet: return YGAlignAuto; + case ASStackLayoutAlignItemsStart: return YGAlignFlexStart; + case ASStackLayoutAlignItemsEnd: return YGAlignFlexEnd; + case ASStackLayoutAlignItemsCenter: return YGAlignCenter; + case ASStackLayoutAlignItemsStretch: return YGAlignStretch; + case ASStackLayoutAlignItemsBaselineFirst: return YGAlignBaseline; + // FIXME: WARNING, Yoga does not currently support last-baseline item alignment. + case ASStackLayoutAlignItemsBaselineLast: return YGAlignBaseline; + } +} + +YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent) +{ + switch (justifyContent) { + case ASStackLayoutJustifyContentStart: return YGJustifyFlexStart; + case ASStackLayoutJustifyContentCenter: return YGJustifyCenter; + case ASStackLayoutJustifyContentEnd: return YGJustifyFlexEnd; + case ASStackLayoutJustifyContentSpaceBetween: return YGJustifySpaceBetween; + case ASStackLayoutJustifyContentSpaceAround: return YGJustifySpaceAround; + } +} + +YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf) +{ + switch (alignSelf) { + case ASStackLayoutAlignSelfStart: return YGAlignFlexStart; + case ASStackLayoutAlignSelfCenter: return YGAlignCenter; + case ASStackLayoutAlignSelfEnd: return YGAlignFlexEnd; + case ASStackLayoutAlignSelfStretch: return YGAlignStretch; + case ASStackLayoutAlignSelfAuto: return YGAlignAuto; + } +} + +YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction) +{ + return direction == ASStackLayoutDirectionVertical ? YGFlexDirectionColumn : YGFlexDirectionRow; +} + +float yogaFloatForCGFloat(CGFloat value) +{ + if (value < CGFLOAT_MAX / 2) { + return value; + } else { + return YGUndefined; + } +} + +float cgFloatForYogaFloat(float yogaFloat) +{ + return (yogaFloat == YGUndefined) ? CGFLOAT_MAX : yogaFloat; +} + +float yogaDimensionToPoints(ASDimension dimension) +{ + ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitPoints, + @"Dimensions should not be type Fraction for this method: %f", dimension.value); + return yogaFloatForCGFloat(dimension.value); +} + +float yogaDimensionToPercent(ASDimension dimension) +{ + ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitFraction, + @"Dimensions should not be type Points for this method: %f", dimension.value); + return 100.0 * yogaFloatForCGFloat(dimension.value); + +} + +ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets) +{ + switch (edge) { + case YGEdgeLeft: return insets.left; + case YGEdgeTop: return insets.top; + case YGEdgeRight: return insets.right; + case YGEdgeBottom: return insets.bottom; + case YGEdgeStart: return insets.start; + case YGEdgeEnd: return insets.end; + case YGEdgeHorizontal: return insets.horizontal; + case YGEdgeVertical: return insets.vertical; + case YGEdgeAll: return insets.all; + default: ASDisplayNodeCAssert(NO, @"YGEdge other than ASEdgeInsets is not supported."); + return ASDimensionAuto; + } +} + +void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id layoutElement) +{ + if (yogaNode == NULL) { + return; + } + + BOOL shouldHaveMeasureFunc = [layoutElement implementsLayoutMethod]; + // How expensive is it to set a baselineFunc on all (leaf) nodes? + BOOL shouldHaveBaselineFunc = YES; + + if (layoutElement != nil) { + if (shouldHaveMeasureFunc || shouldHaveBaselineFunc) { + // Retain the Context object. This must be explicitly released with a + // __bridge_transfer - YGNodeFree() is not sufficient. + YGNodeSetContext(yogaNode, (__bridge_retained void *)layoutElement); + } + if (shouldHaveMeasureFunc) { + YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); + } + if (shouldHaveBaselineFunc) { + YGNodeSetBaselineFunc(yogaNode, &ASLayoutElementYogaBaselineFunc); + } + ASDisplayNodeCAssert(YGNodeGetContext(yogaNode) == (__bridge void *)layoutElement, + @"Yoga node context should contain layoutElement: %@", layoutElement); + } else { + // If we lack any of the conditions above, and currently have a measureFn/baselineFn/context, + // get rid of it. + // Release the __bridge_retained Context object. + __unused id element = (__bridge_transfer id)YGNodeGetContext(yogaNode); + YGNodeSetContext(yogaNode, NULL); + YGNodeSetMeasureFunc(yogaNode, NULL); + YGNodeSetBaselineFunc(yogaNode, NULL); + } +} + +float ASLayoutElementYogaBaselineFunc(YGNodeRef yogaNode, const float width, const float height) +{ + id layoutElement = (__bridge id)YGNodeGetContext(yogaNode); + ASDisplayNodeCAssert([layoutElement conformsToProtocol:@protocol(ASLayoutElement)], + @"Yoga context must be "); + + ASDisplayNode *displayNode = ASDynamicCast(layoutElement, ASDisplayNode); + + switch (displayNode.style.parentAlignStyle) { + case ASStackLayoutAlignItemsBaselineFirst: + return layoutElement.style.ascender; + case ASStackLayoutAlignItemsBaselineLast: + return height + layoutElement.style.descender; + default: + return 0; + } +} + +YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode, + float height, YGMeasureMode heightMode) +{ + id layoutElement = (__bridge id )YGNodeGetContext(yogaNode); + ASDisplayNodeCAssert([layoutElement conformsToProtocol:@protocol(ASLayoutElement)], @"Yoga context must be "); + + width = cgFloatForYogaFloat(width); + height = cgFloatForYogaFloat(height); + + ASSizeRange sizeRange; + sizeRange.min = CGSizeZero; + sizeRange.max = CGSizeMake(width, height); + if (widthMode == YGMeasureModeExactly) { + sizeRange.min.width = sizeRange.max.width; + } else { + // Mode is (YGMeasureModeAtMost | YGMeasureModeUndefined) + ASDimension minWidth = layoutElement.style.minWidth; + sizeRange.min.width = (minWidth.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minWidth) : 0.0); + } + if (heightMode == YGMeasureModeExactly) { + sizeRange.min.height = sizeRange.max.height; + } else { + // Mode is (YGMeasureModeAtMost | YGMeasureModeUndefined) + ASDimension minHeight = layoutElement.style.minHeight; + sizeRange.min.height = (minHeight.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minHeight) : 0.0); + } + + ASDisplayNodeCAssert(isnan(sizeRange.min.width) == NO && isnan(sizeRange.min.height) == NO, @"Yoga size range for measurement should not have NaN in minimum"); + if (isnan(sizeRange.max.width)) { + sizeRange.max.width = CGFLOAT_MAX; + } + if (isnan(sizeRange.max.height)) { + sizeRange.max.height = CGFLOAT_MAX; + } + + CGSize size = [[layoutElement layoutThatFits:sizeRange] size]; + return (YGSize){ .width = (float)size.width, .height = (float)size.height }; +} + +#endif /* YOGA */ diff --git a/submodules/AsyncDisplayKit/Source/Private/ASAbstractLayoutController+FrameworkPrivate.h b/submodules/AsyncDisplayKit/Source/Private/ASAbstractLayoutController+FrameworkPrivate.h new file mode 100644 index 0000000000..f836fa0b84 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASAbstractLayoutController+FrameworkPrivate.h @@ -0,0 +1,21 @@ +// +// ASAbstractLayoutController+FrameworkPrivate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#include + +@interface ASAbstractLayoutController (FrameworkPrivate) + ++ (std::vector>)defaultTuningParameters; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASBasicImageDownloaderInternal.h b/submodules/AsyncDisplayKit/Source/Private/ASBasicImageDownloaderInternal.h new file mode 100644 index 0000000000..ba06e0bc81 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASBasicImageDownloaderInternal.h @@ -0,0 +1,20 @@ +// +// ASBasicImageDownloaderInternal.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +@interface ASBasicImageDownloaderContext : NSObject + ++ (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL; + +@property (nonatomic, readonly) NSURL *URL; +@property (nonatomic, weak) NSURLSessionTask *sessionTask; + +- (BOOL)isCancelled; +- (void)cancel; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASBatchFetching.h b/submodules/AsyncDisplayKit/Source/Private/ASBatchFetching.h new file mode 100644 index 0000000000..0358bf12cc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASBatchFetching.h @@ -0,0 +1,77 @@ +// +// ASBatchFetching.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASBatchContext; +@protocol ASBatchFetchingDelegate; + +@protocol ASBatchFetchingScrollView + +- (BOOL)canBatchFetch; +- (ASBatchContext *)batchContext; +- (CGFloat)leadingScreensForBatching; +- (nullable id)batchFetchingDelegate; + +@end + +/** + @abstract Determine if batch fetching should begin based on the state of the parameters. + @discussion This method is broken into a category for unit testing purposes and should be used with the ASTableView and + * ASCollectionView batch fetching API. + @param scrollView The scroll view that in-flight fetches are happening. + @param scrollDirection The current scrolling direction of the scroll view. + @param scrollableDirections The possible scrolling directions of the scroll view. + @param contentOffset The offset that the scrollview will scroll to. + @param velocity The velocity of the scroll view (in points) at the moment the touch was released. + @return Whether or not the current state should proceed with batch fetching. + */ +AS_EXTERN BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView *scrollView, + ASScrollDirection scrollDirection, + ASScrollDirection scrollableDirections, + CGPoint contentOffset, + CGPoint velocity); + + +/** + @abstract Determine if batch fetching should begin based on the state of the parameters. + @param context The batch fetching context that contains knowledge about in-flight fetches. + @param scrollDirection The current scrolling direction of the scroll view. + @param scrollableDirections The possible scrolling directions of the scroll view. + @param bounds The bounds of the scrollview. + @param contentSize The content size of the scrollview. + @param targetOffset The offset that the scrollview will scroll to. + @param leadingScreens How many screens in the remaining distance will trigger batch fetching. + @param visible Whether the view is visible or not. + @param velocity The velocity of the scroll view (in points) at the moment the touch was released. + @param delegate The delegate to be consulted if needed. + @return Whether or not the current state should proceed with batch fetching. + @discussion This method is broken into a category for unit testing purposes and should be used with the ASTableView and + * ASCollectionView batch fetching API. + */ +AS_EXTERN BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, + ASScrollDirection scrollDirection, + ASScrollDirection scrollableDirections, + CGRect bounds, + CGSize contentSize, + CGPoint targetOffset, + CGFloat leadingScreens, + BOOL visible, + CGPoint velocity, + _Nullable id delegate); + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASBatchFetching.mm b/submodules/AsyncDisplayKit/Source/Private/ASBatchFetching.mm new file mode 100644 index 0000000000..44b31ed77d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASBatchFetching.mm @@ -0,0 +1,102 @@ +// +// ASBatchFetching.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import +#import +#import + +BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView *scrollView, + ASScrollDirection scrollDirection, + ASScrollDirection scrollableDirections, + CGPoint contentOffset, + CGPoint velocity) +{ + // Don't fetch if the scroll view does not allow + if (![scrollView canBatchFetch]) { + return NO; + } + + // Check if we should batch fetch + ASBatchContext *context = scrollView.batchContext; + CGRect bounds = scrollView.bounds; + CGSize contentSize = scrollView.contentSize; + CGFloat leadingScreens = scrollView.leadingScreensForBatching; + id delegate = scrollView.batchFetchingDelegate; + BOOL visible = (scrollView.window != nil); + return ASDisplayShouldFetchBatchForContext(context, scrollDirection, scrollableDirections, bounds, contentSize, contentOffset, leadingScreens, visible, velocity, delegate); +} + +BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, + ASScrollDirection scrollDirection, + ASScrollDirection scrollableDirections, + CGRect bounds, + CGSize contentSize, + CGPoint targetOffset, + CGFloat leadingScreens, + BOOL visible, + CGPoint velocity, + id delegate) +{ + // Do not allow fetching if a batch is already in-flight and hasn't been completed or cancelled + if ([context isFetching]) { + return NO; + } + + // No fetching for null states + if (leadingScreens <= 0.0 || CGRectIsEmpty(bounds)) { + return NO; + } + + + CGFloat viewLength, offset, contentLength, velocityLength; + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { + viewLength = bounds.size.height; + offset = targetOffset.y; + contentLength = contentSize.height; + velocityLength = velocity.y; + } else { // horizontal / right + viewLength = bounds.size.width; + offset = targetOffset.x; + contentLength = contentSize.width; + velocityLength = velocity.x; + } + + BOOL hasSmallContent = contentLength < viewLength; + if (hasSmallContent) { + return YES; + } + + // If we are not visible, but we do have enough content to fill visible area, + // don't batch fetch. + if (visible == NO) { + return NO; + } + + // If they are scrolling toward the head of content, don't batch fetch. + BOOL isScrollingTowardHead = (ASScrollDirectionContainsUp(scrollDirection) || ASScrollDirectionContainsLeft(scrollDirection)); + if (isScrollingTowardHead) { + return NO; + } + + CGFloat triggerDistance = viewLength * leadingScreens; + CGFloat remainingDistance = contentLength - viewLength - offset; + BOOL result = remainingDistance <= triggerDistance; + + if (delegate != nil && velocityLength > 0.0) { + // Don't need to get absolute value of remaining time + // because both remainingDistance and velocityLength are positive when scrolling toward tail + NSTimeInterval remainingTime = remainingDistance / (velocityLength * 1000); + result = [delegate shouldFetchBatchWithRemainingTime:remainingTime hint:result]; + } + + return result; +} + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCellNode+Internal.h b/submodules/AsyncDisplayKit/Source/Private/ASCellNode+Internal.h new file mode 100644 index 0000000000..4c3e63d652 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCellNode+Internal.h @@ -0,0 +1,79 @@ +// +// ASCellNode+Internal.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionElement; + +@protocol ASCellNodeInteractionDelegate + +/** + * Notifies the delegate that a specified cell node invalidates it's size what could result into a size change. + * + * @param node A node informing the delegate about the relayout. + */ +- (void)nodeDidInvalidateSize:(ASCellNode *)node; + +/* + * Methods to be called whenever the selection or highlight state changes + * on ASCellNode. UIKit internally stores these values to update reusable cells. + */ + +- (void)nodeSelectedStateDidChange:(ASCellNode *)node; +- (void)nodeHighlightedStateDidChange:(ASCellNode *)node; + +@end + +@interface ASCellNode () + +@property (nonatomic, weak) id interactionDelegate; + +/* + * Back-pointer to the containing scrollView instance, set only for visible cells. Used for Cell Visibility Event callbacks. + */ +@property (nonatomic, weak) UIScrollView *scrollView; + +- (void)__setSelectedFromUIKit:(BOOL)selected; +- (void)__setHighlightedFromUIKit:(BOOL)highlighted; + +/** + * @note This could be declared @c copy, but since this is only settable internally, we can ensure + * that it's always safe simply to retain it, and copy if needed. Since @c UICollectionViewLayoutAttributes + * is always mutable, @c copy is never "free" like it is for e.g. NSString. + */ +@property (nullable, nonatomic) UICollectionViewLayoutAttributes *layoutAttributes; + +@property (weak, nullable) ASCollectionElement *collectionElement; + +@property (weak, nullable) id owningNode; + +@property (nonatomic, readonly) BOOL shouldUseUIKitCell; + +@end + +@class ASWrapperCellNode; + +typedef CGSize (^ASSizeForItemBlock)(ASWrapperCellNode *node, CGSize collectionSize); +typedef UICollectionViewCell * _Nonnull(^ASCellForItemBlock)(ASWrapperCellNode *node); +typedef UICollectionReusableView * _Nonnull(^ASViewForSupplementaryBlock)(ASWrapperCellNode *node); + +@interface ASWrapperCellNode : ASCellNode + +@property (nonatomic, readonly) ASSizeForItemBlock sizeForItemBlock; +@property (nonatomic, readonly) ASCellForItemBlock cellForItemBlock; +@property (nonatomic, readonly) ASViewForSupplementaryBlock viewForSupplementaryBlock; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayout.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayout.h new file mode 100644 index 0000000000..6ba15e96eb --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayout.h @@ -0,0 +1,54 @@ +// +// ASCollectionLayout.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@protocol ASCollectionLayoutDelegate; +@class ASElementMap, ASCollectionLayout, ASCollectionNode; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionLayout : UICollectionViewLayout + +/** + * The collection node object currently using this layout object. + * + * @discussion The collection node object sets the value of this property when a new layout object is assigned to it. + * + * @discussion To get the truth on the current state of the collection, call methods on the collection node or the data source rather than the collection view because: + * 1. The view might not yet be allocated. + * 2. The collection node and data source are thread-safe. + */ +@property (nonatomic, weak) ASCollectionNode *collectionNode; + +@property (nonatomic, readonly) id layoutDelegate; + +/** + * Initializes with a layout delegate. + * + * @discussion For developers' convenience, the delegate is retained by this layout object, similar to UICollectionView retains its UICollectionViewLayout object. + * + * @discussion For simplicity, the delegate is read-only. If a new layout delegate is needed, construct a new layout object with that delegate and notify ASCollectionView about it. + * This ensures the underlying UICollectionView purges its cache and properly loads the new layout. + */ +- (instancetype)initWithLayoutDelegate:(id)layoutDelegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +- (instancetype)initWithCoder:(NSCoder *)aDecoder __unavailable; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayout.mm b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayout.mm new file mode 100644 index 0000000000..59697f0820 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayout.mm @@ -0,0 +1,390 @@ +// +// ASCollectionLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = { + .leadingBufferScreenfuls = 2.0, + .trailingBufferScreenfuls = 2.0 +}; + +static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); + +@interface ASCollectionLayout () { + ASCollectionLayoutCache *_layoutCache; + ASCollectionLayoutState *_layout; // Main thread only. + + struct { + unsigned int implementsAdditionalInfoForLayoutWithElements:1; + } _layoutDelegateFlags; +} + +@end + +@implementation ASCollectionLayout + +- (instancetype)initWithLayoutDelegate:(id)layoutDelegate +{ + self = [super init]; + if (self) { + ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); + _layoutDelegate = layoutDelegate; + _layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutCache = [[ASCollectionLayoutCache alloc] init]; + } + return self; +} + +#pragma mark - ASDataControllerLayoutDelegate + +- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + + Class layoutDelegateClass = [_layoutDelegate class]; + ASCollectionLayoutCache *layoutCache = _layoutCache; + ASCollectionNode *collectionNode = _collectionNode; + if (collectionNode == nil) { + return [[ASCollectionLayoutContext alloc] initWithViewportSize:CGSizeZero + initialContentOffset:CGPointZero + scrollableDirections:ASScrollDirectionNone + elements:[[ASElementMap alloc] init] + layoutDelegateClass:layoutDelegateClass + layoutCache:layoutCache + additionalInfo:nil]; + } + + ASScrollDirection scrollableDirections = [_layoutDelegate scrollableDirections]; + CGSize viewportSize = [ASCollectionLayout _viewportSizeForCollectionNode:collectionNode scrollableDirections:scrollableDirections]; + CGPoint contentOffset = collectionNode.contentOffset; + + id additionalInfo = nil; + if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { + additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; + } + + return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize + initialContentOffset:contentOffset + scrollableDirections:scrollableDirections + elements:elements + layoutDelegateClass:layoutDelegateClass + layoutCache:layoutCache + additionalInfo:additionalInfo]; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + if (context.elements == nil) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + ASCollectionLayoutState *layout = [context.layoutDelegateClass calculateLayoutWithContext:context]; + [context.layoutCache setLayout:layout forContext:context]; + + // Measure elements in the measure range ahead of time + CGSize viewportSize = context.viewportSize; + CGPoint contentOffset = context.initialContentOffset; + CGRect initialRect = CGRectMake(contentOffset.x, contentOffset.y, viewportSize.width, viewportSize.height); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, + kASDefaultMeasureRangeTuningParameters, + context.scrollableDirections, + kASStaticScrollDirection); + // The first call to -layoutAttributesForElementsInRect: will be with a rect that is way bigger than initialRect here. + // If we only block on initialRect, a few elements that are outside of initialRect but inside measureRect + // may not be available by the time -layoutAttributesForElementsInRect: is called. + // Since this method is usually run off main, let's spawn more threads to measure and block on all elements in measureRect. + [self _measureElementsInRect:measureRect blockingRect:measureRect layout:layout]; + + return layout; +} + +#pragma mark - UICollectionViewLayout overrides + +- (void)prepareLayout +{ + ASDisplayNodeAssertMainThread(); + [super prepareLayout]; + + ASCollectionLayoutContext *context = [self layoutContextWithElements:_collectionNode.visibleElements]; + if (_layout != nil && ASObjectIsEqual(_layout.context, context)) { + // The existing layout is still valid. No-op + return; + } + + if (ASCollectionLayoutState *cachedLayout = [_layoutCache layoutForContext:context]) { + _layout = cachedLayout; + } else { + // A new layout is needed now. Calculate and apply it immediately + _layout = [ASCollectionLayout calculateLayoutWithContext:context]; + } +} + +- (void)invalidateLayout +{ + ASDisplayNodeAssertMainThread(); + [super invalidateLayout]; + if (_layout != nil) { + [_layoutCache removeLayoutForContext:_layout.context]; + _layout = nil; + } +} + +/** + * NOTE: It is suggested practice on the Web to override invalidationContextForInteractivelyMovingItems… and call out to the + * data source to move the item (so that if e.g. the item size depends on the data, you get the data you expect). However, as of iOS 11 this + * doesn't work, because UICV machinery will also call out to the data source to move the item after the interaction is done. The result is + * that your data source state will be incorrect due to this last move call. Plus it's just an API violation. + * + * Things tried: + * - Doing the speculative data source moves, and then UNDOING the last one in invalidationContextForEndingInteractiveMovementOfItems… + * but this does not work because the UICV machinery informs its data source before it calls that method on us, so we are too late. + * + * The correct practice is to use the UIDataSourceTranslating API introduced in iOS 11. Currently Texture does not support this API but we can + * build it if there is demand. We could add an id field onto the layout context object, and the layout client can + * use data source index paths when it reads nodes or other data source data. + */ + +- (CGSize)collectionViewContentSize +{ + ASDisplayNodeAssertMainThread(); + // The content size can be queried right after a layout invalidation (https://github.com/TextureGroup/Texture/pull/509). + // In that case, return zero. + return _layout ? _layout.contentSize : CGSizeZero; +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)blockingRect +{ + ASDisplayNodeAssertMainThread(); + if (CGRectIsEmpty(blockingRect)) { + return nil; + } + + // Measure elements in the measure range, block on the requested rect + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, + kASDefaultMeasureRangeTuningParameters, + _layout.context.scrollableDirections, + kASStaticScrollDirection); + [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:blockingRect layout:_layout]; + + NSArray *result = [_layout layoutAttributesForElementsInRect:blockingRect]; + + ASElementMap *elements = _layout.context.elements; + for (UICollectionViewLayoutAttributes *attrs in result) { + ASCollectionElement *element = [elements elementForLayoutAttributes:attrs]; + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); + } + + return result; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + + ASCollectionElement *element = [_layout.context.elements elementForItemAtIndexPath:indexPath]; + UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; + + ASCellNode *node = element.node; + CGSize elementSize = attrs.frame.size; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + } + + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); + return attrs; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_layout.context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; + UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; + + ASCellNode *node = element.node; + CGSize elementSize = attrs.frame.size; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + } + + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); + return attrs; +} + +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds +{ + return (! CGSizeEqualToSize([ASCollectionLayout _boundsForCollectionNode:_collectionNode], newBounds.size)); +} + +#pragma mark - Private methods + ++ (CGSize)_boundsForCollectionNode:(nonnull ASCollectionNode *)collectionNode +{ + if (collectionNode == nil) { + return CGSizeZero; + } + + if (!collectionNode.isNodeLoaded) { + // TODO consider calculatedSize as well + return collectionNode.threadSafeBounds.size; + } + + ASDisplayNodeAssertMainThread(); + return collectionNode.view.bounds.size; +} + ++ (CGSize)_viewportSizeForCollectionNode:(nonnull ASCollectionNode *)collectionNode scrollableDirections:(ASScrollDirection)scrollableDirections +{ + if (collectionNode == nil) { + return CGSizeZero; + } + + CGSize result = [ASCollectionLayout _boundsForCollectionNode:collectionNode]; + // TODO: Consider using adjustedContentInset on iOS 11 and later, to include the safe area of the scroll view + UIEdgeInsets contentInset = collectionNode.contentInset; + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { + result.height -= (contentInset.top + contentInset.bottom); + } else { + result.width -= (contentInset.left + contentInset.right); + } + return result; +} + +/** + * Measures all elements in the specified rect and blocks the calling thread while measuring those in the blocking rect. + */ ++ (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout +{ + if (CGRectIsEmpty(rect) || layout.context.elements == nil) { + return; + } + BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); + if (hasBlockingRect && CGRectContainsRect(rect, blockingRect) == NO) { + ASDisplayNodeCAssert(NO, @"Blocking rect, if specified, must be within the other (outer) rect"); + return; + } + + // Step 1: Clamp the specified rects between the bounds of content rect + CGSize contentSize = layout.contentSize; + CGRect contentRect = CGRectMake(0, 0, contentSize.width, contentSize.height); + rect = CGRectIntersection(contentRect, rect); + if (CGRectIsNull(rect)) { + return; + } + if (hasBlockingRect) { + blockingRect = CGRectIntersection(contentRect, blockingRect); + hasBlockingRect = !CGRectIsNull(blockingRect); + } + + // Step 2: Get layout attributes of all elements within the specified outer rect + ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect]; + if (attrsTable.count == 0) { + // No elements in this rect! Bail early + return; + } + + // Step 3: Split all those attributes into blocking and non-blocking buckets + // Use ordered sets here because some items may span multiple pages, and the sets will be accessed by indexes later on. + ASCollectionLayoutContext *context = layout.context; + CGSize pageSize = context.viewportSize; + NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; + NSMutableOrderedSet *nonBlockingAttrs = [NSMutableOrderedSet orderedSet]; + for (id pagePtr in attrsTable) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *attrsInPage = [attrsTable objectForPage:page]; + // Calculate the page's rect but only if it's going to be used. + CGRect pageRect = hasBlockingRect ? ASPageCoordinateGetPageRect(page, pageSize) : CGRectZero; + + if (hasBlockingRect && CGRectContainsRect(blockingRect, pageRect)) { + // The page fits well within the blocking rect. All attributes in this page are blocking. + [blockingAttrs addObjectsFromArray:attrsInPage]; + } else if (hasBlockingRect && CGRectIntersectsRect(blockingRect, pageRect)) { + // The page intersects the blocking rect. Some elements in this page are blocking, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(blockingRect, attrs.frame)) { + [blockingAttrs addObject:attrs]; + } else { + [nonBlockingAttrs addObject:attrs]; + } + } + } else { + // The page doesn't intersect the blocking rect. All elements in this page are non-blocking. + [nonBlockingAttrs addObjectsFromArray:attrsInPage]; + } + } + + // Step 4: Allocate and measure blocking elements' node + ASElementMap *elements = context.elements; + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + if (NSUInteger count = blockingAttrs.count) { + ASDispatchApply(count, queue, 0, ^(size_t i) { + UICollectionViewLayoutAttributes *attrs = blockingAttrs[i]; + ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node; + CGSize expectedSize = attrs.frame.size; + if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)]; + } + }); + } + + // Step 5: Allocate and measure non-blocking ones + if (NSUInteger count = nonBlockingAttrs.count) { + __weak ASElementMap *weakElements = elements; + ASDispatchAsync(count, queue, 0, ^(size_t i) { + __strong ASElementMap *strongElements = weakElements; + if (strongElements) { + UICollectionViewLayoutAttributes *attrs = nonBlockingAttrs[i]; + ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node; + CGSize expectedSize = attrs.frame.size; + if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)]; + } + } + }); + } +} + +# pragma mark - Convenient inline functions + +ASDISPLAYNODE_INLINE ASSizeRange ASCollectionLayoutElementSizeRangeFromSize(CGSize size) +{ + // The layout delegate consulted us that this element must fit within this size, + // and the only way to achieve that without asking it again is to use an exact size range here. + return ASSizeRangeMake(size); +} + +ASDISPLAYNODE_INLINE void ASCollectionLayoutSetSizeToElement(CGSize size, ASCollectionElement *element) +{ + if (ASCellNode *node = element.node) { + if (! CGSizeEqualToSize(size, node.frame.size)) { + CGRect frame = CGRectZero; + frame.size = size; + node.frame = frame; + } + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutCache.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutCache.h new file mode 100644 index 0000000000..857e43683f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutCache.h @@ -0,0 +1,34 @@ +// +// ASCollectionLayoutCache.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionLayoutContext, ASCollectionLayoutState; + +/// A thread-safe cache for ASCollectionLayoutContext-ASCollectionLayoutState pairs +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionLayoutCache : NSObject + +- (nullable ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)context; + +- (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayoutContext *)context; + +- (void)removeLayoutForContext:(ASCollectionLayoutContext *)context; + +- (void)removeAllLayouts; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutCache.mm b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutCache.mm new file mode 100644 index 0000000000..b5aecd78b6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutCache.mm @@ -0,0 +1,92 @@ +// +// ASCollectionLayoutCache.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import + +using AS::MutexLocker; + +@implementation ASCollectionLayoutCache { + AS::Mutex __instanceLock__; + + /** + * The underlying data structure of this cache. + * + * The outer map table is a weak to strong table. That is because ASCollectionLayoutContext doesn't (and shouldn't) + * hold a strong reference on its element map. As a result, this cache should handle the case in which + * an element map no longer exists and all contexts and layouts associated with it should be cleared. + * + * The inner map table is a standard strong to strong map. + * Since different ASCollectionLayoutContext objects with the same content are considered equal, + * "object pointer personality" can't be used as a key option. + */ + NSMapTable *> *_map; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _map = [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; + } + return self; +} + +- (ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (elements == nil) { + return nil; + } + + MutexLocker l(__instanceLock__); + return [[_map objectForKey:elements] objectForKey:context]; +} + +- (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (layout == nil || elements == nil) { + return; + } + + MutexLocker l(__instanceLock__); + auto innerMap = [_map objectForKey:elements]; + if (innerMap == nil) { + innerMap = [NSMapTable strongToStrongObjectsMapTable]; + [_map setObject:innerMap forKey:elements]; + } + [innerMap setObject:layout forKey:context]; +} + +- (void)removeLayoutForContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (elements == nil) { + return; + } + + MutexLocker l(__instanceLock__); + [[_map objectForKey:elements] removeObjectForKey:context]; +} + +- (void)removeAllLayouts +{ + MutexLocker l(__instanceLock__); + [_map removeAllObjects]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutContext+Private.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutContext+Private.h new file mode 100644 index 0000000000..2ff68e17bc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutContext+Private.h @@ -0,0 +1,36 @@ +// +// ASCollectionLayoutContext+Private.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +@class ASCollectionLayoutCache; +@protocol ASCollectionLayoutDelegate; + +NS_ASSUME_NONNULL_BEGIN + +@interface ASCollectionLayoutContext (Private) + +@property (nonatomic, readonly) Class layoutDelegateClass; +@property (nonatomic, weak, readonly) ASCollectionLayoutCache *layoutCache; + +- (instancetype)initWithViewportSize:(CGSize)viewportSize + initialContentOffset:(CGPoint)initialContentOffset + scrollableDirections:(ASScrollDirection)scrollableDirections + elements:(ASElementMap *)elements + layoutDelegateClass:(Class)layoutDelegateClass + layoutCache:(ASCollectionLayoutCache *)layoutCache + additionalInfo:(nullable id)additionalInfo; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutDefines.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutDefines.h new file mode 100644 index 0000000000..61a8ac4e88 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutDefines.h @@ -0,0 +1,19 @@ +// +// ASCollectionLayoutDefines.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN ASSizeRange ASSizeRangeForCollectionLayoutThatFitsViewportSize(CGSize viewportSize, ASScrollDirection scrollableDirections) AS_WARN_UNUSED_RESULT; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutDefines.mm b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutDefines.mm new file mode 100644 index 0000000000..e386575c5d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutDefines.mm @@ -0,0 +1,23 @@ +// +// ASCollectionLayoutDefines.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +ASSizeRange ASSizeRangeForCollectionLayoutThatFitsViewportSize(CGSize viewportSize, ASScrollDirection scrollableDirections) +{ + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections) == NO) { + sizeRange.min.height = viewportSize.height; + sizeRange.max.height = viewportSize.height; + } + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections) == NO) { + sizeRange.min.width = viewportSize.width; + sizeRange.max.width = viewportSize.width; + } + return sizeRange; +} diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutState+Private.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutState+Private.h new file mode 100644 index 0000000000..c4d6013332 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionLayoutState+Private.h @@ -0,0 +1,29 @@ +// +// ASCollectionLayoutState+Private.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASCollectionLayoutState (Private) + +/** + * Remove and returns layout attributes for unmeasured elements that intersect the specified rect + * + * @discussion This method is atomic and thread-safe + */ +- (nullable ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionView+Undeprecated.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionView+Undeprecated.h new file mode 100644 index 0000000000..050e029313 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionView+Undeprecated.h @@ -0,0 +1,303 @@ +// +// ASCollectionView+Undeprecated.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Currently our public collection API is on @c ASCollectionNode and the @c ASCollectionView + * API is deprecated, but the implementations still live in the view. + * + * This category lets us avoid deprecation warnings everywhere internally. + * In the future, the @c ASCollectionView public API will be eliminated and so will this file. + */ +@interface ASCollectionView (Undeprecated) + +/** + * The object that acts as the asynchronous delegate of the collection view + * + * @discussion The delegate must adopt the ASCollectionDelegate protocol. The collection view maintains a weak reference to the delegate object. + * + * The delegate object is responsible for providing size constraints for nodes and indicating whether batch fetching should begin. + */ +@property (nonatomic, weak) id asyncDelegate; + +/** + * The object that acts as the asynchronous data source of the collection view + * + * @discussion The datasource must adopt the ASCollectionDataSource protocol. The collection view maintains a weak reference to the datasource object. + * + * The datasource object is responsible for providing nodes or node creation blocks to the collection view. + */ +@property (nonatomic, weak) id asyncDataSource; + +/** + * Initializes an ASCollectionView + * + * @discussion Initializes and returns a newly allocated collection view object with the specified layout. + * + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil. + */ +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout; + +/** + * Initializes an ASCollectionView + * + * @discussion Initializes and returns a newly allocated collection view object with the specified frame and layout. + * + * @param frame The frame rectangle for the collection view, measured in points. The origin of the frame is relative to the superview in which you plan to add it. This frame is passed to the superclass during initialization. + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil. + */ +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout; + +@property (nonatomic) CGFloat leadingScreensForBatching; + +@property (nonatomic) BOOL inverted; + +@property (nonatomic, readonly) ASScrollDirection scrollDirection; + +@property (nonatomic, readonly) ASScrollDirection scrollableDirections; + +@property (nonatomic, weak) id layoutInspector; + +@property (nonatomic) UIEdgeInsets contentInset; + +@property (nonatomic) CGPoint contentOffset; + +/** + * Tuning parameters for a range type in full mode. + * + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in full mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in full mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType; + +/** + * Tuning parameters for a range type in the specified mode. + * + * @param rangeMode The range mode to get the running parameters for. + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in the given mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in the specified mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeMode The range mode to set the running parameters for. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +- (nullable __kindof UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath; + +@property (nonatomic, readonly) NSArray *indexPathsForVisibleItems; + +@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedItems; + +/** + * Scrolls the collection to the given item. + * + * @param indexPath The index path of the item. + * @param scrollPosition Where the row should end up after the scroll. + * @param animated Whether the scroll should be animated or not. + */ +- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated; + +- (void)selectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition; + +/** + * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. + * The asyncDataSource must be updated to reflect the changes before the update block completes. + * + * @param animated NO to disable animations for this batch + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; + +/** + * Perform a batch of updates asynchronously. This method must be called from the main thread. + * The asyncDataSource must be updated to reflect the changes before update block completes. + * + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; + +/** + * Triggers a relayout of all nodes. + * + * @discussion This method invalidates and lays out every cell node in the collection. + */ +- (void)relayoutItems; + +/** + * Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread. + */ +- (void)waitUntilAllUpdatesAreCommitted; + +/** + * Registers the given kind of supplementary node for use in creating node-backed supplementary views. + * + * @param elementKind The kind of supplementary node that will be requested through the data source. + * + * @discussion Use this method to register support for the use of supplementary nodes in place of the default + * `registerClass:forSupplementaryViewOfKind:withReuseIdentifier:` and `registerNib:forSupplementaryViewOfKind:withReuseIdentifier:` + * methods. This method will register an internal backing view that will host the contents of the supplementary nodes + * returned from the data source. + */ +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind; + +/** + * Inserts one or more sections. + * + * @param sections An index set that specifies the sections to insert. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertSections:(NSIndexSet *)sections; + +/** + * Deletes one or more sections. + * + * @param sections An index set that specifies the sections to delete. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteSections:(NSIndexSet *)sections; + +/** + * Reloads the specified sections. + * + * @param sections An index set that specifies the sections to reload. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadSections:(NSIndexSet *)sections; + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; + +/** + * Inserts items at the locations identified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects, each representing an item index and section index that together identify an item. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Deletes the items specified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to delete. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Reloads the specified items. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to reload. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Moves the item at a specified location to a destination location. + * + * @param indexPath The index path identifying the item to move. + * + * @param newIndexPath The index path that is the destination of the move for the item. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; + +/** + * Similar to -visibleCells. + * + * @return an array containing the nodes being displayed on screen. + */ +- (NSArray<__kindof ASCellNode *> *)visibleNodes AS_WARN_UNUSED_RESULT; + +/** + * Similar to -indexPathForCell:. + * + * @param cellNode a cellNode in the collection view + * + * @return The index path for this cell node. + * + * @discussion This index path returned by this method is in the _view's_ index space + * and should only be used with @c ASCollectionView directly. To get an index path suitable + * for use with your data source and @c ASCollectionNode, call @c indexPathForNode: on the + * collection node instead. + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; + +/** + * Invalidates and recalculates the cached sizes stored for pass-through cells used in interop mode. + */ +- (void)invalidateFlowLayoutDelegateMetrics; + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionViewFlowLayoutInspector.h b/submodules/AsyncDisplayKit/Source/Private/ASCollectionViewFlowLayoutInspector.h new file mode 100644 index 0000000000..0553dcd0b6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionViewFlowLayoutInspector.h @@ -0,0 +1,33 @@ +// +// ASCollectionViewFlowLayoutInspector.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionView; +@class UICollectionViewFlowLayout; + +/** + * A layout inspector implementation specific for the sizing behavior of UICollectionViewFlowLayouts + */ +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionViewFlowLayoutInspector : NSObject + +@property (nonatomic, weak, readonly) UICollectionViewFlowLayout *layout; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFlowLayout:(UICollectionViewFlowLayout *)flowLayout NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASCollectionViewFlowLayoutInspector.mm b/submodules/AsyncDisplayKit/Source/Private/ASCollectionViewFlowLayoutInspector.mm new file mode 100644 index 0000000000..6dc9d73bfb --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASCollectionViewFlowLayoutInspector.mm @@ -0,0 +1,159 @@ +// +// ASCollectionViewFlowLayoutInspector.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#ifndef MINIMAL_ASDK +#import +#import +#import +#import +#import +#import + +#define kDefaultItemSize CGSizeMake(50, 50) + +#pragma mark - ASCollectionViewFlowLayoutInspector + +@interface ASCollectionViewFlowLayoutInspector () +@property (nonatomic, weak) UICollectionViewFlowLayout *layout; +@end + +@implementation ASCollectionViewFlowLayoutInspector { + struct { + unsigned int implementsSizeRangeForHeader:1; + unsigned int implementsReferenceSizeForHeader:1; + unsigned int implementsSizeRangeForFooter:1; + unsigned int implementsReferenceSizeForFooter:1; + unsigned int implementsConstrainedSizeForNodeAtIndexPathDeprecated:1; + unsigned int implementsConstrainedSizeForItemAtIndexPath:1; + } _delegateFlags; +} + +#pragma mark Lifecycle + +- (instancetype)initWithFlowLayout:(UICollectionViewFlowLayout *)flowLayout; +{ + NSParameterAssert(flowLayout); + + self = [super init]; + if (self != nil) { + _layout = flowLayout; + } + return self; +} + +#pragma mark ASCollectionViewLayoutInspecting + +- (void)didChangeCollectionViewDelegate:(id)delegate; +{ + if (delegate == nil) { + memset(&_delegateFlags, 0, sizeof(_delegateFlags)); + } else { + _delegateFlags.implementsSizeRangeForHeader = [delegate respondsToSelector:@selector(collectionNode:sizeRangeForHeaderInSection:)]; + _delegateFlags.implementsReferenceSizeForHeader = [delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]; + _delegateFlags.implementsSizeRangeForFooter = [delegate respondsToSelector:@selector(collectionNode:sizeRangeForFooterInSection:)]; + _delegateFlags.implementsReferenceSizeForFooter = [delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]; + _delegateFlags.implementsConstrainedSizeForNodeAtIndexPathDeprecated = [delegate respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)]; + _delegateFlags.implementsConstrainedSizeForItemAtIndexPath = [delegate respondsToSelector:@selector(collectionNode:constrainedSizeForItemAtIndexPath:)]; + } +} + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + ASSizeRange result = ASSizeRangeUnconstrained; + if (_delegateFlags.implementsConstrainedSizeForItemAtIndexPath) { + result = [collectionView.asyncDelegate collectionNode:collectionView.collectionNode constrainedSizeForItemAtIndexPath:indexPath]; + } else if (_delegateFlags.implementsConstrainedSizeForNodeAtIndexPathDeprecated) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + result = [collectionView.asyncDelegate collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]; +#pragma clang diagnostic pop + } else { + // With 2.0 `collectionView:constrainedSizeForNodeAtIndexPath:` was moved to the delegate. Assert if not implemented on the delegate but on the data source + ASDisplayNodeAssert([collectionView.asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] == NO, @"collectionView:constrainedSizeForNodeAtIndexPath: was moved from the ASCollectionDataSource to the ASCollectionDelegate."); + } + + // If we got no size range: + if (ASSizeRangeEqualToSizeRange(result, ASSizeRangeUnconstrained)) { + // Use itemSize if they set it. + CGSize itemSize = _layout.itemSize; + if (CGSizeEqualToSize(itemSize, kDefaultItemSize) == NO) { + result = ASSizeRangeMake(itemSize, itemSize); + } else { + // Compute constraint from scroll direction otherwise. + result = NodeConstrainedSizeForScrollDirection(collectionView); + } + } + + return result; +} + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASSizeRange result = ASSizeRangeZero; + if (ASObjectIsEqual(kind, UICollectionElementKindSectionHeader)) { + if (_delegateFlags.implementsSizeRangeForHeader) { + result = [[self delegateForCollectionView:collectionView] collectionNode:collectionView.collectionNode sizeRangeForHeaderInSection:indexPath.section]; + } else if (_delegateFlags.implementsReferenceSizeForHeader) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CGSize exactSize = [[self delegateForCollectionView:collectionView] collectionView:collectionView layout:_layout referenceSizeForHeaderInSection:indexPath.section]; +#pragma clang diagnostic pop + result = ASSizeRangeMake(exactSize); + } else { + result = ASSizeRangeMake(_layout.headerReferenceSize); + } + } else if (ASObjectIsEqual(kind, UICollectionElementKindSectionFooter)) { + if (_delegateFlags.implementsSizeRangeForFooter) { + result = [[self delegateForCollectionView:collectionView] collectionNode:collectionView.collectionNode sizeRangeForFooterInSection:indexPath.section]; + } else if (_delegateFlags.implementsReferenceSizeForFooter) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CGSize exactSize = [[self delegateForCollectionView:collectionView] collectionView:collectionView layout:_layout referenceSizeForFooterInSection:indexPath.section]; +#pragma clang diagnostic pop + result = ASSizeRangeMake(exactSize); + } else { + result = ASSizeRangeMake(_layout.footerReferenceSize); + } + } else { + ASDisplayNodeFailAssert(@"Unexpected supplementary kind: %@", kind); + return ASSizeRangeZero; + } + + if (_layout.scrollDirection == UICollectionViewScrollDirectionVertical) { + result.min.width = result.max.width = CGRectGetWidth(collectionView.bounds); + } else { + result.min.height = result.max.height = CGRectGetHeight(collectionView.bounds); + } + return result; +} + +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + ASSizeRange constraint = [self collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + if (_layout.scrollDirection == UICollectionViewScrollDirectionVertical) { + return (constraint.max.height > 0 ? 1 : 0); + } else { + return (constraint.max.width > 0 ? 1 : 0); + } +} + +- (ASScrollDirection)scrollableDirections +{ + return (self.layout.scrollDirection == UICollectionViewScrollDirectionHorizontal) ? ASScrollDirectionHorizontalDirections : ASScrollDirectionVerticalDirections; +} + +#pragma mark - Private helpers + +- (id)delegateForCollectionView:(ASCollectionView *)collectionView +{ + return (id)collectionView.asyncDelegate; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASControlNode+Private.h b/submodules/AsyncDisplayKit/Source/Private/ASControlNode+Private.h new file mode 100644 index 0000000000..02f54a20ec --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASControlNode+Private.h @@ -0,0 +1,18 @@ +// +// ASControlNode+Private.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASControlNode (Private) + +#if TARGET_OS_TV +- (void)_pressDown; +#endif + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASControlTargetAction.h b/submodules/AsyncDisplayKit/Source/Private/ASControlTargetAction.h new file mode 100644 index 0000000000..5a3595c5d3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASControlTargetAction.h @@ -0,0 +1,32 @@ +// +// ASControlTargetAction.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + @abstract ASControlTargetAction stores target action pairs registered for specific ASControlNodeEvent values. + */ +@interface ASControlTargetAction : NSObject + +/** + The action to be called on the registered target. + */ +@property (nonatomic) SEL action; + +/** + Event handler target. The specified action will be called on this object. + */ +@property (nonatomic, weak) id target; + +/** + Indicated whether this target was created without a target, so the action should travel up in the responder chain. + */ +@property (nonatomic, readonly) BOOL createdWithNoTarget; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASControlTargetAction.mm b/submodules/AsyncDisplayKit/Source/Private/ASControlTargetAction.mm new file mode 100644 index 0000000000..41cc113314 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASControlTargetAction.mm @@ -0,0 +1,65 @@ +// +// ASControlTargetAction.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@implementation ASControlTargetAction +{ + __weak id _target; + BOOL _createdWithNoTarget; +} + +- (void)setTarget:(id)target { + _target = target; + + if (!target) { + _createdWithNoTarget = YES; + } +} + +- (id)target { + return _target; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ASControlTargetAction class]]) { + return NO; + } + + ASControlTargetAction *otherObject = (ASControlTargetAction *)object; + + BOOL areTargetsEqual; + + if (self.target != nil && otherObject.target != nil && self.target == otherObject.target) { + areTargetsEqual = YES; + } + else if (self.target == nil && otherObject.target == nil && self.createdWithNoTarget && otherObject.createdWithNoTarget) { + areTargetsEqual = YES; + } + else { + areTargetsEqual = NO; + } + + if (!areTargetsEqual) { + return NO; + } + + if (self.action && otherObject.action && self.action == otherObject.action) { + return YES; + } + else { + return NO; + } +} + +- (NSUInteger)hash { + return [self.target hash]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlayButton.h b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlayButton.h new file mode 100644 index 0000000000..e3bc15246d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlayButton.h @@ -0,0 +1,14 @@ +// +// ASDefaultPlayButton.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASDefaultPlayButton : ASButtonNode + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlayButton.mm b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlayButton.mm new file mode 100644 index 0000000000..589bda90c2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlayButton.mm @@ -0,0 +1,66 @@ +// +// ASDefaultPlayButton.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASDefaultPlayButton + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + self.opaque = NO; + + return self; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + CGFloat originX = bounds.size.width/4; + CGRect buttonBounds = CGRectMake(originX, bounds.size.height/4, bounds.size.width/2, bounds.size.height/2); + CGFloat widthHeight = buttonBounds.size.width; + + //When the video isn't a square, the lower bound should be used to figure out the circle size + if (bounds.size.width < bounds.size.height) { + widthHeight = bounds.size.width/2; + originX = (bounds.size.width - widthHeight)/2; + buttonBounds = CGRectMake(originX, (bounds.size.height - widthHeight)/2, widthHeight, widthHeight); + } + if (bounds.size.width > bounds.size.height) { + widthHeight = bounds.size.height/2; + originX = (bounds.size.width - widthHeight)/2; + buttonBounds = CGRectMake(originX, (bounds.size.height - widthHeight)/2, widthHeight, widthHeight); + } + + CGContextRef context = UIGraphicsGetCurrentContext(); + + // Circle Drawing + UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect: buttonBounds]; + [[UIColor colorWithWhite:0.0 alpha:0.5] setFill]; + [ovalPath fill]; + + // Triangle Drawing + CGContextSaveGState(context); + + UIBezierPath *trianglePath = [UIBezierPath bezierPath]; + [trianglePath moveToPoint:CGPointMake(originX + widthHeight/3, bounds.size.height/4 + (bounds.size.height/2)/4)]; + [trianglePath addLineToPoint:CGPointMake(originX + widthHeight/3, bounds.size.height - bounds.size.height/4 - (bounds.size.height/2)/4)]; + [trianglePath addLineToPoint:CGPointMake(bounds.size.width - originX - widthHeight/4, bounds.size.height/2)]; + + [trianglePath closePath]; + [[UIColor colorWithWhite:0.9 alpha:0.9] setFill]; + [trianglePath fill]; + + CGContextRestoreGState(context); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlaybackButton.h b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlaybackButton.h new file mode 100644 index 0000000000..ae7e245dc0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlaybackButton.h @@ -0,0 +1,19 @@ +// +// ASDefaultPlaybackButton.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +typedef NS_ENUM(NSInteger, ASDefaultPlaybackButtonType) { + ASDefaultPlaybackButtonTypePlay, + ASDefaultPlaybackButtonTypePause +}; + +@interface ASDefaultPlaybackButton : ASControlNode +@property (nonatomic) ASDefaultPlaybackButtonType buttonType; +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlaybackButton.mm b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlaybackButton.mm new file mode 100644 index 0000000000..deda7e8716 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDefaultPlaybackButton.mm @@ -0,0 +1,84 @@ +// +// ASDefaultPlaybackButton.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@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 NSDictionary *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + return @{ + @"buttonType" : @(self.buttonType), + @"color" : self.tintColor + }; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + ASDefaultPlaybackButtonType buttonType = (ASDefaultPlaybackButtonType)[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/submodules/AsyncDisplayKit/Source/Private/ASDispatch.h b/submodules/AsyncDisplayKit/Source/Private/ASDispatch.h new file mode 100644 index 0000000000..e20941806f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDispatch.h @@ -0,0 +1,27 @@ +// +// ASDispatch.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +/** + * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +AS_EXTERN void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +AS_EXTERN void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDispatch.mm b/submodules/AsyncDisplayKit/Source/Private/ASDispatch.mm new file mode 100644 index 0000000000..769a9185d4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDispatch.mm @@ -0,0 +1,63 @@ +// +// ASDispatch.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + + +// Prefer C atomics in this file because ObjC blocks can't capture C++ atomics well. +#import + +/** + * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + if (ASActivateExperimentalFeature(ASExperimentalDispatchApply)) { + dispatch_apply(iterationCount, queue, work); + return; + } + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + dispatch_group_t group = dispatch_group_create(); + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_group_async(group, queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); +}; + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_async(queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } +}; + diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+AsyncDisplay.mm b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+AsyncDisplay.mm new file mode 100644 index 0000000000..31662bd70b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -0,0 +1,493 @@ +// +// ASDisplayNode+AsyncDisplay.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +using AS::MutexLocker; + +@interface ASDisplayNode () <_ASDisplayLayerDelegate> +@end + +@implementation ASDisplayNode (AsyncDisplay) + +#if ASDISPLAYNODE_DELAY_DISPLAY + #define ASDN_DELAY_FOR_DISPLAY() usleep( (long)(0.1 * USEC_PER_SEC) ) +#else + #define ASDN_DELAY_FOR_DISPLAY() +#endif + +#define CHECK_CANCELLED_AND_RETURN_NIL(expr) if (isCancelledBlock()) { \ + expr; \ + return nil; \ + } \ + +- (NSObject *)drawParameters +{ + __instanceLock__.lock(); + BOOL implementsDrawParameters = _flags.implementsDrawParameters; + __instanceLock__.unlock(); + + if (implementsDrawParameters) { + return [self drawParametersForAsyncLayer:self.asyncLayer]; + } else { + return nil; + } +} + +- (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock displayBlocks:(NSMutableArray *)displayBlocks +{ + // Skip subtrees that are hidden or zero alpha. + if (self.isHidden || self.alpha <= 0.0) { + return; + } + + __instanceLock__.lock(); + BOOL rasterizingFromAscendent = (_hierarchyState & ASHierarchyStateRasterized); + __instanceLock__.unlock(); + + // if super node is rasterizing descendants, subnodes will not have had layout calls because they don't have layers + if (rasterizingFromAscendent) { + [self __layout]; + } + + // Capture these outside the display block so they are retained. + UIColor *backgroundColor = self.backgroundColor; + CGRect bounds = self.bounds; + CGFloat cornerRadius = self.cornerRadius; + BOOL clipsToBounds = self.clipsToBounds; + + CGRect frame; + + // If this is the root container node, use a frame with a zero origin to draw into. If not, calculate the correct frame using the node's position, transform and anchorPoint. + if (self.rasterizesSubtree) { + frame = CGRectMake(0.0f, 0.0f, bounds.size.width, bounds.size.height); + } else { + CGPoint position = self.position; + CGPoint anchorPoint = self.anchorPoint; + + // Pretty hacky since full 3D transforms aren't actually supported, but attempt to compute the transformed frame of this node so that we can composite it into approximately the right spot. + CGAffineTransform transform = CATransform3DGetAffineTransform(self.transform); + CGSize scaledBoundsSize = CGSizeApplyAffineTransform(bounds.size, transform); + CGPoint origin = CGPointMake(position.x - scaledBoundsSize.width * anchorPoint.x, + position.y - scaledBoundsSize.height * anchorPoint.y); + frame = CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height); + } + + // Get the display block for this node. + asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES]; + + // We'll display something if there is a display block, clipping, translation and/or a background color. + BOOL shouldDisplay = displayBlock || backgroundColor || CGPointEqualToPoint(CGPointZero, frame.origin) == NO || clipsToBounds; + + // If we should display, then push a transform, draw the background color, and draw the contents. + // The transform is popped in a block added after the recursion into subnodes. + if (shouldDisplay) { + dispatch_block_t pushAndDisplayBlock = ^{ + // Push transform relative to parent. + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + + CGContextTranslateCTM(context, frame.origin.x, frame.origin.y); + + //support cornerRadius + if (rasterizingFromAscendent && clipsToBounds) { + if (cornerRadius) { + [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip]; + } else { + CGContextClipToRect(context, bounds); + } + } + + // Fill background if any. + CGColorRef backgroundCGColor = backgroundColor.CGColor; + if (backgroundColor && CGColorGetAlpha(backgroundCGColor) > 0.0) { + CGContextSetFillColorWithColor(context, backgroundCGColor); + CGContextFillRect(context, bounds); + } + + // If there is a display block, call it to get the image, then copy the image into the current context (which is the rasterized container's backing store). + if (displayBlock) { + UIImage *image = (UIImage *)displayBlock(); + if (image) { + BOOL opaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage)); + CGBlendMode blendMode = opaque ? kCGBlendModeCopy : kCGBlendModeNormal; + [image drawInRect:bounds blendMode:blendMode alpha:1]; + } + } + }; + [displayBlocks addObject:pushAndDisplayBlock]; + } + + // Recursively capture displayBlocks for all descendants. + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + } + + // If we pushed a transform, pop it by adding a display block that does nothing other than that. + if (shouldDisplay) { + // Since this block is pure, we can store it statically. + static dispatch_block_t popBlock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + popBlock = ^{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextRestoreGState(context); + }; + }); + [displayBlocks addObject:popBlock]; + } +} + +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous + isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock + rasterizing:(BOOL)rasterizing +{ + ASDisplayNodeAssertMainThread(); + + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags; + + __instanceLock__.lock(); + + flags = _flags; + + // We always create a graphics context, unless a -display method is used, OR if we are a subnode drawing into a rasterized parent. + BOOL shouldCreateGraphicsContext = (flags.implementsImageDisplay == NO && rasterizing == NO); + BOOL shouldBeginRasterizing = (rasterizing == NO && flags.rasterizesSubtree); + BOOL usesImageDisplay = flags.implementsImageDisplay; + BOOL usesDrawRect = flags.implementsDrawRect; + + if (usesImageDisplay == NO && usesDrawRect == NO && shouldBeginRasterizing == NO) { + // Early exit before requesting more expensive properties like bounds and opaque from the layer. + __instanceLock__.unlock(); + return nil; + } + + BOOL opaque = self.opaque; + CGRect bounds = self.bounds; + UIColor *backgroundColor = self.backgroundColor; + CGColorRef borderColor = self.borderColor; + CGFloat borderWidth = self.borderWidth; + CGFloat contentsScaleForDisplay = _contentsScaleForDisplay; + + __instanceLock__.unlock(); + + // Capture drawParameters from delegate on main thread, if this node is displaying itself rather than recursively rasterizing. + id drawParameters = (shouldBeginRasterizing == NO ? [self drawParameters] : nil); + + // Only the -display methods should be called if we can't size the graphics buffer to use. + if (CGRectIsEmpty(bounds) && (shouldBeginRasterizing || shouldCreateGraphicsContext)) { + return nil; + } + + ASDisplayNodeAssert(contentsScaleForDisplay != 0.0, @"Invalid contents scale"); + ASDisplayNodeAssert(rasterizing || !(_hierarchyState & ASHierarchyStateRasterized), + @"Rasterized descendants should never display unless being drawn into the rasterized container."); + + if (shouldBeginRasterizing) { + // Collect displayBlocks for all descendants. + NSMutableArray *displayBlocks = [[NSMutableArray alloc] init]; + [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + CHECK_CANCELLED_AND_RETURN_NIL(); + + // If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing. + // Unlike CALayer drawing, we include the backgroundColor as a base during rasterization. + opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f; + + displayBlock = ^id{ + CHECK_CANCELLED_AND_RETURN_NIL(); + + ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + + for (dispatch_block_t block in displayBlocks) { + CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext()); + block(); + } + + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + + ASDN_DELAY_FOR_DISPLAY(); + return image; + }; + } else { + displayBlock = ^id{ + CHECK_CANCELLED_AND_RETURN_NIL(); + + if (shouldCreateGraphicsContext) { + ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); + } + + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + UIImage *image = nil; + + if (shouldCreateGraphicsContext && !currentContext) { + ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size)); + return nil; + } + + // For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or + // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. + [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; + + if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly. + image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext. + [self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } + + [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; + + if (shouldCreateGraphicsContext) { + CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); + image = ASGraphicsGetImageAndEndCurrentContext(); + } + + ASDN_DELAY_FOR_DISPLAY(); + return image; + }; + } + + /** + If we're profiling, wrap the display block with signpost start and end. + Color the interval red if cancelled, green otherwise. + */ +#if AS_KDEBUG_ENABLE + __unsafe_unretained id ptrSelf = self; + displayBlock = ^{ + ASSignpostStartCustom(ASSignpostLayerDisplay, ptrSelf, 0); + id result = displayBlock(); + ASSignpostEndCustom(ASSignpostLayerDisplay, ptrSelf, 0, isCancelledBlock() ? ASSignpostColorRed : ASSignpostColorGreen); + return result; + }; +#endif + + return displayBlock; +} + +- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters +{ + if (context) { + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) { + ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + // TODO: This clip path should be removed if we are rasterizing. + CGRect boundingBox = CGContextGetClipBoundingBox(context); + [[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip]; + } + + if (willDisplayNodeContentWithRenderingContext) { + willDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + +} +- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor +{ + if (context == NULL && *image == NULL) { + return; + } + + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + CGFloat contentsScale = _contentsScaleForDisplay; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (context != NULL) { + if (didDisplayNodeContentWithRenderingContext) { + didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) { + CGRect bounds = CGRectZero; + if (context == NULL) { + bounds = self.threadSafeBounds; + bounds.size.width *= contentsScale; + bounds.size.height *= contentsScale; + CGFloat white = 0.0f, alpha = 0.0f; + [backgroundColor getWhite:&white alpha:&alpha]; + ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); + [*image drawInRect:bounds]; + } else { + bounds = CGContextGetClipBoundingBox(context); + } + + ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + + UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds]; + [roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]]; + roundedHole.usesEvenOddFillRule = YES; + + UIBezierPath *roundedPath = nil; + if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0 + CGFloat strokeThickness = borderWidth * contentsScale; + CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f; + roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset) + cornerRadius:_cornerRadius * contentsScale]; + roundedPath.lineWidth = strokeThickness; + [[UIColor colorWithCGColor:borderColor] setStroke]; + } + + // Punch out the corners by copying the backgroundColor over them. + // This works for everything from clearColor to opaque colors. + [backgroundColor setFill]; + [roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f]; + + [roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil. + + if (*image) { + *image = ASGraphicsGetImageAndEndCurrentContext(); + } + } +} + +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously +{ + ASDisplayNodeAssertMainThread(); + + __instanceLock__.lock(); + + if (_hierarchyState & ASHierarchyStateRasterized) { + __instanceLock__.unlock(); + return; + } + + CALayer *layer = _layer; + BOOL rasterizesSubtree = _flags.rasterizesSubtree; + + __instanceLock__.unlock(); + + // for async display, capture the current displaySentinel value to bail early when the job is executed if another is + // enqueued + // for sync display, do not support cancellation + + // FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing + // from the displayQueue? Need to not cancel early fails from displaySentinel changes. + asdisplaynode_iscancelled_block_t isCancelledBlock = nil; + if (asynchronously) { + uint displaySentinelValue = ++_displaySentinel; + __weak ASDisplayNode *weakSelf = self; + isCancelledBlock = ^BOOL{ + __strong ASDisplayNode *self = weakSelf; + return self == nil || (displaySentinelValue != self->_displaySentinel.load()); + }; + } else { + isCancelledBlock = ^BOOL{ + return NO; + }; + } + + // Set up displayBlock to call either display or draw on the delegate and return a UIImage contents + asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO]; + + if (!displayBlock) { + return; + } + + ASDisplayNodeAssert(layer, @"Expect _layer to be not nil"); + + // This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously + asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)); + if (stretchable) { + ASDisplayNodeSetResizableContents(layer, image); + } else { + layer.contentsScale = self.contentsScale; + layer.contents = (id)image.CGImage; + } + [self didDisplayAsyncLayer:self.asyncLayer]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node didDisplayAsyncLayer:node.asyncLayer]; + }); + } + } + }; + + // Call willDisplay immediately in either case + [self willDisplayAsyncLayer:self.asyncLayer asynchronously:asynchronously]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node willDisplayAsyncLayer:node.asyncLayer asynchronously:asynchronously]; + }); + } + + if (asynchronously) { + // Async rendering operations are contained by a transaction, which allows them to proceed and concurrently + // while synchronizing the final application of the results to the layer's contents property (completionBlock). + + // First, look to see if we are expected to join a parent's transaction container. + CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer; + + // In the case that a transaction does not yet exist (such as for an individual node outside of a container), + // this call will allocate the transaction and add it to _ASAsyncTransactionGroup. + // It will automatically commit the transaction at the end of the runloop. + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + + // Adding this displayBlock operation to the transaction will start it IMMEDIATELY. + // The only function of the transaction commit is to gate the calling of the completionBlock. + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + UIImage *contents = (UIImage *)displayBlock(); + completionBlock(contents, NO); + } +} + +- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer +{ + _displaySentinel.fetch_add(1); +} + +- (ASDisplayNodeContextModifier)willDisplayNodeContentWithRenderingContext +{ + MutexLocker l(__instanceLock__); + return _willDisplayNodeContentWithRenderingContext; +} + +- (ASDisplayNodeContextModifier)didDisplayNodeContentWithRenderingContext +{ + MutexLocker l(__instanceLock__); + return _didDisplayNodeContentWithRenderingContext; +} + +- (void)setWillDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier +{ + MutexLocker l(__instanceLock__); + _willDisplayNodeContentWithRenderingContext = contextModifier; +} + +- (void)setDidDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier; +{ + MutexLocker l(__instanceLock__); + _didDisplayNodeContentWithRenderingContext = contextModifier; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+DebugTiming.h b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+DebugTiming.h new file mode 100644 index 0000000000..f6935224a9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+DebugTiming.h @@ -0,0 +1,21 @@ +// +// ASDisplayNode+DebugTiming.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASDisplayNode (DebugTiming) + +@property (nonatomic, readonly) NSTimeInterval debugTimeToCreateView; +@property (nonatomic, readonly) NSTimeInterval debugTimeToApplyPendingState; +@property (nonatomic, readonly) NSTimeInterval debugTimeToAddSubnodeViews; +@property (nonatomic, readonly) NSTimeInterval debugTimeForDidLoad; + +- (NSTimeInterval)debugAllCreationTime; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+DebugTiming.mm b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+DebugTiming.mm new file mode 100644 index 0000000000..d9311a10d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+DebugTiming.mm @@ -0,0 +1,85 @@ +// +// ASDisplayNode+DebugTiming.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASDisplayNode (DebugTiming) + +#if TIME_DISPLAYNODE_OPS +- (NSTimeInterval)debugTimeToCreateView +{ + return _debugTimeToCreateView; +} + +- (NSTimeInterval)debugTimeToApplyPendingState +{ + return _debugTimeToApplyPendingState; +} + +- (NSTimeInterval)debugTimeToAddSubnodeViews +{ + return _debugTimeToAddSubnodeViews; +} + +- (NSTimeInterval)debugTimeForDidLoad +{ + return _debugTimeForDidLoad; +} + +- (NSTimeInterval)debugAllCreationTime +{ + return self.debugTimeToCreateView + self.debugTimeToApplyPendingState + self.debugTimeToAddSubnodeViews + self.debugTimeForDidLoad; +} + +// This would over-count views that are created in the parent's didload or addsubnodesubviews, so we need to take a more basic approach +//- (NSTimeInterval)debugRecursiveAllCreationTime +//{ +// __block NSTimeInterval total = 0; +// ASDisplayNodeFindAllSubnodes(self, ^(ASDisplayNode *n){ +// total += self.debugTimeToCreateView; +// total += self.debugTimeToApplyPendingState; +// total += self.debugTimeToAddSubnodeViews; +// total += self.debugTimeForDidLoad; +// return NO; +// }); +// return total; +//} + +#else + +// These ivars are compiled out so we don't have the info available +- (NSTimeInterval)debugTimeToCreateView +{ + return -1; +} + +- (NSTimeInterval)debugTimeToApplyPendingState +{ + return -1; +} + +- (NSTimeInterval)debugTimeToAddSubnodeViews +{ + return -1; +} + +- (NSTimeInterval)debugTimeForDidLoad +{ + return -1; +} + +- (NSTimeInterval)debugAllCreationTime +{ + return -1; +} + +#endif + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+FrameworkPrivate.h b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+FrameworkPrivate.h new file mode 100644 index 0000000000..c8d5476b3a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -0,0 +1,327 @@ +// +// ASDisplayNode+FrameworkPrivate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASInterfaceStateDelegate; + +/** + Hierarchy state is propagated from nodes to all of their children when certain behaviors are required from the subtree. + Examples include rasterization and external driving of the .interfaceState property. + By passing this information explicitly, performance is optimized by avoiding iteration up the supernode chain. + Lastly, this avoidance of supernode traversal protects against the possibility of deadlocks when a supernode is + simultaneously attempting to materialize views / layers for its subtree (as many related methods require property locking) + + Note: as the hierarchy deepens, more state properties may be enabled. However, state properties may never be disabled / + cancelled below the point they are enabled. They continue to the leaves of the hierarchy. + */ + +typedef NS_OPTIONS(NSUInteger, ASHierarchyState) +{ + /** The node may or may not have a supernode, but no supernode has a special hierarchy-influencing option enabled. */ + ASHierarchyStateNormal = 0, + /** The node has a supernode with .rasterizesSubtree = YES. + Note: the root node of the rasterized subtree (the one with the property set on it) will NOT have this state set. */ + ASHierarchyStateRasterized = 1 << 0, + /** The node or one of its supernodes is managed by a class like ASRangeController. Most commonly, these nodes are + ASCellNode objects or a subnode of one, and are used in ASTableView or ASCollectionView. + These nodes also receive regular updates to the .interfaceState property with more detailed status information. */ + ASHierarchyStateRangeManaged = 1 << 1, + /** Down-propagated version of _flags.visibilityNotificationsDisabled. This flag is very rarely set, but by having it + locally available to nodes, they do not have to walk up supernodes at the critical points it is checked. */ + ASHierarchyStateTransitioningSupernodes = 1 << 2, + /** One of the supernodes of this node is performing a transition. + Any layout calculated during this state should not be applied immediately, but pending until later. */ + ASHierarchyStateLayoutPending = 1 << 3, +}; + +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateLayoutPending) == ASHierarchyStateLayoutPending); +} + +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged); +} + +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRasterized(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized); +} + +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesTransitioningSupernodes(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateTransitioningSupernodes) == ASHierarchyStateTransitioningSupernodes); +} + +__unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyState hierarchyState) +{ + NSMutableArray *states = [NSMutableArray array]; + if (hierarchyState == ASHierarchyStateNormal) { + [states addObject:@"Normal"]; + } + if (ASHierarchyStateIncludesRangeManaged(hierarchyState)) { + [states addObject:@"RangeManaged"]; + } + if (ASHierarchyStateIncludesLayoutPending(hierarchyState)) { + [states addObject:@"LayoutPending"]; + } + if (ASHierarchyStateIncludesRasterized(hierarchyState)) { + [states addObject:@"Rasterized"]; + } + if (ASHierarchyStateIncludesTransitioningSupernodes(hierarchyState)) { + [states addObject:@"TransitioningSupernodes"]; + } + return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; +} + +#define HIERARCHY_STATE_DELTA(Name) ({ \ + if ((oldState & ASHierarchyState##Name) != (newState & ASHierarchyState##Name)) { \ + [changes appendFormat:@"%c%s ", (newState & ASHierarchyState##Name ? '+' : '-'), #Name]; \ + } \ +}) + +__unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarchyState oldState, ASHierarchyState newState) +{ + if (oldState == newState) { + return @"{ }"; + } + + NSMutableString *changes = [NSMutableString stringWithString:@"{ "]; + HIERARCHY_STATE_DELTA(Rasterized); + HIERARCHY_STATE_DELTA(RangeManaged); + HIERARCHY_STATE_DELTA(TransitioningSupernodes); + HIERARCHY_STATE_DELTA(LayoutPending); + [changes appendString:@"}"]; + return changes; +} + +#undef HIERARCHY_STATE_DELTA + +@interface ASDisplayNode () +{ +@protected + ASInterfaceState _interfaceState; + ASHierarchyState _hierarchyState; +} + +// The view class to use when creating a new display node instance. Defaults to _ASDisplayView. ++ (Class)viewClass; + +// Thread safe way to access the bounds of the node +@property (nonatomic) CGRect threadSafeBounds; + +// Returns the bounds of the node without reaching the view or layer +- (CGRect)_locked_threadSafeBounds; + +// The -pendingInterfaceState holds the value that will be applied to -interfaceState by the +// ASCATransactionQueue. If already applied, it matches -interfaceState. Thread-safe access. +@property (nonatomic, readonly) ASInterfaceState pendingInterfaceState; + +// These methods are recursive, and either union or remove the provided interfaceState to all sub-elements. +- (void)enterInterfaceState:(ASInterfaceState)interfaceState; +- (void)exitInterfaceState:(ASInterfaceState)interfaceState; +- (void)recursivelySetInterfaceState:(ASInterfaceState)interfaceState; + +// These methods are recursive, and either union or remove the provided hierarchyState to all sub-elements. +- (void)enterHierarchyState:(ASHierarchyState)hierarchyState; +- (void)exitHierarchyState:(ASHierarchyState)hierarchyState; + +// Changed before calling willEnterHierarchy / didExitHierarchy. +@property (readonly, getter = isInHierarchy) BOOL inHierarchy; +// Call willEnterHierarchy if necessary and set inHierarchy = YES if visibility notifications are enabled on all of its parents +- (void)__enterHierarchy; +// Call didExitHierarchy if necessary and set inHierarchy = NO if visibility notifications are enabled on all of its parents +- (void)__exitHierarchy; + +/** + * @abstract Returns the Hierarchy State of the node. + * + * @return The current ASHierarchyState of the node, indicating whether it is rasterized or managed by a range controller. + * + * @see ASInterfaceState + */ +@property (nonatomic) ASHierarchyState hierarchyState; + +/** + * @abstract Return if the node is range managed or not + * + * @discussion Currently only set interface state on nodes in table and collection views. For other nodes, if they are + * in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`. + */ +- (BOOL)supportsRangeManagedInterfaceState; + +- (BOOL)_locked_displaysAsynchronously; + +// The two methods below will eventually be exposed, but their names are subject to change. +/** + * @abstract Ensure that all rendering is complete for this node and its descendants. + * + * @discussion Calling this method on the main thread after a node is added to the view hierarchy will ensure that + * placeholder states are never visible to the user. It is used by ASTableView, ASCollectionView, and ASViewController + * to implement their respective ".neverShowPlaceholders" option. + * + * If all nodes have layer.contents set and/or their layer does not have -needsDisplay set, the method will return immediately. + * + * This method is capable of handling a mixed set of nodes, with some not having started display, some in progress on an + * asynchronous display operation, and some already finished. + * + * In order to guarantee against deadlocks, this method should only be called on the main thread. + * It may block on the private queue, [_ASDisplayLayer displayQueue] + */ +- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously; + +/** + * @abstract Calls -didExitPreloadState on the receiver and its subnode hierarchy. + * + * @discussion Clears any memory-intensive preloaded content. + * This method is used to notify the node that it should purge any content that is both expensive to fetch and to + * retain in memory. + * + * @see [ASDisplayNode(Subclassing) didExitPreloadState] and [ASDisplayNode(Subclassing) didEnterPreloadState] + */ +- (void)recursivelyClearPreloadedData; + +/** + * @abstract Calls -didEnterPreloadState on the receiver and its subnode hierarchy. + * + * @discussion Fetches content from remote sources for the current node and all subnodes. + * + * @see [ASDisplayNode(Subclassing) didEnterPreloadState] and [ASDisplayNode(Subclassing) didExitPreloadState] + */ +- (void)recursivelyPreload; + +/** + * @abstract Triggers a recursive call to -didEnterPreloadState when the node has an interfaceState of ASInterfaceStatePreload + */ +- (void)setNeedsPreload; + +/** + * @abstract Allows a node to bypass all ensureDisplay passes. Defaults to NO. + * + * @discussion Nodes that are expensive to draw and expected to have placeholder even with + * .neverShowPlaceholders enabled should set this to YES. + * + * ASImageNode uses the default of NO, as it is often used for UI images that are expected to synchronize with ensureDisplay. + * + * ASNetworkImageNode and ASMultiplexImageNode set this to YES, because they load data from a database or server, + * and are expected to support a placeholder state given that display is often blocked on slow data fetching. + */ +@property BOOL shouldBypassEnsureDisplay; + +/** + * @abstract Checks whether a node should be scheduled for display, considering its current and new interface states. + */ +- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState; + +/** + * @abstract safeAreaInsets will fallback to this value if the corresponding UIKit property is not available + * (due to an old iOS version). + * + * @discussion This should be set by the owning view controller based on it's layout guides. + * If this is not a view controllet's node the value will be calculated automatically by the parent node. + */ +@property (nonatomic) UIEdgeInsets fallbackSafeAreaInsets; + +/** + * @abstract Indicates if this node is a view controller's root node. Defaults to NO. + * + * @discussion Set to YES in -[ASViewController initWithNode:]. + * + * YES here only means that this node is used as an ASViewController node. It doesn't mean that this node is a root of + * ASDisplayNode hierarchy, e.g. when its view controller is parented by another ASViewController. + */ +@property (nonatomic, getter=isViewControllerRoot) BOOL viewControllerRoot; + +@end + + +@interface ASDisplayNode (ASLayoutInternal) + +/** + * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. + * + * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know + * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. + */ +- (void)_u_setNeedsLayoutFromAbove; + +/** + * @abstract Subclass hook for nodes that are acting as root nodes. This method is called if one of the subnodes + * size is invalidated and may need to result in a different size as the current calculated size. + */ +- (void)_rootNodeDidInvalidateSize; + +/** + * This method will confirm that the layout is up to date (and update if needed). + * Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). + */ +- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds; + +/** + * Layout all of the subnodes based on the sublayouts + */ +- (void)_layoutSublayouts; + +@end + +@interface ASDisplayNode (ASLayoutTransitionInternal) + +/** + * If one or multiple layout transitions are in flight this methods returns if the current layout transition that + * happens in in this particular thread was invalidated through another thread is starting a transition for this node + */ +- (BOOL)_isLayoutTransitionInvalid; + +/** + * Same as @c -_isLayoutTransitionInvalid but must be called with the node's instance lock held. + */ +- (BOOL)_locked_isLayoutTransitionInvalid; + +/** + * Internal method that can be overriden by subclasses to add specific behavior after the measurement of a layout + * transition did finish. + */ +- (void)_layoutTransitionMeasurementDidFinish; + +/** + * Informs the node that the pending layout transition did complete + */ +- (void)_completePendingLayoutTransition; + +/** + * Called if the pending layout transition did complete + */ +- (void)_pendingLayoutTransitionDidComplete; + +@end + +@interface ASDisplayNode (AccessibilityInternal) +- (NSArray *)accessibilityElements; +@end; + +@interface UIView (ASDisplayNodeInternal) +@property (nullable, weak) ASDisplayNode *asyncdisplaykit_node; +@end + +@interface CALayer (ASDisplayNodeInternal) +@property (nullable, weak) ASDisplayNode *asyncdisplaykit_node; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+UIViewBridge.mm b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+UIViewBridge.mm new file mode 100644 index 0000000000..a6d60a7748 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -0,0 +1,1325 @@ +// +// ASDisplayNode+UIViewBridge.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import + +/** + * The following macros are conveniences to help in the common tasks related to the bridging that ASDisplayNode does to UIView and CALayer. + * In general, a property can either be: + * - Always sent to the layer or view's layer + * use _getFromLayer / _setToLayer + * - Bridged to the view if view-backed or the layer if layer-backed + * use _getFromViewOrLayer / _setToViewOrLayer / _messageToViewOrLayer + * - Only applicable if view-backed + * use _setToViewOnly / _getFromViewOnly + * - Has differing types on views and layers, or custom ASDisplayNode-specific behavior is desired + * manually implement + * + * _bridge_prologue_write is defined to take the node's property lock. Add it at the beginning of any bridged property setters. + * _bridge_prologue_read is defined to take the node's property lock and enforce thread affinity. Add it at the beginning of any bridged property getters. + */ + +#define DISPLAYNODE_USE_LOCKS 1 + +#if DISPLAYNODE_USE_LOCKS +#define _bridge_prologue_read AS::MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self) +#define _bridge_prologue_write AS::MutexLocker l(__instanceLock__) +#else +#define _bridge_prologue_read ASDisplayNodeAssertThreadAffinity(self) +#define _bridge_prologue_write +#endif + +/// Returns YES if the property set should be applied to view/layer immediately. +/// Side Effect: Registers the node with the shared ASPendingStateController if +/// the property cannot be immediately applied and the node does not already have pending changes. +/// This function must be called with the node's lock already held (after _bridge_prologue_write). +/// *warning* the lock should *not* be released until the pending state is updated if this method +/// returns NO. Otherwise, the pending state can be scheduled and flushed *before* you get a chance +/// to apply it. +ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNode *node) { + BOOL loaded = _loaded(node); + if (ASDisplayNodeThreadIsMain()) { + return loaded; + } else { + if (loaded && !ASDisplayNodeGetPendingState(node).hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:node]; + } + return NO; + } +}; + +#define _getFromViewOrLayer(layerProperty, viewAndPendingViewStateProperty) _loaded(self) ? \ + (_view ? _view.viewAndPendingViewStateProperty : _layer.layerProperty )\ + : ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty + +#define _setToViewOrLayer(layerProperty, layerValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ + if (shouldApply) { (_view ? _view.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr) : _layer.layerProperty = (layerValueExpr)); } else { ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr); } + +#define _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ +if (shouldApply) { _view.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr); } else { ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr); } + +#define _getFromViewOnly(viewAndPendingViewStateProperty) _loaded(self) ? _view.viewAndPendingViewStateProperty : ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty + +#define _getFromLayer(layerProperty) _loaded(self) ? _layer.layerProperty : ASDisplayNodeGetPendingState(self).layerProperty + +#define _setToLayer(layerProperty, layerValueExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ +if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNodeGetPendingState(self).layerProperty = (layerValueExpr); } + +/** + * This category implements certain frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node, + * with minimal loss in performance. Unlike UIView and CALayer methods, these can be called from a non-main thread until the view or layer is created. + * This allows text sizing in -calculateSizeThatFits: (essentially a simplified layout) to happen off the main thread + * without any CALayer or UIView actually existing while still being able to set and read properties from ASDisplayNode instances. + */ +@implementation ASDisplayNode (UIViewBridge) + +#if TARGET_OS_TV +// Focus Engine +- (BOOL)canBecomeFocused +{ + return NO; +} + +- (void)setNeedsFocusUpdate +{ + ASDisplayNodeAssertMainThread(); + [_view setNeedsFocusUpdate]; +} + +- (void)updateFocusIfNeeded +{ + ASDisplayNodeAssertMainThread(); + [_view updateFocusIfNeeded]; +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + return NO; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + +} + +- (UIView *)preferredFocusedView +{ + if (self.nodeLoaded) { + return _view; + } + else { + return nil; + } +} +#endif + +- (BOOL)canBecomeFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __canBecomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __canResignFirstResponder]; +} + +- (BOOL)isFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __isFirstResponder]; +} + +- (BOOL)becomeFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __resignFirstResponder]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self.view canPerformAction:action withSender:sender]; +} + +- (CGFloat)alpha +{ + _bridge_prologue_read; + return _getFromViewOrLayer(opacity, alpha); +} + +- (void)setAlpha:(CGFloat)newAlpha +{ + _bridge_prologue_write; + _setToViewOrLayer(opacity, newAlpha, alpha, newAlpha); +} + +- (CGFloat)cornerRadius +{ + AS::MutexLocker l(__instanceLock__); + return _cornerRadius; +} + +- (void)setCornerRadius:(CGFloat)newCornerRadius +{ + [self updateCornerRoundingWithType:self.cornerRoundingType cornerRadius:newCornerRadius]; +} + +- (ASCornerRoundingType)cornerRoundingType +{ + AS::MutexLocker l(__instanceLock__); + return _cornerRoundingType; +} + +- (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType +{ + [self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius]; +} + +- (NSString *)contentsGravity +{ + _bridge_prologue_read; + return _getFromLayer(contentsGravity); +} + +- (void)setContentsGravity:(NSString *)newContentsGravity +{ + _bridge_prologue_write; + _setToLayer(contentsGravity, newContentsGravity); +} + +- (CGRect)contentsRect +{ + _bridge_prologue_read; + return _getFromLayer(contentsRect); +} + +- (void)setContentsRect:(CGRect)newContentsRect +{ + _bridge_prologue_write; + _setToLayer(contentsRect, newContentsRect); +} + +- (CGRect)contentsCenter +{ + _bridge_prologue_read; + return _getFromLayer(contentsCenter); +} + +- (void)setContentsCenter:(CGRect)newContentsCenter +{ + _bridge_prologue_write; + _setToLayer(contentsCenter, newContentsCenter); +} + +- (CGFloat)contentsScale +{ + _bridge_prologue_read; + return _getFromLayer(contentsScale); +} + +- (void)setContentsScale:(CGFloat)newContentsScale +{ + _bridge_prologue_write; + _setToLayer(contentsScale, newContentsScale); +} + +- (CGFloat)rasterizationScale +{ + _bridge_prologue_read; + return _getFromLayer(rasterizationScale); +} + +- (void)setRasterizationScale:(CGFloat)newRasterizationScale +{ + _bridge_prologue_write; + _setToLayer(rasterizationScale, newRasterizationScale); +} + +- (CGRect)bounds +{ + _bridge_prologue_read; + return _getFromViewOrLayer(bounds, bounds); +} + +- (void)setBounds:(CGRect)newBounds +{ + _bridge_prologue_write; + _setToViewOrLayer(bounds, newBounds, bounds, newBounds); + self.threadSafeBounds = newBounds; +} + +- (CGRect)frame +{ + _bridge_prologue_read; + + // Frame is only defined when transform is identity. +//#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. +// ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"-[ASDisplayNode frame] - self.transform must be identity in order to use the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); +//#endif + + CGPoint position = self.position; + CGRect bounds = self.bounds; + CGPoint anchorPoint = self.anchorPoint; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + return CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height); +} + +- (void)setFrame:(CGRect)rect +{ + BOOL setToView = NO; + BOOL setToLayer = NO; + CGRect newBounds = CGRectZero; + CGPoint newPosition = CGPointZero; + BOOL nodeLoaded = NO; + BOOL isMainThread = ASDisplayNodeThreadIsMain(); + { + _bridge_prologue_write; + + // For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - make sure UIView gets setFrame: + struct ASDisplayNodeFlags flags = _flags; + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), flags.layerBacked); + + nodeLoaded = _loaded(self); + if (!specialPropertiesHandling) { + BOOL canReadProperties = isMainThread || !nodeLoaded; + if (canReadProperties) { + // We don't have to set frame directly, and we can read current properties. + // Compute a new bounds and position and set them on self. + CALayer *layer = _layer; + CGPoint origin = (nodeLoaded ? layer.bounds.origin : self.bounds.origin); + CGPoint anchorPoint = (nodeLoaded ? layer.anchorPoint : self.anchorPoint); + + ASBoundsAndPositionForFrame(rect, origin, anchorPoint, &newBounds, &newPosition); + + if (ASIsCGRectValidForLayout(newBounds) == NO || ASIsCGPositionValidForLayout(newPosition) == NO) { + ASDisplayNodeAssertNonFatal(NO, @"-[ASDisplayNode setFrame:] - The new frame (%@) is invalid and unsafe to be set.", NSStringFromCGRect(rect)); + return; + } + + if (nodeLoaded) { + setToLayer = YES; + } else { + self.bounds = newBounds; + self.position = newPosition; + } + } else { + // We don't have to set frame directly, but we can't read properties. + // Store the frame in our pending state, and it'll get decomposed into + // bounds and position when the pending state is applied. + _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); + if (nodeLoaded && !pendingState.hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:self]; + } + pendingState.frame = rect; + } + } else { + if (nodeLoaded && isMainThread) { + // We do have to set frame directly, and we're on main thread with a loaded node. + // Just set the frame on the view. + // NOTE: Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform. + setToView = YES; + } else { + // We do have to set frame directly, but either the node isn't loaded or we're on a non-main thread. + // Set the frame on the pending state, and it'll call setFrame: when applied. + _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); + if (nodeLoaded && !pendingState.hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:self]; + } + pendingState.frame = rect; + } + } + } + + if (setToView) { + ASDisplayNodeAssertTrue(nodeLoaded && isMainThread); + _view.frame = rect; + } else if (setToLayer) { + ASDisplayNodeAssertTrue(nodeLoaded && isMainThread); + _layer.bounds = newBounds; + _layer.position = newPosition; + } +} + +- (void)setNeedsDisplay +{ + BOOL isRasterized = NO; + BOOL shouldApply = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + isRasterized = _hierarchyState & ASHierarchyStateRasterized; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + viewOrLayer = _view ?: _layer; + + if (isRasterized == NO && shouldApply == NO) { + // We can't release the lock before applying to pending state, or it may be flushed before it can be applied. + [ASDisplayNodeGetPendingState(self) setNeedsDisplay]; + } + } + + if (isRasterized) { + ASPerformBlockOnMainThread(^{ + // The below operation must be performed on the main thread to ensure against an extremely rare deadlock, where a parent node + // begins materializing the view / layer hierarchy (locking itself or a descendant) while this node walks up + // the tree and requires locking that node to access .rasterizesSubtree. + // For this reason, this method should be avoided when possible. Use _hierarchyState & ASHierarchyStateRasterized. + ASDisplayNodeAssertMainThread(); + ASDisplayNode *rasterizedContainerNode = self.supernode; + while (rasterizedContainerNode) { + if (rasterizedContainerNode.rasterizesSubtree) { + break; + } + rasterizedContainerNode = rasterizedContainerNode.supernode; + } + [rasterizedContainerNode setNeedsDisplay]; + }); + } else { + if (shouldApply) { + // If not rasterized, and the node is loaded (meaning we certainly have a view or layer), send a + // message to the view/layer first. This is because __setNeedsDisplay calls as scheduleNodeForDisplay, + // which may call -displayIfNeeded. We want to ensure the needsDisplay flag is set now, and then cleared. + [viewOrLayer setNeedsDisplay]; + } + [self __setNeedsDisplay]; + } +} + +- (void)setNeedsLayout +{ + BOOL shouldApply = NO; + BOOL loaded = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + loaded = _loaded(self); + viewOrLayer = _view ?: _layer; + if (shouldApply == NO && loaded) { + // The node is loaded but we're not on main. + // We will call [self __setNeedsLayout] when we apply the pending state. + // We need to call it on main if the node is loaded to support automatic subnode management. + // We can't release the lock before applying to pending state, or it may be flushed before it can be applied. + [ASDisplayNodeGetPendingState(self) setNeedsLayout]; + } + } + + if (shouldApply) { + // The node is loaded and we're on main. + // Quite the opposite of setNeedsDisplay, we must call __setNeedsLayout before messaging + // the view or layer to ensure that measurement and implicitly added subnodes have been handled. + [self __setNeedsLayout]; + [viewOrLayer setNeedsLayout]; + } else if (loaded == NO) { + // The node is not loaded and we're not on main. + [self __setNeedsLayout]; + } +} + +- (void)layoutIfNeeded +{ + BOOL shouldApply = NO; + BOOL loaded = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + loaded = _loaded(self); + viewOrLayer = _view ?: _layer; + if (shouldApply == NO && loaded) { + // The node is loaded but we're not on main. + // We will call layoutIfNeeded on the view or layer when we apply the pending state. __layout will in turn be called on us (see -[_ASDisplayLayer layoutSublayers]). + // We need to call it on main if the node is loaded to support automatic subnode management. + // We can't release the lock before applying to pending state, or it may be flushed before it can be applied. + [ASDisplayNodeGetPendingState(self) layoutIfNeeded]; + } + } + + if (shouldApply) { + // The node is loaded and we're on main. + // Message the view or layer which in turn will call __layout on us (see -[_ASDisplayLayer layoutSublayers]). + [viewOrLayer layoutIfNeeded]; + } else if (loaded == NO) { + // The node is not loaded and we're not on main. + [self __layout]; + } +} + +- (BOOL)isOpaque +{ + _bridge_prologue_read; + return _getFromLayer(opaque); +} + +- (void)setOpaque:(BOOL)newOpaque +{ + _bridge_prologue_write; + + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + + if (shouldApply) { + BOOL oldOpaque = _layer.opaque; + _layer.opaque = newOpaque; + if (oldOpaque != newOpaque) { + [self setNeedsDisplay]; + } + } else { + // NOTE: If we're in the background, we cannot read the current value of self.opaque (if loaded). + // When the pending state is applied to the view on main, we will call `setNeedsDisplay` if + // the new opaque value doesn't match the one on the layer. + ASDisplayNodeGetPendingState(self).opaque = newOpaque; + } +} + +- (BOOL)isUserInteractionEnabled +{ + _bridge_prologue_read; + if (_flags.layerBacked) return NO; + return _getFromViewOnly(userInteractionEnabled); +} + +- (void)setUserInteractionEnabled:(BOOL)enabled +{ + _bridge_prologue_write; + _setToViewOnly(userInteractionEnabled, enabled); +} +#if TARGET_OS_IOS +- (BOOL)isExclusiveTouch +{ + _bridge_prologue_read; + return _getFromViewOnly(exclusiveTouch); +} + +- (void)setExclusiveTouch:(BOOL)exclusiveTouch +{ + _bridge_prologue_write; + _setToViewOnly(exclusiveTouch, exclusiveTouch); +} +#endif +- (BOOL)clipsToBounds +{ + _bridge_prologue_read; + return _getFromViewOrLayer(masksToBounds, clipsToBounds); +} + +- (void)setClipsToBounds:(BOOL)clips +{ + _bridge_prologue_write; + _setToViewOrLayer(masksToBounds, clips, clipsToBounds, clips); +} + +- (CGPoint)anchorPoint +{ + _bridge_prologue_read; + return _getFromLayer(anchorPoint); +} + +- (void)setAnchorPoint:(CGPoint)newAnchorPoint +{ + _bridge_prologue_write; + _setToLayer(anchorPoint, newAnchorPoint); +} + +- (CGPoint)position +{ + _bridge_prologue_read; + return _getFromLayer(position); +} + +- (void)setPosition:(CGPoint)newPosition +{ + _bridge_prologue_write; + _setToLayer(position, newPosition); +} + +- (CGFloat)zPosition +{ + _bridge_prologue_read; + return _getFromLayer(zPosition); +} + +- (void)setZPosition:(CGFloat)newPosition +{ + _bridge_prologue_write; + _setToLayer(zPosition, newPosition); +} + +- (CATransform3D)transform +{ + _bridge_prologue_read; + return _getFromLayer(transform); +} + +- (void)setTransform:(CATransform3D)newTransform +{ + _bridge_prologue_write; + _setToLayer(transform, newTransform); +} + +- (CATransform3D)subnodeTransform +{ + _bridge_prologue_read; + return _getFromLayer(sublayerTransform); +} + +- (void)setSubnodeTransform:(CATransform3D)newSubnodeTransform +{ + _bridge_prologue_write; + _setToLayer(sublayerTransform, newSubnodeTransform); +} + +- (id)contents +{ + _bridge_prologue_read; + return _getFromLayer(contents); +} + +- (void)setContents:(id)newContents +{ + _bridge_prologue_write; + _setToLayer(contents, newContents); +} + +- (BOOL)isHidden +{ + _bridge_prologue_read; + return _getFromViewOrLayer(hidden, hidden); +} + +- (void)setHidden:(BOOL)flag +{ + _bridge_prologue_write; + _setToViewOrLayer(hidden, flag, hidden, flag); +} + +- (BOOL)needsDisplayOnBoundsChange +{ + _bridge_prologue_read; + return _getFromLayer(needsDisplayOnBoundsChange); +} + +- (void)setNeedsDisplayOnBoundsChange:(BOOL)flag +{ + _bridge_prologue_write; + _setToLayer(needsDisplayOnBoundsChange, flag); +} + +- (BOOL)autoresizesSubviews +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(autoresizesSubviews); +} + +- (void)setAutoresizesSubviews:(BOOL)flag +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(autoresizesSubviews, flag); +} + +- (UIViewAutoresizing)autoresizingMask +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(autoresizingMask); +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)mask +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(autoresizingMask, mask); +} + +- (UIViewContentMode)contentMode +{ + _bridge_prologue_read; + if (_loaded(self)) { + if (_flags.layerBacked) { + return ASDisplayNodeUIContentModeFromCAContentsGravity(_layer.contentsGravity); + } else { + return _view.contentMode; + } + } else { + return ASDisplayNodeGetPendingState(self).contentMode; + } +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + _bridge_prologue_write; + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + if (shouldApply) { + if (_flags.layerBacked) { + _layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode); + } else { + _view.contentMode = contentMode; + } + } else { + ASDisplayNodeGetPendingState(self).contentMode = contentMode; + } +} + +- (void)setAccessibilityCustomActions:(NSArray *)accessibilityCustomActions +{ + _bridge_prologue_write; + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + if (shouldApply) { + if (_flags.layerBacked) { + } else { + _view.accessibilityCustomActions = accessibilityCustomActions; + } + } else { + ASDisplayNodeGetPendingState(self).accessibilityCustomActions = accessibilityCustomActions; + } +} + +- (UIColor *)backgroundColor +{ + _bridge_prologue_read; + return [UIColor colorWithCGColor:_getFromLayer(backgroundColor)]; +} + +- (void)setBackgroundColor:(UIColor *)newBackgroundColor +{ + _bridge_prologue_write; + + CGColorRef newBackgroundCGColor = CGColorRetain([newBackgroundColor CGColor]); + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + + if (shouldApply) { + CGColorRef oldBackgroundCGColor = CGColorRetain(_layer.backgroundColor); + + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); + if (specialPropertiesHandling) { + _view.backgroundColor = newBackgroundColor; + } else { + _layer.backgroundColor = newBackgroundCGColor; + } + + if (!CGColorEqualToColor(oldBackgroundCGColor, newBackgroundCGColor)) { + [self setNeedsDisplay]; + } + + CGColorRelease(oldBackgroundCGColor); + } else { + // NOTE: If we're in the background, we cannot read the current value of bgcolor (if loaded). + // When the pending state is applied to the view on main, we will call `setNeedsDisplay` if + // the new background color doesn't match the one on the layer. + ASDisplayNodeGetPendingState(self).backgroundColor = newBackgroundCGColor; + } + CGColorRelease(newBackgroundCGColor); +} + +- (UIColor *)tintColor +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(tintColor); +} + +- (void)setTintColor:(UIColor *)color +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(tintColor, color); +} + +- (void)tintColorDidChange +{ + // ignore this, allow subclasses to be notified +} + +- (CGColorRef)shadowColor +{ + _bridge_prologue_read; + return _getFromLayer(shadowColor); +} + +- (void)setShadowColor:(CGColorRef)colorValue +{ + _bridge_prologue_write; + _setToLayer(shadowColor, colorValue); +} + +- (CGFloat)shadowOpacity +{ + _bridge_prologue_read; + return _getFromLayer(shadowOpacity); +} + +- (void)setShadowOpacity:(CGFloat)opacity +{ + _bridge_prologue_write; + _setToLayer(shadowOpacity, opacity); +} + +- (CGSize)shadowOffset +{ + _bridge_prologue_read; + return _getFromLayer(shadowOffset); +} + +- (void)setShadowOffset:(CGSize)offset +{ + _bridge_prologue_write; + _setToLayer(shadowOffset, offset); +} + +- (CGFloat)shadowRadius +{ + _bridge_prologue_read; + return _getFromLayer(shadowRadius); +} + +- (void)setShadowRadius:(CGFloat)radius +{ + _bridge_prologue_write; + _setToLayer(shadowRadius, radius); +} + +- (CGFloat)borderWidth +{ + _bridge_prologue_read; + return _getFromLayer(borderWidth); +} + +- (void)setBorderWidth:(CGFloat)width +{ + _bridge_prologue_write; + _setToLayer(borderWidth, width); +} + +- (CGColorRef)borderColor +{ + _bridge_prologue_read; + return _getFromLayer(borderColor); +} + +- (void)setBorderColor:(CGColorRef)colorValue +{ + _bridge_prologue_write; + _setToLayer(borderColor, colorValue); +} + +- (BOOL)allowsGroupOpacity +{ + _bridge_prologue_read; + return _getFromLayer(allowsGroupOpacity); +} + +- (void)setAllowsGroupOpacity:(BOOL)allowsGroupOpacity +{ + _bridge_prologue_write; + _setToLayer(allowsGroupOpacity, allowsGroupOpacity); +} + +- (BOOL)allowsEdgeAntialiasing +{ + _bridge_prologue_read; + return _getFromLayer(allowsEdgeAntialiasing); +} + +- (void)setAllowsEdgeAntialiasing:(BOOL)allowsEdgeAntialiasing +{ + _bridge_prologue_write; + _setToLayer(allowsEdgeAntialiasing, allowsEdgeAntialiasing); +} + +- (unsigned int)edgeAntialiasingMask +{ + _bridge_prologue_read; + return _getFromLayer(edgeAntialiasingMask); +} + +- (void)setEdgeAntialiasingMask:(unsigned int)edgeAntialiasingMask +{ + _bridge_prologue_write; + _setToLayer(edgeAntialiasingMask, edgeAntialiasingMask); +} + +- (UISemanticContentAttribute)semanticContentAttribute +{ + _bridge_prologue_read; + return _getFromViewOnly(semanticContentAttribute); +} + +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)semanticContentAttribute +{ + _bridge_prologue_write; + _setToViewOnly(semanticContentAttribute, semanticContentAttribute); +#if YOGA + [self semanticContentAttributeDidChange:semanticContentAttribute]; +#endif +} + +- (UIEdgeInsets)layoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + UIEdgeInsets margins = _getFromViewOnly(layoutMargins); + + if (!AS_AT_LEAST_IOS11 && self.insetsLayoutMarginsFromSafeArea) { + UIEdgeInsets safeArea = self.safeAreaInsets; + margins = ASConcatInsets(margins, safeArea); + } + + return margins; +} + +- (void)setLayoutMargins:(UIEdgeInsets)layoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(layoutMargins, layoutMargins); +} + +- (BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(preservesSuperviewLayoutMargins); +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(preservesSuperviewLayoutMargins, preservesSuperviewLayoutMargins); +} + +- (void)layoutMarginsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnLayoutMarginsChanges) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)safeAreaInsets +{ + _bridge_prologue_read; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked && _loaded(self)) { + return self.view.safeAreaInsets; + } + } + return _fallbackSafeAreaInsets; +} + +- (BOOL)insetsLayoutMarginsFromSafeArea +{ + _bridge_prologue_read; + + return [self _locked_insetsLayoutMarginsFromSafeArea]; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea +{ + ASDisplayNodeAssertThreadAffinity(self); + BOOL shouldNotifyAboutUpdate; + { + _bridge_prologue_write; + + _fallbackInsetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + _setToViewOnly(insetsLayoutMarginsFromSafeArea, insetsLayoutMarginsFromSafeArea); + } + } + + shouldNotifyAboutUpdate = _loaded(self) && (!AS_AT_LEAST_IOS11 || _flags.layerBacked); + } + + if (shouldNotifyAboutUpdate) { + [self layoutMarginsDidChange]; + } +} + +- (void)safeAreaInsetsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnSafeAreaChanges) { + [self setNeedsLayout]; + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + +@end + +@implementation ASDisplayNode (InternalPropertyBridge) + +- (CGFloat)layerCornerRadius +{ + _bridge_prologue_read; + return _getFromLayer(cornerRadius); +} + +- (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius +{ + _bridge_prologue_write; + _setToLayer(cornerRadius, newLayerCornerRadius); +} + +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea +{ + ASAssertLocked(__instanceLock__); + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + return _getFromViewOnly(insetsLayoutMarginsFromSafeArea); + } + } + return _fallbackInsetsLayoutMarginsFromSafeArea; +} + +@end + +#pragma mark - UIViewBridgeAccessibility + +// ASDK supports accessibility for view or layer backed nodes. To be able to provide support for layer backed +// nodes, properties for all of the UIAccessibility protocol defined properties need to be provided an held in sync +// between node and view + +// Helper function with following logic: +// - If the node is not loaded yet use the property from the pending state +// - In case the node is loaded +// - Check if the node has a view and get the value from the view if loaded or from the pending state +// - If view is not available, e.g. the node is layer backed return the property value +#define _getAccessibilityFromViewOrProperty(nodeProperty, viewAndPendingViewStateProperty) _loaded(self) ? \ +(_view ? _view.viewAndPendingViewStateProperty : nodeProperty )\ +: ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty + +// Helper function to set property values on pending state or view and property if loaded +#define _setAccessibilityToViewAndProperty(nodeProperty, nodeValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) \ +nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) + +@implementation ASDisplayNode (UIViewBridgeAccessibility) + +// iOS 11 only properties. Add this to silence "unimplemented selector" warnings +// in old SDKs. If the caller doesn't respect our API_AVAILABLE attributes, then they +// get an appropriate "unrecognized selector" runtime error. +#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_11_0 +@dynamic accessibilityAttributedLabel, accessibilityAttributedHint, accessibilityAttributedValue; +#endif + +- (BOOL)isAccessibilityElement +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_isAccessibilityElement, isAccessibilityElement); +} + +- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_isAccessibilityElement, isAccessibilityElement, isAccessibilityElement, isAccessibilityElement); +} + +- (NSString *)accessibilityLabel +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityLabel, accessibilityLabel); +} + +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityLabel, accessibilityLabel, accessibilityLabel); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSAttributedString *accessibilityAttributedLabel = accessibilityLabel ? [[NSAttributedString alloc] initWithString:accessibilityLabel] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); + } +#endif +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel); +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); } + { _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityAttributedLabel.string, accessibilityLabel, accessibilityAttributedLabel.string); } +} +#endif + +- (NSString *)accessibilityHint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityHint, accessibilityHint); +} + +- (void)setAccessibilityHint:(NSString *)accessibilityHint +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityHint, accessibilityHint, accessibilityHint); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSAttributedString *accessibilityAttributedHint = accessibilityHint ? [[NSAttributedString alloc] initWithString:accessibilityHint] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); + } +#endif +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedHint, accessibilityAttributedHint); +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); } + + { _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityAttributedHint.string, accessibilityHint, accessibilityAttributedHint.string); } +} +#endif + +- (NSString *)accessibilityValue +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityValue, accessibilityValue); +} + +- (void)setAccessibilityValue:(NSString *)accessibilityValue +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityValue, accessibilityValue, accessibilityValue); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSAttributedString *accessibilityAttributedValue = accessibilityValue ? [[NSAttributedString alloc] initWithString:accessibilityValue] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); + } +#endif +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedValue, accessibilityAttributedValue); +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); } + { _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityAttributedValue.string, accessibilityValue, accessibilityAttributedValue.string); } +} +#endif + +- (UIAccessibilityTraits)accessibilityTraits +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityTraits, accessibilityTraits); +} + +- (void)setAccessibilityTraits:(UIAccessibilityTraits)accessibilityTraits +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityTraits, accessibilityTraits, accessibilityTraits, accessibilityTraits); +} + +- (CGRect)accessibilityFrame +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityFrame, accessibilityFrame); +} + +- (void)setAccessibilityFrame:(CGRect)accessibilityFrame +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityFrame, accessibilityFrame, accessibilityFrame, accessibilityFrame); +} + +- (NSString *)accessibilityLanguage +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityLanguage, accessibilityLanguage); +} + +- (void)setAccessibilityLanguage:(NSString *)accessibilityLanguage +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityLanguage, accessibilityLanguage, accessibilityLanguage, accessibilityLanguage); +} + +- (BOOL)accessibilityElementsHidden +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityElementsHidden, accessibilityElementsHidden); +} + +- (void)setAccessibilityElementsHidden:(BOOL)accessibilityElementsHidden +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityElementsHidden, accessibilityElementsHidden, accessibilityElementsHidden, accessibilityElementsHidden); +} + +- (BOOL)accessibilityViewIsModal +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityViewIsModal, accessibilityViewIsModal); +} + +- (void)setAccessibilityViewIsModal:(BOOL)accessibilityViewIsModal +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityViewIsModal, accessibilityViewIsModal, accessibilityViewIsModal, accessibilityViewIsModal); +} + +- (BOOL)shouldGroupAccessibilityChildren +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren); +} + +- (void)setShouldGroupAccessibilityChildren:(BOOL)shouldGroupAccessibilityChildren +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren); +} + +- (NSString *)accessibilityIdentifier +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityIdentifier, accessibilityIdentifier); +} + +- (void)setAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityIdentifier, accessibilityIdentifier, accessibilityIdentifier, accessibilityIdentifier); +} + +- (void)setAccessibilityNavigationStyle:(UIAccessibilityNavigationStyle)accessibilityNavigationStyle +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityNavigationStyle, accessibilityNavigationStyle, accessibilityNavigationStyle, accessibilityNavigationStyle); +} + +- (UIAccessibilityNavigationStyle)accessibilityNavigationStyle +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityNavigationStyle, accessibilityNavigationStyle); +} + +#if TARGET_OS_TV +- (void)setAccessibilityHeaderElements:(NSArray *)accessibilityHeaderElements +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityHeaderElements, accessibilityHeaderElements, accessibilityHeaderElements, accessibilityHeaderElements); +} + +- (NSArray *)accessibilityHeaderElements +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityHeaderElements, accessibilityHeaderElements); +} +#endif + +- (void)setAccessibilityActivationPoint:(CGPoint)accessibilityActivationPoint +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityActivationPoint, accessibilityActivationPoint, accessibilityActivationPoint, accessibilityActivationPoint); +} + +- (CGPoint)accessibilityActivationPoint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityActivationPoint, accessibilityActivationPoint); +} + +- (void)setAccessibilityPath:(UIBezierPath *)accessibilityPath +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityPath, accessibilityPath, accessibilityPath, accessibilityPath); +} + +- (UIBezierPath *)accessibilityPath +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityPath, accessibilityPath); +} + +- (NSInteger)accessibilityElementCount +{ + _bridge_prologue_read; + return _getFromViewOnly(accessibilityElementCount); +} + +@end + + +#pragma mark - ASAsyncTransactionContainer + +@implementation ASDisplayNode (ASAsyncTransactionContainer) + +- (BOOL)asyncdisplaykit_isAsyncTransactionContainer +{ + _bridge_prologue_read; + return _getFromViewOrLayer(asyncdisplaykit_isAsyncTransactionContainer, asyncdisplaykit_isAsyncTransactionContainer); +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)asyncTransactionContainer +{ + _bridge_prologue_write; + _setToViewOrLayer(asyncdisplaykit_asyncTransactionContainer, asyncTransactionContainer, asyncdisplaykit_asyncTransactionContainer, asyncTransactionContainer); +} + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + ASDisplayNodeAssertMainThread(); + return [_layer asyncdisplaykit_asyncTransactionContainerState]; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + ASDisplayNodeAssertMainThread(); + [_layer asyncdisplaykit_cancelAsyncTransactions]; +} + +- (void)asyncdisplaykit_setCurrentAsyncTransaction:(_ASAsyncTransaction *)transaction +{ + _layer.asyncdisplaykit_currentAsyncTransaction = transaction; +} + +- (_ASAsyncTransaction *)asyncdisplaykit_currentAsyncTransaction +{ + return _layer.asyncdisplaykit_currentAsyncTransaction; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeCornerLayerDelegate.h b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeCornerLayerDelegate.h new file mode 100644 index 0000000000..cb95e9fbf6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeCornerLayerDelegate.h @@ -0,0 +1,13 @@ +// +// ASDisplayNodeCornerLayerDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASDisplayNodeCornerLayerDelegate : NSObject +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeCornerLayerDelegate.mm b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeCornerLayerDelegate.mm new file mode 100644 index 0000000000..42838baba1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeCornerLayerDelegate.mm @@ -0,0 +1,19 @@ +// +// ASDisplayNodeCornerLayerDelegate.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASDisplayNodeCornerLayerDelegate.h" + +@implementation ASDisplayNodeCornerLayerDelegate + +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + return (id)kCFNull; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeInternal.h b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeInternal.h new file mode 100644 index 0000000000..8cc49441e1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeInternal.h @@ -0,0 +1,417 @@ +// +// ASDisplayNodeInternal.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#import +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol _ASDisplayLayerDelegate; +@class _ASDisplayLayer; +@class _ASPendingState; +@class ASNodeController; +struct ASDisplayNodeFlags; + +BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); +BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLayerBacked); + +/// Get the pending view state for the node, creating one if needed. +_ASPendingState * ASDisplayNodeGetPendingState(ASDisplayNode * node); + +typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) +{ + ASDisplayNodeMethodOverrideNone = 0, + ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0, + ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, + ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, + ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, + ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, + ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, + ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, + ASDisplayNodeMethodOverrideCanBecomeFirstResponder= 1 << 7, + ASDisplayNodeMethodOverrideBecomeFirstResponder = 1 << 8, + ASDisplayNodeMethodOverrideCanResignFirstResponder= 1 << 9, + ASDisplayNodeMethodOverrideResignFirstResponder = 1 << 10, + ASDisplayNodeMethodOverrideIsFirstResponder = 1 << 11, +}; + +typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) +{ + Synchronous = 1 << 0, + YogaLayoutInProgress = 1 << 1, +}; + +// Can be called without the node's lock. Client is responsible for thread safety. +#define _loaded(node) (node->_layer != nil) + +#define checkFlag(flag) ((_atomicFlags.load() & flag) != 0) +// Returns the old value of the flag as a BOOL. +#define setFlag(flag, x) (((x ? _atomicFlags.fetch_or(flag) \ + : _atomicFlags.fetch_and(~flag)) & flag) != 0) + +AS_EXTERN NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification; +AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp; + +// Allow 2^n increments of begin disabling hierarchy notifications +#define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4 + +#define TIME_DISPLAYNODE_OPS 0 // If you're using this information frequently, try: (DEBUG || PROFILE) + +#define NUM_CLIP_CORNER_LAYERS 4 + +@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> +{ +@package + AS::RecursiveMutex __instanceLock__; + + _ASPendingState *_pendingViewState; + ASInterfaceState _pendingInterfaceState; + ASInterfaceState _preExitingInterfaceState; + + UIView *_view; + CALayer *_layer; + + std::atomic _atomicFlags; + + struct ASDisplayNodeFlags { + // public properties + unsigned viewEverHadAGestureRecognizerAttached:1; + unsigned layerBacked:1; + unsigned displaysAsynchronously:1; + unsigned rasterizesSubtree:1; + unsigned shouldBypassEnsureDisplay:1; + unsigned displaySuspended:1; + unsigned shouldAnimateSizeChanges:1; + + // Wrapped view handling + + // The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically + // optimized for performance and does not use the usual way to provide the contents of the CALayer via the + // CALayerDelegate method that backs the UIImageView. + unsigned canClearContentsOfLayer:1; + + // Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer + // triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView + // it goes through the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately + // UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the + // methods at all instead it throws away the contents of the layer and nothing will show up. + unsigned canCallSetNeedsDisplayOfLayer:1; + + unsigned implementsDrawRect:1; + unsigned implementsImageDisplay:1; + unsigned implementsDrawParameters:1; + + // internal state + unsigned isEnteringHierarchy:1; + unsigned isExitingHierarchy:1; + unsigned isInHierarchy:1; + unsigned visibilityNotificationsDisabled:VISIBILITY_NOTIFICATIONS_DISABLED_BITS; + unsigned isDeallocating:1; + } _flags; + +@protected + ASDisplayNode * __weak _supernode; + NSMutableArray *_subnodes; + + ASNodeController *_strongNodeController; + __weak ASNodeController *_weakNodeController; + + // Set this to nil whenever you modify _subnodes + NSArray *_cachedSubnodes; + + std::atomic_uint _displaySentinel; + + // This is the desired contentsScale, not the scale at which the layer's contents should be displayed + CGFloat _contentsScaleForDisplay; + ASDisplayNodeMethodOverrides _methodOverrides; + + UIEdgeInsets _hitTestSlop; + +#if ASEVENTLOG_ENABLE + ASEventLog *_eventLog; +#endif + + + // Layout support + ASLayoutElementStyle *_style; + std::atomic _primitiveTraitCollection; + + // Layout Spec + ASLayoutSpecBlock _layoutSpecBlock; + NSString *_debugName; + +#if YOGA + // Only ASDisplayNodes are supported in _yogaChildren currently. This means that it is necessary to + // create ASDisplayNodes to make a stack layout when using Yoga. + // However, the implementation is mostly ready for id , with a few areas requiring updates. + NSMutableArray *_yogaChildren; + __weak ASDisplayNode *_yogaParent; + ASLayout *_yogaCalculatedLayout; +#endif + + // Automatically manages subnodes + BOOL _automaticallyManagesSubnodes; // Main thread only + + // Layout Transition + _ASTransitionContext *_pendingLayoutTransitionContext; + NSTimeInterval _defaultLayoutTransitionDuration; + NSTimeInterval _defaultLayoutTransitionDelay; + UIViewAnimationOptions _defaultLayoutTransitionOptions; + + std::atomic _transitionID; + std::atomic _pendingTransitionID; + ASLayoutTransition *_pendingLayoutTransition; + ASDisplayNodeLayout _calculatedDisplayNodeLayout; + ASDisplayNodeLayout _pendingDisplayNodeLayout; + + /// Sentinel for layout data. Incremented when we get -setNeedsLayout / -invalidateCalculatedLayout. + /// Starts at 1. + std::atomic _layoutVersion; + + + // Layout Spec performance measurement + ASDisplayNodePerformanceMeasurementOptions _measurementOptions; + NSTimeInterval _layoutSpecTotalTime; + NSInteger _layoutSpecNumberOfPasses; + NSTimeInterval _layoutComputationTotalTime; + NSInteger _layoutComputationNumberOfPasses; + + + // View Loading + ASDisplayNodeViewBlock _viewBlock; + ASDisplayNodeLayerBlock _layerBlock; + NSMutableArray *_onDidLoadBlocks; + Class _viewClass; // nil -> _ASDisplayView + Class _layerClass; // nil -> _ASDisplayLayer + + + // Placeholder support + UIImage *_placeholderImage; + BOOL _placeholderEnabled; + CALayer *_placeholderLayer; + + // keeps track of nodes/subnodes that have not finished display, used with placeholders + ASWeakSet *_pendingDisplayNodes; + + + // Corner Radius support + CGFloat _cornerRadius; + ASCornerRoundingType _cornerRoundingType; + CALayer *_clipCornerLayers[NUM_CLIP_CORNER_LAYERS]; + + ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; + + + // Accessibility support + BOOL _isAccessibilityElement; + NSString *_accessibilityLabel; + NSAttributedString *_accessibilityAttributedLabel; + NSString *_accessibilityHint; + NSAttributedString *_accessibilityAttributedHint; + NSString *_accessibilityValue; + NSAttributedString *_accessibilityAttributedValue; + UIAccessibilityTraits _accessibilityTraits; + CGRect _accessibilityFrame; + NSString *_accessibilityLanguage; + BOOL _accessibilityElementsHidden; + BOOL _accessibilityViewIsModal; + BOOL _shouldGroupAccessibilityChildren; + NSString *_accessibilityIdentifier; + UIAccessibilityNavigationStyle _accessibilityNavigationStyle; + NSArray *_accessibilityHeaderElements; + CGPoint _accessibilityActivationPoint; + UIBezierPath *_accessibilityPath; + BOOL _isAccessibilityContainer; + + + // Safe Area support + // These properties are used on iOS 10 and lower, where safe area is not supported by UIKit. + UIEdgeInsets _fallbackSafeAreaInsets; + BOOL _fallbackInsetsLayoutMarginsFromSafeArea; + + BOOL _automaticallyRelayoutOnSafeAreaChanges; + BOOL _automaticallyRelayoutOnLayoutMarginsChanges; + + BOOL _isViewControllerRoot; + + +#pragma mark - ASDisplayNode (Debugging) + ASLayout *_unflattenedLayout; + +#if TIME_DISPLAYNODE_OPS +@public + NSTimeInterval _debugTimeToCreateView; + NSTimeInterval _debugTimeToApplyPendingState; + NSTimeInterval _debugTimeToAddSubnodeViews; + NSTimeInterval _debugTimeForDidLoad; +#endif + + /// Fast path: tells whether we've ever had an interface state delegate before. + BOOL _hasHadInterfaceStateDelegates; + __weak id _interfaceStateDelegates[AS_MAX_INTERFACE_STATE_DELEGATES]; +} + ++ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node; + +/// The _ASDisplayLayer backing the node, if any. +@property (nullable, nonatomic, readonly) _ASDisplayLayer *asyncLayer; + +/// Bitmask to check which methods an object overrides. +- (ASDisplayNodeMethodOverrides)methodOverrides; + +/** + * Invoked before a call to setNeedsLayout to the underlying view + */ +- (void)__setNeedsLayout; + +/** + * Invoked after a call to setNeedsDisplay to the underlying view + */ +- (void)__setNeedsDisplay; + +/** + * Setup the node -> controller reference. Strong or weak is based on + * the "shouldInvertStrongReference" property of the controller. + * + * Note: To prevent lock-ordering deadlocks, this method does not take the node's lock. + * In practice, changing the node controller of a node multiple times is not + * supported behavior. + */ +- (void)__setNodeController:(ASNodeController *)controller; + +/** + * Called whenever the node needs to layout its subnodes and, if it's already loaded, its subviews. Executes the layout pass for the node + * + * This method is thread-safe but asserts thread affinity. + */ +- (void)__layout; + +/** + * Internal method to add / replace / insert subnode and remove from supernode without checking if + * node has automaticallyManagesSubnodes set to YES. + */ +- (void)_addSubnode:(ASDisplayNode *)subnode; +- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode; +- (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below; +- (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above; +- (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx; +- (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode; +- (void)_removeFromSupernode; + +// Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this. +- (BOOL)__visibilityNotificationsDisabled; +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled; +- (void)__incrementVisibilityNotificationsDisabled; +- (void)__decrementVisibilityNotificationsDisabled; + +// Helper methods for UIResponder forwarding +- (BOOL)__canBecomeFirstResponder; +- (BOOL)__becomeFirstResponder; +- (BOOL)__canResignFirstResponder; +- (BOOL)__resignFirstResponder; +- (BOOL)__isFirstResponder; + +/// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay; + +/// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. +- (void)displayImmediately; + +/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type. +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius; + +/// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. +- (instancetype)initWithViewClass:(Class)viewClass; + +/// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses. +- (instancetype)initWithLayerClass:(Class)layerClass; + +@property (nonatomic) CGFloat contentsScaleForDisplay; + +- (void)applyPendingViewState; + +/** + * Makes a local copy of the interface state delegates then calls the block on each. + * + * Lock is not held during block invocation. Method must not be called with the lock held. + */ +- (void)enumerateInterfaceStateDelegates:(void(NS_NOESCAPE ^)(id delegate))block; + +/** + * // TODO: NOT YET IMPLEMENTED + * + * @abstract Prevents interface state changes from affecting the node, until disabled. + * + * @discussion Useful to avoid flashing after removing a node from the hierarchy and re-adding it. + * Removing a node from the hierarchy will cause it to exit the Display state, clearing its contents. + * For some animations, it's desirable to be able to remove a node without causing it to re-display. + * Once re-enabled, the interface state will be updated to the same value it would have been. + * + * @see ASInterfaceState + */ +@property (nonatomic) BOOL interfaceStateSuspended; + +/** + * This method has proven helpful in a few rare scenarios, similar to a category extension on UIView, + * but it's considered private API for now and its use should not be encouraged. + * @param checkViewHierarchy If YES, and no supernode can be found, method will walk up from `self.view` to find a supernode. + * If YES, this method must be called on the main thread and the node must not be layer-backed. + */ +- (nullable ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy; + +/** + * Whether this node rasterizes its descendants. See -enableSubtreeRasterization. + */ +@property (readonly) BOOL rasterizesSubtree; + +/** + * Called if a gesture recognizer was attached to an _ASDisplayView + */ +- (void)nodeViewDidAddGestureRecognizer; + +// Recalculates fallbackSafeAreaInsets for the subnodes +- (void)_fallbackUpdateSafeAreaOnChildren; + +@end + +@interface ASDisplayNode (InternalPropertyBridge) + +@property (nonatomic) CGFloat layerCornerRadius; + +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea; + +@end + +@interface ASDisplayNode (ASLayoutElementPrivate) + +/** + * Returns the internal style object or creates a new if no exists. Need to be called with lock held. + */ +- (ASLayoutElementStyle *)_locked_style; + +/** + * Returns the current layout element. Need to be called with lock held. + */ +- (id)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeLayout.h b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeLayout.h new file mode 100644 index 0000000000..ba8d9c273e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeLayout.h @@ -0,0 +1,58 @@ +// +// ASDisplayNodeLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import + +@class ASLayout; + +/* + * Represents a connection between an ASLayout and a ASDisplayNode + * ASDisplayNode uses this to store additional information that are necessary besides the layout + */ +struct ASDisplayNodeLayout { + ASLayout *layout; + ASSizeRange constrainedSize; + CGSize parentSize; + BOOL requestedLayoutFromAbove; + NSUInteger version; + + /* + * Create a new display node layout with + * @param layout The layout to associate, usually returned from a call to -layoutThatFits:parentSize: + * @param constrainedSize Constrained size used to create the layout + * @param parentSize Parent size used to create the layout + * @param version The version of the source layout data – see ASDisplayNode's _layoutVersion. + */ + ASDisplayNodeLayout(ASLayout *layout, ASSizeRange constrainedSize, CGSize parentSize, NSUInteger version) + : layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), requestedLayoutFromAbove(NO), version(version) {}; + + /* + * Creates a layout without any layout associated. By default this display node layout is dirty. + */ + ASDisplayNodeLayout() + : layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), requestedLayoutFromAbove(NO), version(0) {}; + + /** + * Returns whether this is valid for a given version + */ + BOOL isValid(NSUInteger versionArg) { + return layout != nil && version >= versionArg; + } + + /** + * Returns whether this is valid for a given constrained size, parent size, and version + */ + BOOL isValid(ASSizeRange theConstrainedSize, CGSize theParentSize, NSUInteger versionArg) { + return isValid(versionArg) + && CGSizeEqualToSize(parentSize, theParentSize) + && ASSizeRangeEqualToSizeRange(constrainedSize, theConstrainedSize); + } +}; diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeTipState.h b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeTipState.h new file mode 100644 index 0000000000..1e1ef20726 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeTipState.h @@ -0,0 +1,32 @@ +// +// ASDisplayNodeTipState.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@class ASDisplayNode, ASTipNode; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASDisplayNodeTipState : NSObject + +- (instancetype)initWithNode:(ASDisplayNode *)node NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/// Unsafe because once the node is deallocated, we will not be able to access the tip state. +@property (nonatomic, unsafe_unretained, readonly) ASDisplayNode *node; + +/// Main-thread-only. +@property (nonatomic, nullable) ASTipNode *tipNode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeTipState.mm b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeTipState.mm new file mode 100644 index 0000000000..b5a4231998 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASDisplayNodeTipState.mm @@ -0,0 +1,25 @@ +// +// ASDisplayNodeTipState.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASDisplayNodeTipState.h" + +@interface ASDisplayNodeTipState () +@end + +@implementation ASDisplayNodeTipState + +- (instancetype)initWithNode:(ASDisplayNode *)node +{ + if (self = [super init]) { + _node = node; + } + return self; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASIGListAdapterBasedDataSource.h b/submodules/AsyncDisplayKit/Source/Private/ASIGListAdapterBasedDataSource.h new file mode 100644 index 0000000000..5be2185f2f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASIGListAdapterBasedDataSource.h @@ -0,0 +1,29 @@ +// +// ASIGListAdapterBasedDataSource.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_IG_LIST_KIT + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASIGListAdapterBasedDataSource : NSObject + +- (instancetype)initWithListAdapter:(IGListAdapter *)listAdapter collectionDelegate:(nullable id)collectionDelegate; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASIGListAdapterBasedDataSource.mm b/submodules/AsyncDisplayKit/Source/Private/ASIGListAdapterBasedDataSource.mm new file mode 100644 index 0000000000..3cf77afc0a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASIGListAdapterBasedDataSource.mm @@ -0,0 +1,364 @@ +// +// ASIGListAdapterBasedDataSource.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_IG_LIST_KIT + +#import "ASIGListAdapterBasedDataSource.h" +#import +#import + +typedef IGListSectionController ASIGSectionController; + +/// The optional methods that a class implements from ASSectionController. +/// Note: Bitfields are not supported by NSValue so we can't use them. +typedef struct { + BOOL sizeRangeForItem; + BOOL shouldBatchFetch; + BOOL beginBatchFetchWithContext; +} ASSectionControllerOverrides; + +/// The optional methods that a class implements from ASSupplementaryNodeSource. +/// Note: Bitfields are not supported by NSValue so we can't use them. +typedef struct { + BOOL sizeRangeForSupplementary; +} ASSupplementarySourceOverrides; + +@protocol ASIGSupplementaryNodeSource +@end + +@interface ASIGListAdapterBasedDataSource () +@property (nonatomic, weak, readonly) IGListAdapter *listAdapter; +@property (nonatomic, readonly) id delegate; +@property (nonatomic, readonly) id dataSource; +@property (nonatomic, weak, readonly) id collectionDelegate; + +/** + * The section controller that we will forward beginBatchFetchWithContext: to. + * Since shouldBatchFetch: is called on main, we capture the last section controller in there, + * and then we use it and clear it in beginBatchFetchWithContext: (on default queue). + * + * It is safe to use it without a lock in this limited way, since those two methods will + * never execute in parallel. + */ +@property (nonatomic, weak) ASIGSectionController *sectionControllerForBatchFetching; +@end + +@implementation ASIGListAdapterBasedDataSource + +- (instancetype)initWithListAdapter:(IGListAdapter *)listAdapter collectionDelegate:(nullable id)collectionDelegate +{ + if (self = [super init]) { +#if IG_LIST_COLLECTION_VIEW + [ASIGListAdapterBasedDataSource setASCollectionViewSuperclass]; +#endif + [ASIGListAdapterBasedDataSource configureUpdater:listAdapter.updater]; + + ASDisplayNodeAssert([listAdapter conformsToProtocol:@protocol(UICollectionViewDataSource)], @"Expected IGListAdapter to conform to UICollectionViewDataSource."); + ASDisplayNodeAssert([listAdapter conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)], @"Expected IGListAdapter to conform to UICollectionViewDelegateFlowLayout."); + _listAdapter = listAdapter; + _collectionDelegate = collectionDelegate; + } + return self; +} + +- (id)dataSource +{ + return (id)_listAdapter; +} + +- (id)delegate +{ + return (id)_listAdapter; +} + +#pragma mark - ASCollectionDelegate + +- (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view didSelectItemAtIndexPath:indexPath]; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + [self.delegate scrollViewDidScroll:scrollView]; +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + [self.delegate scrollViewWillBeginDragging:scrollView]; +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + // IGListAdapter doesn't implement scrollViewWillEndDragging yet (pending pull request), so we need this check for now. Doesn't hurt to have it anyways :) + if ([self.delegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { + [self.delegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + } +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate +{ + [self.delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView +{ + [self.delegate scrollViewDidEndDecelerating:scrollView]; +} + +- (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode +{ + if ([_collectionDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionNode:)]) { + return [_collectionDelegate shouldBatchFetchForCollectionNode:collectionNode]; + } + + NSInteger sectionCount = [self numberOfSectionsInCollectionNode:collectionNode]; + if (sectionCount == 0) { + return NO; + } + + // If they implement shouldBatchFetch, call it. Otherwise, just say YES if they implement beginBatchFetch. + ASIGSectionController *ctrl = [self sectionControllerForSection:sectionCount - 1]; + ASSectionControllerOverrides o = [ASIGListAdapterBasedDataSource overridesForSectionControllerClass:ctrl.class]; + BOOL result = (o.shouldBatchFetch ? [ctrl shouldBatchFetch] : o.beginBatchFetchWithContext); + if (result) { + self.sectionControllerForBatchFetching = ctrl; + } + return result; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + if ([_collectionDelegate respondsToSelector:@selector(collectionNode:willBeginBatchFetchWithContext:)]) { + [_collectionDelegate collectionNode:collectionNode willBeginBatchFetchWithContext:context]; + return; + } + + ASIGSectionController *ctrl = self.sectionControllerForBatchFetching; + self.sectionControllerForBatchFetching = nil; + [ctrl beginBatchFetchWithContext:context]; +} + +/** + * Note: It is not documented that ASCollectionNode will forward these UIKit delegate calls if they are implemented. + * It is not considered harmful to do so, and adding them to documentation will confuse most users, who should + * instead using the ASCollectionDelegate callbacks. + */ +#pragma mark - ASCollectionDelegateInterop + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; +} + +#pragma mark - ASCollectionDelegateFlowLayout + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForHeaderInSection:(NSInteger)section +{ + id src = [self supplementaryElementSourceForSection:section]; + if ([ASIGListAdapterBasedDataSource overridesForSupplementarySourceClass:[src class]].sizeRangeForSupplementary) { + return [src sizeRangeForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndex:0]; + } else { + return ASSizeRangeZero; + } +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForFooterInSection:(NSInteger)section +{ + id src = [self supplementaryElementSourceForSection:section]; + if ([ASIGListAdapterBasedDataSource overridesForSupplementarySourceClass:[src class]].sizeRangeForSupplementary) { + return [src sizeRangeForSupplementaryElementOfKind:UICollectionElementKindSectionFooter atIndex:0]; + } else { + return ASSizeRangeZero; + } +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section +{ + return [self.delegate collectionView:collectionView layout:collectionViewLayout insetForSectionAtIndex:section]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section +{ + return [self.delegate collectionView:collectionView layout:collectionViewLayout minimumLineSpacingForSectionAtIndex:section]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section +{ + return [self.delegate collectionView:collectionView layout:collectionViewLayout minimumInteritemSpacingForSectionAtIndex:section]; +} + +#pragma mark - ASCollectionDataSource + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return [self.dataSource collectionView:collectionNode.view numberOfItemsInSection:section]; +} + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ + return [self.dataSource numberOfSectionsInCollectionView:collectionNode.view]; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; + ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeBlockForItemAtIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeBlockForItemAtIndex:)), ctrl); + return [ctrl nodeBlockForItemAtIndex:indexPath.item]; +} + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; + ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeForItemAtIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeForItemAtIndex:)), ctrl); + return [ctrl nodeForItemAtIndex:indexPath.item]; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; + if ([ASIGListAdapterBasedDataSource overridesForSectionControllerClass:ctrl.class].sizeRangeForItem) { + return [ctrl sizeRangeForItemAtIndex:indexPath.item]; + } else { + return ASSizeRangeUnconstrained; + } +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + id ctrl = [self supplementaryElementSourceForSection:indexPath.section]; + ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeBlockForSupplementaryElementOfKind:atIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeBlockForSupplementaryElementOfKind:atIndex:)), ctrl); + return [ctrl nodeBlockForSupplementaryElementOfKind:kind atIndex:indexPath.item]; +} + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + id ctrl = [self supplementaryElementSourceForSection:indexPath.section]; + ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeForSupplementaryElementOfKind:atIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeForSupplementaryElementOfKind:atIndex:)), ctrl); + return [ctrl nodeForSupplementaryElementOfKind:kind atIndex:indexPath.item]; +} + +- (NSArray *)collectionNode:(ASCollectionNode *)collectionNode supplementaryElementKindsInSection:(NSInteger)section +{ + return [[self supplementaryElementSourceForSection:section] supportedElementKinds]; +} + +#pragma mark - ASCollectionDataSourceInterop + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + return [self.dataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return [self.dataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; +} + ++ (BOOL)dequeuesCellsForNodeBackedItems +{ + return YES; +} + +#pragma mark - Helpers + +- (id)supplementaryElementSourceForSection:(NSInteger)section +{ + ASIGSectionController *ctrl = [self sectionControllerForSection:section]; + id src = (id)ctrl.supplementaryViewSource; + ASDisplayNodeAssert(src == nil || [src conformsToProtocol:@protocol(ASSupplementaryNodeSource)], @"Supplementary view source should conform to %@", NSStringFromProtocol(@protocol(ASSupplementaryNodeSource))); + return src; +} + +- (ASIGSectionController *)sectionControllerForSection:(NSInteger)section +{ + id object = [_listAdapter objectAtSection:section]; + ASIGSectionController *ctrl = (ASIGSectionController *)[_listAdapter sectionControllerForObject:object]; + ASDisplayNodeAssert([ctrl conformsToProtocol:@protocol(ASSectionController)], @"Expected section controller to conform to %@. Controller: %@", NSStringFromProtocol(@protocol(ASSectionController)), ctrl); + return ctrl; +} + +/// If needed, set ASCollectionView's superclass to IGListCollectionView (IGListKit < 3.0). +#if IG_LIST_COLLECTION_VIEW ++ (void)setASCollectionViewSuperclass +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + class_setSuperclass([ASCollectionView class], [IGListCollectionView class]); + }); +#pragma clang diagnostic pop +} +#endif + +/// Ensure updater won't call reloadData on us. ++ (void)configureUpdater:(id)updater +{ + // Cast to NSObject will be removed after https://github.com/Instagram/IGListKit/pull/435 + if ([(id)updater isKindOfClass:[IGListAdapterUpdater class]]) { + [(IGListAdapterUpdater *)updater setAllowsBackgroundReloading:NO]; + } else { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSLog(@"WARNING: Use of non-%@ updater with AsyncDisplayKit is discouraged. Updater: %@", NSStringFromClass([IGListAdapterUpdater class]), updater); + }); + } +} + ++ (ASSupplementarySourceOverrides)overridesForSupplementarySourceClass:(Class)c +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[NSCache alloc] init]; + }); + NSValue *obj = [cache objectForKey:c]; + ASSupplementarySourceOverrides o; + if (obj == nil) { + o.sizeRangeForSupplementary = [c instancesRespondToSelector:@selector(sizeRangeForSupplementaryElementOfKind:atIndex:)]; + obj = [NSValue valueWithBytes:&o objCType:@encode(ASSupplementarySourceOverrides)]; + [cache setObject:obj forKey:c]; + } else { + [obj getValue:&o]; + } + return o; +} + ++ (ASSectionControllerOverrides)overridesForSectionControllerClass:(Class)c +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[NSCache alloc] init]; + }); + NSValue *obj = [cache objectForKey:c]; + ASSectionControllerOverrides o; + if (obj == nil) { + o.sizeRangeForItem = [c instancesRespondToSelector:@selector(sizeRangeForItemAtIndex:)]; + o.beginBatchFetchWithContext = [c instancesRespondToSelector:@selector(beginBatchFetchWithContext:)]; + o.shouldBatchFetch = [c instancesRespondToSelector:@selector(shouldBatchFetch)]; + obj = [NSValue valueWithBytes:&o objCType:@encode(ASSectionControllerOverrides)]; + [cache setObject:obj forKey:c]; + } else { + [obj getValue:&o]; + } + return o; +} + +@end + +#endif // AS_IG_LIST_KIT diff --git a/submodules/AsyncDisplayKit/Source/Private/ASImageNode+AnimatedImagePrivate.h b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+AnimatedImagePrivate.h new file mode 100644 index 0000000000..528b39beb3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -0,0 +1,47 @@ +// +// ASImageNode+AnimatedImagePrivate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define ASAnimatedImageDefaultRunLoopMode NSRunLoopCommonModes + +@interface ASImageNode () +{ +#ifndef MINIMAL_ASDK + AS::Mutex _displayLinkLock; + id _animatedImage; + BOOL _animatedImagePaused; + NSString *_animatedImageRunLoopMode; + CADisplayLink *_displayLink; + NSUInteger _lastSuccessfulFrameIndex; + + //accessed on main thread only + CFTimeInterval _playHead; + NSUInteger _playedLoops; +#endif +} + +@property (nonatomic) CFTimeInterval lastDisplayLinkFire; + +@end + +#ifndef MINIMAL_ASDK +@interface ASImageNode (AnimatedImagePrivate) + +- (void)_locked_setAnimatedImage:(id )animatedImage; + +@end + +@interface ASImageNode (AnimatedImageInvalidation) + +- (void)invalidateAnimatedImage; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASImageNode+CGExtras.h b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+CGExtras.h new file mode 100644 index 0000000000..01b546443b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+CGExtras.h @@ -0,0 +1,31 @@ +// +// ASImageNode+CGExtras.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +/** + @abstract Decides how to scale and crop an image to fit in the provided size, while not wasting memory by upscaling images + @param sourceImageSize The size of the encoded image. + @param boundsSize The bounds in which the image will be displayed. + @param contentMode The mode that defines how image will be scaled and cropped to fit. Supported values are UIViewContentModeScaleToAspectFill and UIViewContentModeScaleToAspectFit. + @param cropRect A rectangle that is to be featured by the cropped image. The rectangle is specified as a "unit rectangle," using fractions of the source image's width and height, e.g. CGRectMake(0.5, 0, 0.5, 1.0) will feature the full right half a photo. If the cropRect is empty, the contentMode will be used to determine the drawRect's size, and only the cropRect's origin will be used for positioning. + @param forceUpscaling A boolean that indicates you would *not* like the backing size to be downscaled if the image is smaller than the destination size. Setting this to YES will result in higher memory usage when images are smaller than their destination. + @param forcedSize A CGSize, that if non-CGSizeZero, indicates that the backing size should be forcedSize and not calculated based on boundsSize. + @discussion If the image is smaller than the size and UIViewContentModeScaleToAspectFill is specified, we suggest the input size so it will be efficiently upscaled on the GPU by the displaying layer at composite time. + */ +AS_EXTERN void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, + CGSize boundsSize, + UIViewContentMode contentMode, + CGRect cropRect, + BOOL forceUpscaling, + CGSize forcedSize, + CGSize *outBackingSize, + CGRect *outDrawRect + ); diff --git a/submodules/AsyncDisplayKit/Source/Private/ASImageNode+CGExtras.mm b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+CGExtras.mm new file mode 100644 index 0000000000..c4f9fdcbc9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+CGExtras.mm @@ -0,0 +1,123 @@ +// +// ASImageNode+CGExtras.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +// TODO rewrite these to be closer to the intended use -- take UIViewContentMode as param, CGRect destinationBounds, CGSize sourceSize. +static CGSize _ASSizeFillWithAspectRatio(CGFloat aspectRatio, CGSize constraints); +static CGSize _ASSizeFitWithAspectRatio(CGFloat aspectRatio, CGSize constraints); + +static CGSize _ASSizeFillWithAspectRatio(CGFloat sizeToScaleAspectRatio, CGSize destinationSize) +{ + CGFloat destinationAspectRatio = destinationSize.width / destinationSize.height; + if (sizeToScaleAspectRatio > destinationAspectRatio) { + return CGSizeMake(destinationSize.height * sizeToScaleAspectRatio, destinationSize.height); + } else { + return CGSizeMake(destinationSize.width, round(destinationSize.width / sizeToScaleAspectRatio)); + } +} + +static CGSize _ASSizeFitWithAspectRatio(CGFloat aspectRatio, CGSize constraints) +{ + CGFloat constraintAspectRatio = constraints.width / constraints.height; + if (aspectRatio > constraintAspectRatio) { + return CGSizeMake(constraints.width, constraints.width / aspectRatio); + } else { + return CGSizeMake(constraints.height * aspectRatio, constraints.height); + } +} + +void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, + CGSize boundsSize, + UIViewContentMode contentMode, + CGRect cropRect, + BOOL forceUpscaling, + CGSize forcedSize, + CGSize *outBackingSize, + CGRect *outDrawRect + ) +{ + + size_t destinationWidth = boundsSize.width; + size_t destinationHeight = boundsSize.height; + + // Often, an image is too low resolution to completely fill the width and height provided. + // Per the API contract as commented in the header, we will adjust input parameters (destinationWidth, destinationHeight) to ensure that the image is not upscaled on the CPU. + CGFloat boundsAspectRatio = (CGFloat)destinationWidth / (CGFloat)destinationHeight; + + CGSize scaledSizeForImage = sourceImageSize; + BOOL cropToRectDimensions = !CGRectIsEmpty(cropRect); + + if (cropToRectDimensions) { + scaledSizeForImage = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); + } else { + if (contentMode == UIViewContentModeScaleAspectFill) + scaledSizeForImage = _ASSizeFillWithAspectRatio(boundsAspectRatio, sourceImageSize); + else if (contentMode == UIViewContentModeScaleAspectFit) + scaledSizeForImage = _ASSizeFitWithAspectRatio(boundsAspectRatio, sourceImageSize); + } + + // If fitting the desired aspect ratio to the image size actually results in a larger buffer, use the input values. + // However, if there is a pixel savings (e.g. we would have to upscale the image), override the function arguments. + if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) { + destinationWidth = (size_t)round(forcedSize.width); + destinationHeight = (size_t)round(forcedSize.height); + } else if (forceUpscaling == NO && (scaledSizeForImage.width * scaledSizeForImage.height) < (destinationWidth * destinationHeight)) { + destinationWidth = (size_t)round(scaledSizeForImage.width); + destinationHeight = (size_t)round(scaledSizeForImage.height); + if (destinationWidth == 0 || destinationHeight == 0) { + *outBackingSize = CGSizeZero; + *outDrawRect = CGRectZero; + return; + } + } + + // Figure out the scaled size within the destination bounds. + CGFloat sourceImageAspectRatio = sourceImageSize.width / sourceImageSize.height; + CGSize scaledSizeForDestination = CGSizeMake(destinationWidth, destinationHeight); + + if (cropToRectDimensions) { + scaledSizeForDestination = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); + } else { + if (contentMode == UIViewContentModeScaleAspectFill) + scaledSizeForDestination = _ASSizeFillWithAspectRatio(sourceImageAspectRatio, scaledSizeForDestination); + else if (contentMode == UIViewContentModeScaleAspectFit) + scaledSizeForDestination = _ASSizeFitWithAspectRatio(sourceImageAspectRatio, scaledSizeForDestination); + } + + // Figure out the rectangle into which to draw the image. + CGRect drawRect = CGRectZero; + if (cropToRectDimensions) { + drawRect = CGRectMake(-cropRect.origin.x * scaledSizeForDestination.width, + -cropRect.origin.y * scaledSizeForDestination.height, + scaledSizeForDestination.width, + scaledSizeForDestination.height); + } else { + // We want to obey the origin of cropRect in aspect-fill mode. + if (contentMode == UIViewContentModeScaleAspectFill) { + drawRect = CGRectMake(((destinationWidth - scaledSizeForDestination.width) * cropRect.origin.x), + ((destinationHeight - scaledSizeForDestination.height) * cropRect.origin.y), + scaledSizeForDestination.width, + scaledSizeForDestination.height); + + } + // And otherwise just center it. + else { + drawRect = CGRectMake(((destinationWidth - scaledSizeForDestination.width) / 2.0), + ((destinationHeight - scaledSizeForDestination.height) / 2.0), + scaledSizeForDestination.width, + scaledSizeForDestination.height); + } + } + + *outDrawRect = drawRect; + *outBackingSize = CGSizeMake(destinationWidth, destinationHeight); +} diff --git a/submodules/AsyncDisplayKit/Source/Private/ASImageNode+Private.h b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+Private.h new file mode 100644 index 0000000000..8de78c8784 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASImageNode+Private.h @@ -0,0 +1,17 @@ +// +// ASImageNode+Private.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +@interface ASImageNode (Private) + +- (void)_locked_setImage:(UIImage *)image; +- (UIImage *)_locked_Image; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASInternalHelpers.h b/submodules/AsyncDisplayKit/Source/Private/ASInternalHelpers.h new file mode 100644 index 0000000000..8bc53fa0e6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASInternalHelpers.h @@ -0,0 +1,124 @@ +// +// ASInternalHelpers.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASAvailability.h" + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN void ASInitializeFrameworkMainThread(void); + +AS_EXTERN BOOL ASDefaultAllowsGroupOpacity(void); +AS_EXTERN BOOL ASDefaultAllowsEdgeAntialiasing(void); + +AS_EXTERN BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); +AS_EXTERN BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector); + +/// Replace a method from the given class with a block and returns the original method IMP +AS_EXTERN IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block); + +/// Dispatches the given block to the main queue if not already running on the main thread +AS_EXTERN void ASPerformBlockOnMainThread(void (^block)(void)); + +/// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue +AS_EXTERN void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT + +/// For deallocation of objects on a background thread without GCD overhead / thread explosion +AS_EXTERN void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object); + +AS_EXTERN CGFloat ASScreenScale(void); + +AS_EXTERN CGSize ASFloorSizeValues(CGSize s); + +AS_EXTERN CGFloat ASFloorPixelValue(CGFloat f); + +AS_EXTERN CGPoint ASCeilPointValues(CGPoint p); + +AS_EXTERN CGSize ASCeilSizeValues(CGSize s); + +AS_EXTERN CGFloat ASCeilPixelValue(CGFloat f); + +AS_EXTERN CGFloat ASRoundPixelValue(CGFloat f); + +AS_EXTERN Class _Nullable ASGetClassFromType(const char * _Nullable type); + +ASDISPLAYNODE_INLINE BOOL ASImageAlphaInfoIsOpaque(CGImageAlphaInfo info) { + switch (info) { + case kCGImageAlphaNone: + case kCGImageAlphaNoneSkipLast: + case kCGImageAlphaNoneSkipFirst: + return YES; + default: + return NO; + } +} + +/** + @summary Conditionally performs UIView geometry changes in the given block without animation. + + Used primarily to circumvent UITableView forcing insertion animations when explicitly told not to via + `UITableViewRowAnimationNone`. More info: https://github.com/facebook/AsyncDisplayKit/pull/445 + + @param withoutAnimation Set to `YES` to perform given block without animation + @param block Perform UIView geometry changes within the passed block + */ +ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)(void)) { + if (withoutAnimation) { + [UIView performWithoutAnimation:block]; + } else { + block(); + } +} + +ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origin, CGPoint anchorPoint, CGRect *bounds, CGPoint *position) +{ + *bounds = (CGRect){ origin, rect.size }; + *position = CGPointMake(rect.origin.x + rect.size.width * anchorPoint.x, + rect.origin.y + rect.size.height * anchorPoint.y); +} + +ASDISPLAYNODE_INLINE UIEdgeInsets ASConcatInsets(UIEdgeInsets insetsA, UIEdgeInsets insetsB) +{ + insetsA.top += insetsB.top; + insetsA.left += insetsB.left; + insetsA.bottom += insetsB.bottom; + insetsA.right += insetsB.right; + return insetsA; +} + +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASImageDownloaderPriority ASImageDownloaderPriorityWithInterfaceState(ASInterfaceState interfaceState) { + if (ASInterfaceStateIncludesVisible(interfaceState)) { + return ASImageDownloaderPriorityVisible; + } + + if (ASInterfaceStateIncludesDisplay(interfaceState)) { + return ASImageDownloaderPriorityImminent; + } + + if (ASInterfaceStateIncludesPreload(interfaceState)) { + return ASImageDownloaderPriorityPreload; + } + + return ASImageDownloaderPriorityPreload; +} + +@interface NSIndexPath (ASInverseComparison) +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; +@end + +NS_ASSUME_NONNULL_END + +#ifndef AS_INITIALIZE_FRAMEWORK_MANUALLY +#define AS_INITIALIZE_FRAMEWORK_MANUALLY 0 +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASInternalHelpers.mm b/submodules/AsyncDisplayKit/Source/Private/ASInternalHelpers.mm new file mode 100644 index 0000000000..a9926ccca4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASInternalHelpers.mm @@ -0,0 +1,233 @@ +// +// ASInternalHelpers.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import + +#import +#import +#import + +static NSNumber *allowsGroupOpacityFromUIKitOrNil; +static NSNumber *allowsEdgeAntialiasingFromUIKitOrNil; + +BOOL ASDefaultAllowsGroupOpacity() +{ + static BOOL groupOpacity; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSNumber *groupOpacityObj = allowsGroupOpacityFromUIKitOrNil ?: [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIViewGroupOpacity"]; + groupOpacity = groupOpacityObj ? groupOpacityObj.boolValue : YES; + }); + return groupOpacity; +} + +BOOL ASDefaultAllowsEdgeAntialiasing() +{ + static BOOL edgeAntialiasing; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSNumber *antialiasingObj = allowsEdgeAntialiasingFromUIKitOrNil ?: [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIViewEdgeAntialiasing"]; + edgeAntialiasing = antialiasingObj ? antialiasingObj.boolValue : NO; + }); + return edgeAntialiasing; +} + +void ASInitializeFrameworkMainThread(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ASDisplayNodeCAssertMainThread(); + // Ensure these values are cached on the main thread before needed in the background. + if (ASActivateExperimentalFeature(ASExperimentalLayerDefaults)) { + // Nop. We will gather default values on-demand in ASDefaultAllowsGroupOpacity and ASDefaultAllowsEdgeAntialiasing + } else { + CALayer *layer = [[[UIView alloc] init] layer]; + allowsGroupOpacityFromUIKitOrNil = @(layer.allowsGroupOpacity); + allowsEdgeAntialiasingFromUIKitOrNil = @(layer.allowsEdgeAntialiasing); + } + ASNotifyInitialized(); + }); +} + +BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector) +{ + if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself. + Method superclassMethod = class_getInstanceMethod(superclass, selector); + Method subclassMethod = class_getInstanceMethod(subclass, selector); + return (superclassMethod != subclassMethod); +} + +BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector) +{ + if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself. + Method superclassMethod = class_getClassMethod(superclass, selector); + Method subclassMethod = class_getClassMethod(subclass, selector); + return (superclassMethod != subclassMethod); +} + +IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block) +{ + NSCParameterAssert(block); + + // Get original method + Method origMethod = class_getInstanceMethod(c, origSEL); + NSCParameterAssert(origMethod); + + // Convert block to IMP trampoline and replace method implementation + IMP newIMP = imp_implementationWithBlock(block); + + // Try adding the method if not yet in the current class + if (!class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) { + return method_setImplementation(origMethod, newIMP); + } else { + return method_getImplementation(origMethod); + } +} + +void ASPerformBlockOnMainThread(void (^block)(void)) +{ + if (block == nil){ + return; + } + if (ASDisplayNodeThreadIsMain()) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), block); + } +} + +void ASPerformBlockOnBackgroundThread(void (^block)(void)) +{ + if (block == nil){ + return; + } + if (ASDisplayNodeThreadIsMain()) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); + } else { + block(); + } +} + +void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object) +{ + [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; +} + +Class _Nullable ASGetClassFromType(const char * _Nullable type) +{ + // Class types all start with @" + if (type == NULL || strncmp(type, "@\"", 2) != 0) { + return Nil; + } + + // Ensure length >= 3 + size_t typeLength = strlen(type); + if (typeLength < 3) { + ASDisplayNodeCFailAssert(@"Got invalid type-encoding: %s", type); + return Nil; + } + + // Copy type[2..(end-1)]. So @"UIImage" -> UIImage + size_t resultLength = typeLength - 3; + char className[resultLength + 1]; + strncpy(className, type + 2, resultLength); + className[resultLength] = '\0'; + return objc_getClass(className); +} + +CGFloat ASScreenScale() +{ + static CGFloat __scale = 0.0; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + __scale = CGContextGetCTM(UIGraphicsGetCurrentContext()).a; + UIGraphicsEndImageContext(); + }); + return __scale; +} + +CGSize ASFloorSizeValues(CGSize s) +{ + return CGSizeMake(ASFloorPixelValue(s.width), ASFloorPixelValue(s.height)); +} + +// See ASCeilPixelValue for a more thoroguh explanation of (f + FLT_EPSILON), +// but here is some quick math: +// +// Imagine a layout that comes back with a height of 100.66666666663 +// for a 3x deice: +// 100.66666666663 * 3 = 301.99999999988995 +// floor(301.99999999988995) = 301 +// 301 / 3 = 100.333333333 +// +// If we add FLT_EPSILON to normalize the garbage at the end we get: +// po (100.66666666663 + FLT_EPSILON) * 3 = 302.00000035751782 +// floor(302.00000035751782) = 302 +// 302/3 = 100.66666666 +CGFloat ASFloorPixelValue(CGFloat f) +{ + CGFloat scale = ASScreenScale(); + return floor((f + FLT_EPSILON) * scale) / scale; +} + +CGPoint ASCeilPointValues(CGPoint p) +{ + return CGPointMake(ASCeilPixelValue(p.x), ASCeilPixelValue(p.y)); +} + +CGSize ASCeilSizeValues(CGSize s) +{ + return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height)); +} + +// With 3x devices layouts will often to compute to pixel bounds but +// include garbage values beyond the precision of a float/double. +// This garbage can result in a pixel value being rounded up when it isn't +// necessary. +// +// For example, imagine a layout that comes back with a height of 100.666666666669 +// for a 3x device: +// 100.666666666669 * 3 = 302.00000000000699 +// ceil(302.00000000000699) = 303 +// 303/3 = 101 +// +// If we use FLT_EPSILON to get rid of the garbage at the end of the value, +// things work as expected: +// (100.666666666669 - FLT_EPSILON) * 3 = 301.99999964237912 +// ceil(301.99999964237912) = 302 +// 302/3 = 100.666666666 +// +// For even more conversation around this, see: +// https://github.com/TextureGroup/Texture/issues/838 +CGFloat ASCeilPixelValue(CGFloat f) +{ + CGFloat scale = ASScreenScale(); + return ceil((f - FLT_EPSILON) * scale) / scale; +} + +CGFloat ASRoundPixelValue(CGFloat f) +{ + CGFloat scale = ASScreenScale(); + return round(f * scale) / scale; +} + +@implementation NSIndexPath (ASInverseComparison) + +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath +{ + return [otherIndexPath compare:self]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASLayerBackingTipProvider.h b/submodules/AsyncDisplayKit/Source/Private/ASLayerBackingTipProvider.h new file mode 100644 index 0000000000..0cc3d03b61 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASLayerBackingTipProvider.h @@ -0,0 +1,24 @@ +// +// ASLayerBackingTipProvider.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTipProvider.h" +#import + +#if AS_ENABLE_TIPS + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASLayerBackingTipProvider : ASTipProvider + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASLayerBackingTipProvider.mm b/submodules/AsyncDisplayKit/Source/Private/ASLayerBackingTipProvider.mm new file mode 100644 index 0000000000..19d1850ab7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASLayerBackingTipProvider.mm @@ -0,0 +1,45 @@ +// +// ASLayerBackingTipProvider.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayerBackingTipProvider.h" + +#if AS_ENABLE_TIPS + +#import +#import +#import +#import +#import + +@implementation ASLayerBackingTipProvider + +- (ASTip *)tipForNode:(ASDisplayNode *)node +{ + // Already layer-backed. + if (node.layerBacked) { + return nil; + } + + // TODO: Avoid revisiting nodes we already visited + ASDisplayNode *failNode = ASDisplayNodeFindFirstNode(node, ^BOOL(ASDisplayNode * _Nonnull node) { + return !node.supportsLayerBacking; + }); + if (failNode != nil) { + return nil; + } + + ASTip *result = [[ASTip alloc] initWithNode:node + kind:ASTipKindEnableLayerBacking + format:@"Enable layer backing to improve performance"]; + return result; +} + +@end + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASLayoutTransition.h b/submodules/AsyncDisplayKit/Source/Private/ASLayoutTransition.h new file mode 100644 index 0000000000..f2bcf9b253 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASLayoutTransition.h @@ -0,0 +1,93 @@ +// +// ASLayoutTransition.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - ASLayoutElementTransition + +/** + * Objects conform to this project returns if it's possible to layout asynchronous + */ +@protocol ASLayoutElementTransition + +/** + * @abstract Returns if the layoutElement can be used to layout in an asynchronous way on a background thread. + */ +@property (nonatomic, readonly) BOOL canLayoutAsynchronous; + +@end + +@interface ASDisplayNode () +@end +@interface ASLayoutSpec () +@end + + +#pragma mark - ASLayoutTransition + +AS_SUBCLASSING_RESTRICTED +@interface ASLayoutTransition : NSObject <_ASTransitionContextLayoutDelegate> + +/** + * Node to apply layout transition on + */ +@property (nonatomic, weak, readonly) ASDisplayNode *node; + +/** + * Previous layout to transition from + */ +@property (nonatomic, readonly) const ASDisplayNodeLayout &previousLayout NS_RETURNS_INNER_POINTER; + +/** + * Pending layout to transition to + */ +@property (nonatomic, readonly) const ASDisplayNodeLayout &pendingLayout NS_RETURNS_INNER_POINTER; + +/** + * Returns if the layout transition needs to happen synchronously + */ +@property (nonatomic, readonly) BOOL isSynchronous; + +/** + * Returns a newly initialized layout transition + */ +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(const ASDisplayNodeLayout &)pendingLayout + previousLayout:(const ASDisplayNodeLayout &)previousLayout NS_DESIGNATED_INITIALIZER; + +/** + * Insert and remove subnodes that were added or removed between the previousLayout and the pendingLayout + */ +- (void)commitTransition; + +/** + * Insert all new subnodes that were added and move the subnodes that moved between the previous layout and + * the pending layout. + */ +- (void)applySubnodeInsertionsAndMoves; + +/** + * Remove all subnodes that are removed between the previous layout and the pending layout + */ +- (void)applySubnodeRemovals; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASLayoutTransition.mm b/submodules/AsyncDisplayKit/Source/Private/ASLayoutTransition.mm new file mode 100644 index 0000000000..2fea2b8ad0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASLayoutTransition.mm @@ -0,0 +1,302 @@ +// +// ASLayoutTransition.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import // Required for _insertSubnode... / _removeFromSupernode. +#import + +#import + +#if AS_IG_LIST_KIT +#import +#import +#endif + +using AS::MutexLocker; + +/** + * Search the whole layout stack if at least one layout has a layoutElement object that can not be layed out asynchronous. + * This can be the case for example if a node was already loaded + */ +static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) { + // Queue used to keep track of sublayouts while traversing this layout in a BFS fashion. + std::queue queue; + queue.push(layout); + + while (!queue.empty()) { + layout = queue.front(); + queue.pop(); + +#if DEBUG + ASDisplayNodeCAssert([layout.layoutElement conformsToProtocol:@protocol(ASLayoutElementTransition)], @"ASLayoutElement in a layout transition needs to conforms to the ASLayoutElementTransition protocol."); +#endif + if (((id)layout.layoutElement).canLayoutAsynchronous == NO) { + return NO; + } + + // Add all sublayouts to process in next step + for (ASLayout *sublayout in layout.sublayouts) { + queue.push(sublayout); + } + } + + return YES; +} + +@implementation ASLayoutTransition { + std::shared_ptr __instanceLock__; + + BOOL _calculatedSubnodeOperations; + NSArray *_insertedSubnodes; + NSArray *_removedSubnodes; + std::vector _insertedSubnodePositions; + std::vector> _subnodeMoves; + ASDisplayNodeLayout _pendingLayout; + ASDisplayNodeLayout _previousLayout; +} + +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(const ASDisplayNodeLayout &)pendingLayout + previousLayout:(const ASDisplayNodeLayout &)previousLayout +{ + self = [super init]; + if (self) { + __instanceLock__ = std::make_shared(); + + _node = node; + _pendingLayout = pendingLayout; + _previousLayout = previousLayout; + } + return self; +} + +- (BOOL)isSynchronous +{ + MutexLocker l(*__instanceLock__); + return !ASLayoutCanTransitionAsynchronous(_pendingLayout.layout); +} + +- (void)commitTransition +{ + [self applySubnodeRemovals]; + [self applySubnodeInsertionsAndMoves]; +} + +- (void)applySubnodeInsertionsAndMoves +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + + // Create an activity even if no subnodes affected. + as_activity_create_for_scope("Apply subnode insertions and moves"); + if (_insertedSubnodePositions.size() == 0 && _subnodeMoves.size() == 0) { + return; + } + + ASDisplayNodeLogEvent(_node, @"insertSubnodes: %@", _insertedSubnodes); + NSUInteger i = 0; + NSUInteger j = 0; + for (auto const &move : _subnodeMoves) { + [move.first _removeFromSupernodeIfEqualTo:_node]; + } + j = 0; + while (i < _insertedSubnodePositions.size() && j < _subnodeMoves.size()) { + NSUInteger p = _insertedSubnodePositions[i]; + NSUInteger q = _subnodeMoves[j].second; + if (p < q) { + [_node _insertSubnode:_insertedSubnodes[i] atIndex:p]; + i++; + } else { + [_node _insertSubnode:_subnodeMoves[j].first atIndex:q]; + j++; + } + } + for (; i < _insertedSubnodePositions.size(); ++i) { + [_node _insertSubnode:_insertedSubnodes[i] atIndex:_insertedSubnodePositions[i]]; + } + for (; j < _subnodeMoves.size(); ++j) { + [_node _insertSubnode:_subnodeMoves[j].first atIndex:_subnodeMoves[j].second]; + } +} + +- (void)applySubnodeRemovals +{ + as_activity_scope(as_activity_create("Apply subnode removals", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + + if (_removedSubnodes.count == 0) { + return; + } + + ASDisplayNodeLogEvent(_node, @"removeSubnodes: %@", _removedSubnodes); + for (ASDisplayNode *subnode in _removedSubnodes) { + // In this case we should only remove the subnode if it's still a subnode of the _node that executes a layout transition. + // It can happen that a node already did a layout transition and added this subnode, in this case the subnode + // would be removed from the new node instead of _node + if (_node.automaticallyManagesSubnodes) { + [subnode _removeFromSupernodeIfEqualTo:_node]; + } + } +} + +- (void)calculateSubnodeOperationsIfNeeded +{ + MutexLocker l(*__instanceLock__); + if (_calculatedSubnodeOperations) { + return; + } + + // Create an activity even if no subnodes affected. + as_activity_create_for_scope("Calculate subnode operations"); + ASLayout *previousLayout = _previousLayout.layout; + ASLayout *pendingLayout = _pendingLayout.layout; + + if (previousLayout) { +#if AS_IG_LIST_KIT + // IGListDiff completes in linear time O(m+n), so use it if we have it: + IGListIndexSetResult *result = IGListDiff(previousLayout.sublayouts, pendingLayout.sublayouts, IGListDiffEquality); + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, result.inserts, &_insertedSubnodes); + findNodesInLayoutAtIndexes(previousLayout, result.deletes, &_removedSubnodes); + for (IGListMoveIndex *move in result.moves) { + _subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[move.from].layoutElement, move.to)); + } + + // Sort by ascending order of move destinations, this will allow easy loop of `insertSubnode:AtIndex` later. + std::sort(_subnodeMoves.begin(), _subnodeMoves.end(), [](std::pair, NSUInteger> a, + std::pair b) { + return a.second < b.second; + }); +#else + NSIndexSet *insertions, *deletions; + NSArray *moves; + NSArray *previousNodes = [previousLayout.sublayouts valueForKey:@"layoutElement"]; + NSArray *pendingNodes = [pendingLayout.sublayouts valueForKey:@"layoutElement"]; + [previousNodes asdk_diffWithArray:pendingNodes + insertions:&insertions + deletions:&deletions + moves:&moves]; + + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, insertions, &_insertedSubnodes); + _removedSubnodes = [previousNodes objectsAtIndexes:deletions]; + // These should arrive sorted in ascending order of move destinations. + for (NSIndexPath *move in moves) { + _subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[([move indexAtPosition:0])].layoutElement, + [move indexAtPosition:1])); + } +#endif + } else { + NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [pendingLayout.sublayouts count])]; + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, indexes, &_insertedSubnodes); + _removedSubnodes = nil; + } + _calculatedSubnodeOperations = YES; +} + +#pragma mark - _ASTransitionContextDelegate + +- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + MutexLocker l(*__instanceLock__); + return _node.subnodes; +} + +- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + return _insertedSubnodes; +} + +- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + return _removedSubnodes; +} + +- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key +{ + MutexLocker l(*__instanceLock__); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousLayout.layout; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingLayout.layout; + } else { + return nil; + } +} + +- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key +{ + MutexLocker l(*__instanceLock__); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousLayout.constrainedSize; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingLayout.constrainedSize; + } else { + return ASSizeRangeMake(CGSizeZero, CGSizeZero); + } +} + +#pragma mark - Filter helpers + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + */ +static inline std::vector findNodesInLayoutAtIndexes(ASLayout *layout, + NSIndexSet *indexes, + NSArray * __strong *storedNodes) +{ + return findNodesInLayoutAtIndexesWithFilteredNodes(layout, indexes, nil, storedNodes); +} + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + * Call only with a flattened layout. + * @discussion If the node exists in the `filteredNodes` array, the node is not added to `storedNodes`. + */ +static inline std::vector findNodesInLayoutAtIndexesWithFilteredNodes(ASLayout *layout, + NSIndexSet *indexes, + NSArray *filteredNodes, + NSArray * __strong *storedNodes) +{ + NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:indexes.count]; + std::vector positions = std::vector(); + + // From inspection, this is how enumerateObjectsAtIndexes: works under the hood + NSUInteger firstIndex = indexes.firstIndex; + NSUInteger lastIndex = indexes.lastIndex; + NSUInteger idx = 0; + for (ASLayout *sublayout in layout.sublayouts) { + if (idx > lastIndex) { break; } + if (idx >= firstIndex && [indexes containsIndex:idx]) { + ASDisplayNode *node = (ASDisplayNode *)(sublayout.layoutElement); + ASDisplayNodeCAssert(node, @"ASDisplayNode was deallocated before it was added to a subnode. It's likely the case that you use automatically manages subnodes and allocate a ASDisplayNode in layoutSpecThatFits: and don't have any strong reference to it."); + ASDisplayNodeCAssert([node isKindOfClass:[ASDisplayNode class]], @"sublayout is an ASLayout, but not an ASDisplayNode - only call findNodesInLayoutAtIndexesWithFilteredNodes with a flattened layout (all sublayouts are ASDisplayNodes)."); + if (node != nil) { + BOOL notFiltered = (filteredNodes == nil || [filteredNodes indexOfObjectIdenticalTo:node] == NSNotFound); + if (notFiltered) { + [nodes addObject:node]; + positions.push_back(idx); + } + } + } + idx += 1; + } + *storedNodes = nodes; + + return positions; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASMutableElementMap.h b/submodules/AsyncDisplayKit/Source/Private/ASMutableElementMap.h new file mode 100644 index 0000000000..ed4f0b5ff4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASMutableElementMap.h @@ -0,0 +1,66 @@ +// +// ASMutableElementMap.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASSection, ASCollectionElement, _ASHierarchyChangeSet; + +/** + * This mutable version will be removed in the future. It's only here now to keep the diff small + * as we port data controller to use ASElementMap. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASMutableElementMap : NSObject + +- (instancetype)init __unavailable; + +- (instancetype)initWithSections:(NSArray *)sections items:(ASCollectionElementTwoDimensionalArray *)items supplementaryElements:(ASSupplementaryElementDictionary *)supplementaryElements; + +- (void)insertSection:(ASSection *)section atIndex:(NSInteger)index; + +- (void)removeAllSections; + +/// Only modifies the array of ASSection * objects +- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes; + +- (void)removeAllElements; + +- (void)removeItemsAtIndexPaths:(NSArray *)indexPaths; + +- (void)removeSectionsOfItems:(NSIndexSet *)itemSections; + +- (void)removeSupplementaryElementsAtIndexPaths:(NSArray *)indexPaths kind:(NSString *)kind; + +- (void)insertEmptySectionsOfItemsAtIndexes:(NSIndexSet *)sections; + +- (void)insertElement:(ASCollectionElement *)element atIndexPath:(NSIndexPath *)indexPath; + +/** + * Update the index paths for all supplementary elements to account for section-level + * deletes, moves, inserts. This must be called before adding new supplementary elements. + * + * This also deletes any supplementary elements in deleted sections. + */ +- (void)migrateSupplementaryElementsWithSectionMapping:(ASIntegerMap *)mapping; + +@end + +@interface ASElementMap (MutableCopying) +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASMutableElementMap.mm b/submodules/AsyncDisplayKit/Source/Private/ASMutableElementMap.mm new file mode 100644 index 0000000000..73798be119 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASMutableElementMap.mm @@ -0,0 +1,149 @@ +// +// ASMutableElementMap.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import + +typedef NSMutableArray *> ASMutableCollectionElementTwoDimensionalArray; + +typedef NSMutableDictionary *> ASMutableSupplementaryElementDictionary; + +@implementation ASMutableElementMap { + ASMutableSupplementaryElementDictionary *_supplementaryElements; + NSMutableArray *_sections; + ASMutableCollectionElementTwoDimensionalArray *_sectionsOfItems; +} + +- (instancetype)initWithSections:(NSArray *)sections items:(ASCollectionElementTwoDimensionalArray *)items supplementaryElements:(ASSupplementaryElementDictionary *)supplementaryElements +{ + if (self = [super init]) { + _sections = [sections mutableCopy]; + _sectionsOfItems = (ASMutableCollectionElementTwoDimensionalArray *)ASTwoDimensionalArrayDeepMutableCopy(items); + _supplementaryElements = [ASMutableElementMap deepMutableCopyOfElementsDictionary:supplementaryElements]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + return [[ASElementMap alloc] initWithSections:_sections items:_sectionsOfItems supplementaryElements:_supplementaryElements]; +} + +- (void)removeAllSections +{ + [_sections removeAllObjects]; +} + +- (void)insertSection:(ASSection *)section atIndex:(NSInteger)index +{ + [_sections insertObject:section atIndex:index]; +} + +- (void)removeItemsAtIndexPaths:(NSArray *)indexPaths +{ + ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(_sectionsOfItems, indexPaths); +} + +- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes +{ + [_sections removeObjectsAtIndexes:indexes]; +} + +- (void)removeSupplementaryElementsAtIndexPaths:(NSArray *)indexPaths kind:(NSString *)kind +{ + [_supplementaryElements[kind] removeObjectsForKeys:indexPaths]; +} + +- (void)removeAllElements +{ + [_sectionsOfItems removeAllObjects]; + [_supplementaryElements removeAllObjects]; +} + +- (void)removeSectionsOfItems:(NSIndexSet *)itemSections +{ + [_sectionsOfItems removeObjectsAtIndexes:itemSections]; +} + +- (void)insertEmptySectionsOfItemsAtIndexes:(NSIndexSet *)sections +{ + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_sectionsOfItems insertObject:[[NSMutableArray alloc] init] atIndex:idx]; + }]; +} + +- (void)insertElement:(ASCollectionElement *)element atIndexPath:(NSIndexPath *)indexPath +{ + NSString *kind = element.supplementaryElementKind; + if (kind == nil) { + [_sectionsOfItems[indexPath.section] insertObject:element atIndex:indexPath.item]; + } else { + NSMutableDictionary *supplementariesForKind = _supplementaryElements[kind]; + if (supplementariesForKind == nil) { + supplementariesForKind = [[NSMutableDictionary alloc] init]; + _supplementaryElements[kind] = supplementariesForKind; + } + supplementariesForKind[indexPath] = element; + } +} + +- (void)migrateSupplementaryElementsWithSectionMapping:(ASIntegerMap *)mapping +{ + // Fast-path, no section changes. + if (mapping == ASIntegerMap.identityMap) { + return; + } + + // For each element kind, + [_supplementaryElements enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableDictionary * _Nonnull supps, BOOL * _Nonnull stop) { + + // For each index path of that kind, move entries into a new dictionary. + // Note: it's tempting to update the dictionary in-place but because of the likely collision between old and new index paths, + // subtle bugs are possible. Note that this process is rare (only on section-level updates), + // that this work is done off-main, and that the typical supplementary element use case is just 1-per-section (header). + NSMutableDictionary *newSupps = [[NSMutableDictionary alloc] init]; + [supps enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull oldIndexPath, ASCollectionElement * _Nonnull obj, BOOL * _Nonnull stop) { + NSInteger oldSection = oldIndexPath.section; + NSInteger newSection = [mapping integerForKey:oldSection]; + + if (oldSection == newSection) { + // Index path stayed the same, just copy it over. + newSupps[oldIndexPath] = obj; + } else if (newSection != NSNotFound) { + // Section index changed, move it. + NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:oldIndexPath.item inSection:newSection]; + newSupps[newIndexPath] = obj; + } + }]; + [supps setDictionary:newSupps]; + }]; +} + +#pragma mark - Helpers + ++ (ASMutableSupplementaryElementDictionary *)deepMutableCopyOfElementsDictionary:(ASSupplementaryElementDictionary *)originalDict +{ + NSMutableDictionary *deepCopy = [[NSMutableDictionary alloc] initWithCapacity:originalDict.count]; + [originalDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) { + deepCopy[key] = [obj mutableCopy]; + }]; + + return deepCopy; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASNetworkImageLoadInfo+Private.h b/submodules/AsyncDisplayKit/Source/Private/ASNetworkImageLoadInfo+Private.h new file mode 100644 index 0000000000..36e423b535 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASNetworkImageLoadInfo+Private.h @@ -0,0 +1,22 @@ +// +// ASNetworkImageLoadInfo+Private.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASNetworkImageLoadInfo () + +- (instancetype)initWithURL:(NSURL *)url + sourceType:(ASNetworkImageSourceType)sourceType + downloadIdentifier:(nullable id)downloadIdentifier + userInfo:(nullable id)userInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASPendingStateController.h b/submodules/AsyncDisplayKit/Source/Private/ASPendingStateController.h new file mode 100644 index 0000000000..b50a7c1f94 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASPendingStateController.h @@ -0,0 +1,50 @@ +// +// ASPendingStateController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@class ASDisplayNode; + +NS_ASSUME_NONNULL_BEGIN + +/** + A singleton that is responsible for applying changes to + UIView/CALayer properties of display nodes when they + have been set on background threads. + + This controller will enqueue run-loop events to flush changes + but if you need them flushed now you can call `flush` from the main thread. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASPendingStateController : NSObject + ++ (ASPendingStateController *)sharedInstance; + +@property (nonatomic, readonly) BOOL hasChanges; + +/** + Flush all pending states for nodes now. Any UIView/CALayer properties + that have been set in the background will be applied to their + corresponding views/layers before this method returns. + + You must call this method on the main thread. + */ +- (void)flush; + +/** + Register this node as having pending state that needs to be copied + over to the view/layer. This is called automatically by display nodes + when their view/layer properties are set post-load on background threads. + */ +- (void)registerNode:(ASDisplayNode *)node; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASPendingStateController.mm b/submodules/AsyncDisplayKit/Source/Private/ASPendingStateController.mm new file mode 100644 index 0000000000..269b37e948 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASPendingStateController.mm @@ -0,0 +1,102 @@ +// +// ASPendingStateController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import // Required for -applyPendingViewState; consider moving this to +FrameworkPrivate + +@interface ASPendingStateController() +{ + AS::Mutex _lock; + + struct ASPendingStateControllerFlags { + unsigned pendingFlush:1; + } _flags; +} + +@property (nonatomic, readonly) ASWeakSet *dirtyNodes; +@end + +@implementation ASPendingStateController + +#pragma mark Lifecycle & Singleton + +- (instancetype)init +{ + self = [super init]; + if (self) { + _dirtyNodes = [[ASWeakSet alloc] init]; + } + return self; +} + ++ (ASPendingStateController *)sharedInstance +{ + static dispatch_once_t onceToken; + static ASPendingStateController *controller = nil; + dispatch_once(&onceToken, ^{ + controller = [[ASPendingStateController alloc] init]; + }); + return controller; +} + +#pragma mark External API + +- (void)registerNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssert(node.nodeLoaded, @"Expected display node to be loaded before it was registered with ASPendingStateController. Node: %@", node); + AS::MutexLocker l(_lock); + [_dirtyNodes addObject:node]; + + [self scheduleFlushIfNeeded]; +} + +- (void)flush +{ + ASDisplayNodeAssertMainThread(); + _lock.lock(); + ASWeakSet *dirtyNodes = _dirtyNodes; + _dirtyNodes = [[ASWeakSet alloc] init]; + _flags.pendingFlush = NO; + _lock.unlock(); + + for (ASDisplayNode *node in dirtyNodes) { + [node applyPendingViewState]; + } +} + + +#pragma mark Private Methods + +/** + This method is assumed to be called with the lock held. + */ +- (void)scheduleFlushIfNeeded +{ + if (_flags.pendingFlush) { + return; + } + + _flags.pendingFlush = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + [self flush]; + }); +} + +@end + +@implementation ASPendingStateController (Testing) + +- (BOOL)test_isFlushScheduled +{ + return _flags.pendingFlush; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASResponderChainEnumerator.h b/submodules/AsyncDisplayKit/Source/Private/ASResponderChainEnumerator.h new file mode 100644 index 0000000000..dcd2e1c4c4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASResponderChainEnumerator.h @@ -0,0 +1,29 @@ +// +// ASResponderChainEnumerator.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASResponderChainEnumerator : NSEnumerator + +- (instancetype)initWithResponder:(UIResponder *)responder; + +@end + +@interface UIResponder (ASResponderChainEnumerator) + +- (ASResponderChainEnumerator *)asdk_responderChainEnumerator; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASResponderChainEnumerator.mm b/submodules/AsyncDisplayKit/Source/Private/ASResponderChainEnumerator.mm new file mode 100644 index 0000000000..bb16e0fc57 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASResponderChainEnumerator.mm @@ -0,0 +1,45 @@ +// +// ASResponderChainEnumerator.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASResponderChainEnumerator { + UIResponder *_currentResponder; +} + +- (instancetype)initWithResponder:(UIResponder *)responder +{ + ASDisplayNodeAssertMainThread(); + if (self = [super init]) { + _currentResponder = responder; + } + return self; +} + +#pragma mark - NSEnumerator + +- (id)nextObject +{ + ASDisplayNodeAssertMainThread(); + id result = [_currentResponder nextResponder]; + _currentResponder = result; + return result; +} + +@end + +@implementation UIResponder (ASResponderChainEnumerator) + +- (NSEnumerator *)asdk_responderChainEnumerator +{ + return [[ASResponderChainEnumerator alloc] initWithResponder:self]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/ASSection.h b/submodules/AsyncDisplayKit/Source/Private/ASSection.h new file mode 100644 index 0000000000..92b6a17170 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASSection.h @@ -0,0 +1,40 @@ +// +// ASSection.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +@protocol ASSectionContext; + +NS_ASSUME_NONNULL_BEGIN + +/** + * An object representing the metadata for a section of elements in a collection. + * + * Its sectionID is namespaced to the data controller that created the section. + * + * These are useful for tracking the movement & lifetime of sections, independent of + * their contents. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASSection : NSObject + +@property (readonly) NSInteger sectionID; +@property (nullable, readonly) id context; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithSectionID:(NSInteger)sectionID context:(nullable id)context NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASSection.mm b/submodules/AsyncDisplayKit/Source/Private/ASSection.mm new file mode 100644 index 0000000000..a151774bd4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASSection.mm @@ -0,0 +1,29 @@ +// +// ASSection.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import + +@implementation ASSection + +- (instancetype)initWithSectionID:(NSInteger)sectionID context:(id)context +{ + self = [super init]; + if (self) { + _sectionID = sectionID; + _context = context; + } + return self; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTableView+Undeprecated.h b/submodules/AsyncDisplayKit/Source/Private/ASTableView+Undeprecated.h new file mode 100644 index 0000000000..46c54368a6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTableView+Undeprecated.h @@ -0,0 +1,298 @@ +// +// ASTableView+Undeprecated.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Currently our public table API is on @c ASTableNode and the @c ASTableView + * API is deprecated, but the implementations still live in the view. + * + * This category lets us avoid deprecation warnings everywhere internally. + * In the future, the ASTableView public API will be eliminated and so will this file. + */ +@interface ASTableView (Undeprecated) + +@property (nullable, nonatomic, weak) id asyncDelegate; +@property (nullable, nonatomic, weak) id asyncDataSource; +@property (nonatomic) UIEdgeInsets contentInset; +@property (nonatomic) CGPoint contentOffset; +@property (nonatomic) BOOL automaticallyAdjustsContentOffset; +@property (nonatomic) BOOL inverted; +@property (nullable, nonatomic, readonly) NSArray *indexPathsForVisibleRows; +@property (nullable, nonatomic, readonly) NSArray *indexPathsForSelectedRows; +@property (nullable, nonatomic, readonly) NSIndexPath *indexPathForSelectedRow; + +/** + * The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called. + * + * Defaults to two screenfuls. + */ +@property (nonatomic) CGFloat leadingScreensForBatching; + +/** + * Initializer. + * + * @param frame A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. + * The frame of the table view changes as table cells are added and deleted. + * + * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. + */ +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style; + +/** + * Tuning parameters for a range type in full mode. + * + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in full mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in full mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType; + +/** + * Tuning parameters for a range type in the specified mode. + * + * @param rangeMode The range mode to get the running parameters for. + * @param rangeType The range type to get the tuning parameters for. + * + * @return A tuning parameter value for the given range type in the given mode. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType AS_WARN_UNUSED_RESULT; + +/** + * Set the tuning parameters for a range type in the specified mode. + * + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeMode The range mode to set the running parameters for. + * @param rangeType The range type to set the tuning parameters for. + * + * @see ASLayoutRangeMode + * @see ASLayoutRangeType + */ +- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; + +- (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Scrolls the table to the given row. + * + * @param indexPath The index path of the row. + * @param scrollPosition Where the row should end up after the scroll. + * @param animated Whether the scroll should be animated or not. + */ +- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated; + +- (void)selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition; + +- (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point; + +- (nullable NSArray *)indexPathsForRowsInRect:(CGRect)rect; + +/** + * Similar to -visibleCells. + * + * @return an array containing the cell nodes being displayed on screen. + */ +- (NSArray *)visibleNodes AS_WARN_UNUSED_RESULT; + +/** + * Similar to -indexPathForCell:. + * + * @param cellNode a cellNode part of the table view + * + * @return an indexPath for this cellNode + */ +- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @param completion block to run on completion of asynchronous loading or nil. If supplied, the block is run on + * the main thread. + * @warning This method is substantially more expensive than UITableView's version. + */ +-(void)reloadDataWithCompletion:(void (^ _Nullable)(void))completion; + +/** + * Reload everything from scratch, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UITableView's version. + */ +- (void)reloadData; + +/** + * Triggers a relayout of all nodes. + * + * @discussion This method invalidates and lays out every cell node in the table view. + */ +- (void)relayoutItems; + +/** + * Begins a series of method calls that insert, delete, select, or reload rows and sections of the table view, with animation enabled and no completion block. + * + * @discussion You call this method to bracket a series of method calls that ends with endUpdates and that consists of operations + * to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating + * the operations simultaneously. It's important to remember that the ASTableView will be processing the updates asynchronously after this call is completed. + * + * @warning This method must be called from the main thread. + */ +- (void)beginUpdates; + +/** + * Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view, with animation enabled and no completion block. + * + * @discussion You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations + * to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating + * the operations simultaneously. It's important to remember that the ASTableView will be processing the updates asynchronously after this call is completed. + * + * @warning This method is must be called from the main thread. + */ +- (void)endUpdates; + +/** + * Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view. + * You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations + * to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating + * the operations simultaneously. This method is must be called from the main thread. It's important to remember that the ASTableView will + * be processing the updates asynchronously after this call and are not guaranteed to be reflected in the ASTableView until + * the completion block is executed. + * + * @param animated NO to disable all animations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL completed))completion; + +/** + * Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread. + */ +- (void)waitUntilAllUpdatesAreCommitted; + +/** + * Inserts one or more sections, with an option to animate the insertion. + * + * @param sections An index set that specifies the sections to insert. + * + * @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Deletes one or more sections, with an option to animate the deletion. + * + * @param sections An index set that specifies the sections to delete. + * + * @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Reloads the specified sections using a given animation effect. + * + * @param sections An index set that specifies the sections to reload. + * + * @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; + +/** + * Inserts rows at the locations identified by an array of index paths, with an option to animate the insertion. + * + * @param indexPaths An array of NSIndexPath objects, each representing a row index and section index that together identify a row. + * + * @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Deletes the rows specified by an array of index paths, with an option to animate the deletion. + * + * @param indexPaths An array of NSIndexPath objects identifying the rows to delete. + * + * @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Reloads the specified rows using a given animation effect. + * + * @param indexPaths An array of NSIndexPath objects identifying the rows to reload. + * + * @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Moves the row at a specified location to a destination location. + * + * @param indexPath The index path identifying the row to move. + * + * @param newIndexPath The index path that is the destination of the move for the row. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + +@end +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTip.h b/submodules/AsyncDisplayKit/Source/Private/ASTip.h new file mode 100644 index 0000000000..5ac6ac18bc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTip.h @@ -0,0 +1,49 @@ +// +// ASTip.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_ENABLE_TIPS + +NS_ASSUME_NONNULL_BEGIN + +@class ASDisplayNode; + +typedef NS_ENUM (NSInteger, ASTipKind) { + ASTipKindEnableLayerBacking +}; + +AS_SUBCLASSING_RESTRICTED +@interface ASTip : NSObject + +- (instancetype)initWithNode:(ASDisplayNode *)node + kind:(ASTipKind)kind + format:(NSString *)format, ... NS_FORMAT_FUNCTION(3, 4); + +/** + * The kind of tip this is. + */ +@property (nonatomic, readonly) ASTipKind kind; + +/** + * The node that this tip applies to. + */ +@property (nonatomic, readonly) ASDisplayNode *node; + +/** + * The text to show the user. + */ +@property (nonatomic, readonly) NSString *text; + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTip.mm b/submodules/AsyncDisplayKit/Source/Private/ASTip.mm new file mode 100644 index 0000000000..af1d299221 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTip.mm @@ -0,0 +1,35 @@ +// +// ASTip.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TIPS + +#import + +@implementation ASTip + +- (instancetype)initWithNode:(ASDisplayNode *)node + kind:(ASTipKind)kind + format:(NSString *)format, ... +{ + if (self = [super init]) { + _node = node; + _kind = kind; + va_list args; + va_start(args, format); + _text = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + } + return self; +} + +@end + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipNode.h b/submodules/AsyncDisplayKit/Source/Private/ASTipNode.h new file mode 100644 index 0000000000..d01637d869 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipNode.h @@ -0,0 +1,39 @@ +// +// ASTipNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_ENABLE_TIPS + +@class ASTip; + +NS_ASSUME_NONNULL_BEGIN + +/** + * ASTipNode will send these up the responder chain. + */ +@protocol ASTipNodeActions +- (void)didTapTipNode:(id)sender; +@end + +AS_SUBCLASSING_RESTRICTED +@interface ASTipNode : ASControlNode + +- (instancetype)initWithTip:(ASTip *)tip NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@property (nonatomic, readonly) ASTip *tip; + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipNode.mm b/submodules/AsyncDisplayKit/Source/Private/ASTipNode.mm new file mode 100644 index 0000000000..dcca908b8a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipNode.mm @@ -0,0 +1,28 @@ +// +// ASTipNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTipNode.h" + +#if AS_ENABLE_TIPS + +@implementation ASTipNode + +- (instancetype)initWithTip:(ASTip *)tip +{ + if (self = [super init]) { + self.backgroundColor = [UIColor colorWithRed:0 green:0.7 blue:0.2 alpha:0.3]; + _tip = tip; + [self addTarget:nil action:@selector(didTapTipNode:) forControlEvents:ASControlNodeEventTouchUpInside]; + } + return self; +} + +@end + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipProvider.h b/submodules/AsyncDisplayKit/Source/Private/ASTipProvider.h new file mode 100644 index 0000000000..e2aba6c5d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipProvider.h @@ -0,0 +1,42 @@ +// +// ASTipProvider.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_ENABLE_TIPS + +@class ASDisplayNode, ASTip; + +NS_ASSUME_NONNULL_BEGIN + +/** + * An abstract superclass for all tip providers. + */ +@interface ASTipProvider : NSObject + +/** + * The provider looks at the node's current situation and + * generates a tip, if any, to add to the node. + * + * Subclasses must override this. + */ +- (nullable ASTip *)tipForNode:(ASDisplayNode *)node; + +@end + +@interface ASTipProvider (Lookup) + +@property (class, nonatomic, copy, readonly) NSArray<__kindof ASTipProvider *> *all; + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipProvider.mm b/submodules/AsyncDisplayKit/Source/Private/ASTipProvider.mm new file mode 100644 index 0000000000..237e83cda0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipProvider.mm @@ -0,0 +1,43 @@ +// +// ASTipProvider.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TIPS + +#import + +// Concrete classes +#import + +@implementation ASTipProvider + +- (ASTip *)tipForNode:(ASDisplayNode *)node +{ + ASDisplayNodeFailAssert(@"Subclasses must override %@", NSStringFromSelector(_cmd)); + return nil; +} + +@end + +@implementation ASTipProvider (Lookup) + ++ (NSArray *)all +{ + static NSArray *providers; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + providers = @[ [ASLayerBackingTipProvider new] ]; + }); + return providers; +} + +@end + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipsController.h b/submodules/AsyncDisplayKit/Source/Private/ASTipsController.h new file mode 100644 index 0000000000..fceeb92f60 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipsController.h @@ -0,0 +1,40 @@ +// +// ASTipsController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_ENABLE_TIPS + +@class ASDisplayNode; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTipsController : NSObject + +/** + * The shared tip controller instance. + */ +@property (class, readonly) ASTipsController *shared; + +#pragma mark - Node Event Hooks + +/** + * Informs the controller that the sender did enter the visible range. + * + * The controller will run a pass with its tip providers, adding tips as needed. + */ +- (void)nodeDidAppear:(ASDisplayNode *)node; + +@end + +NS_ASSUME_NONNULL_END + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipsController.mm b/submodules/AsyncDisplayKit/Source/Private/ASTipsController.mm new file mode 100644 index 0000000000..acc592130c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipsController.mm @@ -0,0 +1,185 @@ +// +// ASTipsController.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TIPS + +#import +#import +#import +#import +#import +#import + +@interface ASTipsController () + +/// Nil on init, updates to most recent visible window. +@property (nonatomic) UIWindow *appVisibleWindow; + +/// Nil until an application window has become visible. +@property (nonatomic) ASTipsWindow *tipWindow; + +/// Main-thread-only. +@property (nonatomic, readonly) NSMapTable *nodeToTipStates; + +@property (nonatomic) NSMutableArray *nodesThatAppearedDuringRunLoop; + +@end + +@implementation ASTipsController + +#pragma mark - Singleton + ++ (void)load +{ + [NSNotificationCenter.defaultCenter addObserver:self.shared + selector:@selector(windowDidBecomeVisibleWithNotification:) + name:UIWindowDidBecomeVisibleNotification + object:nil]; +} + ++ (ASTipsController *)shared +{ + static ASTipsController *ctrl; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ctrl = [[ASTipsController alloc] init]; + }); + return ctrl; +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + ASDisplayNodeAssertMainThread(); + if (self = [super init]) { + _nodeToTipStates = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality) valueOptions:NSPointerFunctionsStrongMemory]; + _nodesThatAppearedDuringRunLoop = [NSMutableArray array]; + } + return self; +} + +#pragma mark - Event Handling + +- (void)nodeDidAppear:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + // If they disabled tips on this class, bail. + if (![[node class] enableTips]) { + return; + } + + // If this node appeared in some other window (like our tips window) ignore it. + if (ASFindWindowOfLayer(node.layer) != self.appVisibleWindow) { + return; + } + + [_nodesThatAppearedDuringRunLoop addObject:node]; +} + +// If this is a main window, start watching it and clear out our tip window. +- (void)windowDidBecomeVisibleWithNotification:(NSNotification *)notification +{ + ASDisplayNodeAssertMainThread(); + UIWindow *window = notification.object; + + // If this is the same window we're already watching, bail. + if (window == self.appVisibleWindow) { + return; + } + + // Ignore windows that are not at the normal level or have empty bounds + if (window.windowLevel != UIWindowLevelNormal || CGRectIsEmpty(window.bounds)) { + return; + } + + self.appVisibleWindow = window; + + // Create the tip window if needed. + [self createTipWindowIfNeededWithFrame:window.bounds]; + + // Clear out our tip window and reset our states. + self.tipWindow.mainWindow = window; + [_nodeToTipStates removeAllObjects]; +} + +- (void)runLoopDidTick +{ + NSArray *nodes = [_nodesThatAppearedDuringRunLoop copy]; + [_nodesThatAppearedDuringRunLoop removeAllObjects]; + + // Go through the old array, removing any that have tips but aren't still visible. + for (ASDisplayNode *node in [_nodeToTipStates copy]) { + if (!node.visible) { + [_nodeToTipStates removeObjectForKey:node]; + } + } + + for (ASDisplayNode *node in nodes) { + // Get the tip state for the node. + ASDisplayNodeTipState *tipState = [_nodeToTipStates objectForKey:node]; + + // If the node already has a tip, bail. This could change. + if (tipState.tipNode != nil) { + return; + } + + for (ASTipProvider *provider in ASTipProvider.all) { + ASTip *tip = [provider tipForNode:node]; + if (!tip) { continue; } + + if (!tipState) { + tipState = [self createTipStateForNode:node]; + } + tipState.tipNode = [[ASTipNode alloc] initWithTip:tip]; + } + } + self.tipWindow.nodeToTipStates = _nodeToTipStates; + [self.tipWindow setNeedsLayout]; +} + +#pragma mark - Internal + +- (void)createTipWindowIfNeededWithFrame:(CGRect)tipWindowFrame +{ + // Lots of property accesses, but simple safe code, only run once. + if (self.tipWindow == nil) { + self.tipWindow = [[ASTipsWindow alloc] initWithFrame:tipWindowFrame]; + self.tipWindow.hidden = NO; + [self setupRunLoopObserver]; + } +} + +/** + * In order to keep the UI updated, the tips controller registers a run loop observer. + * Before the transaction commit happens, the tips controller calls -setNeedsLayout + * on the view controller's view. It will then layout the main window, and then update the frames + * for tip nodes accordingly. + */ +- (void)setupRunLoopObserver +{ + CFRunLoopObserverRef o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + [self runLoopDidTick]; + }); + CFRunLoopAddObserver(CFRunLoopGetMain(), o, kCFRunLoopCommonModes); +} + +- (ASDisplayNodeTipState *)createTipStateForNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeTipState *tipState = [[ASDisplayNodeTipState alloc] initWithNode:node]; + [_nodeToTipStates setObject:tipState forKey:node]; + return tipState; +} + +@end + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipsWindow.h b/submodules/AsyncDisplayKit/Source/Private/ASTipsWindow.h new file mode 100644 index 0000000000..fab52510fc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipsWindow.h @@ -0,0 +1,36 @@ +// +// ASTipsWindow.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_ENABLE_TIPS + +@class ASDisplayNode, ASDisplayNodeTipState; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A window that shows tips. This was originally meant to be a view controller + * but UIKit will not manage view controllers in non-key windows correctly AT ALL + * as of the time of this writing. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTipsWindow : UIWindow + +/// The main application window that the tips are tracking. +@property (nonatomic, weak) UIWindow *mainWindow; + +@property (nonatomic, copy, nullable) NSMapTable *nodeToTipStates; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTipsWindow.mm b/submodules/AsyncDisplayKit/Source/Private/ASTipsWindow.mm new file mode 100644 index 0000000000..010932613a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTipsWindow.mm @@ -0,0 +1,98 @@ +// +// ASTipsWindow.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#if AS_ENABLE_TIPS + +#import +#import +#import +#import + +@interface ASTipsWindow () +@property (nonatomic, readonly) ASDisplayNode *node; +@end + +@implementation ASTipsWindow + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + /** + * UIKit throws an exception if you don't add a root view controller to a window, + * but if the window isn't key, then it doesn't manage the root view controller correctly! + * + * So we set a dummy root view controller and hide it. + */ + self.rootViewController = [UIViewController new]; + self.rootViewController.view.hidden = YES; + + _node = [[ASDisplayNode alloc] init]; + [self addSubnode:_node]; + + self.windowLevel = UIWindowLevelNormal + 1; + self.opaque = NO; + } + return self; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *result = [super hitTest:point withEvent:event]; + // Ignore touches unless they hit one of my node's subnodes + if (result == _node.view) { + return nil; + } + return result; +} + +- (void)setMainWindow:(UIWindow *)mainWindow +{ + _mainWindow = mainWindow; + for (ASDisplayNode *node in _node.subnodes) { + [node removeFromSupernode]; + } +} + +- (void)didTapTipNode:(ASTipNode *)tipNode +{ + ASDisplayNode.tipDisplayBlock(tipNode.tip.node, tipNode.tip.text); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + _node.frame = self.bounds; + + // Ensure the main window is laid out first. + [self.mainWindow layoutIfNeeded]; + + NSMutableSet *tipNodesToRemove = [NSMutableSet setWithArray:_node.subnodes]; + for (ASDisplayNodeTipState *tipState in [_nodeToTipStates objectEnumerator]) { + ASDisplayNode *node = tipState.node; + ASTipNode *tipNode = tipState.tipNode; + [tipNodesToRemove removeObject:tipNode]; + CGRect rect = node.bounds; + rect = [node.view convertRect:rect toView:nil]; + rect = [self convertRect:rect fromView:nil]; + tipNode.frame = rect; + if (tipNode.supernode != _node) { + [_node addSubnode:tipNode]; + } + } + + // Clean up any tip nodes whose target nodes have disappeared. + for (ASTipNode *tipNode in tipNodesToRemove) { + [tipNode removeFromSupernode]; + } +} + +@end + +#endif // AS_ENABLE_TIPS diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTwoDimensionalArrayUtils.h b/submodules/AsyncDisplayKit/Source/Private/ASTwoDimensionalArrayUtils.h new file mode 100644 index 0000000000..6a3c9bb5ce --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTwoDimensionalArrayUtils.h @@ -0,0 +1,46 @@ +// +// ASTwoDimensionalArrayUtils.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Helper functions for two-dimensional array, where the objects of the root array are each arrays. + */ + +/** + * Deep mutable copy of an array that contains arrays, which contain objects. It will go one level deep into the array to copy. + * This method is substantially faster than the generalized version, e.g. about 10x faster, so use it whenever it fits the need. + */ +AS_EXTERN NSMutableArray *ASTwoDimensionalArrayDeepMutableCopy(NSArray *array) AS_WARN_UNUSED_RESULT; + +/** + * Delete the elements of the mutable two-dimensional array at given index paths – sorted in descending order! + */ +AS_EXTERN void ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths); + +/** + * Return all the index paths of a two-dimensional array, in ascending order. + */ +AS_EXTERN NSArray *ASIndexPathsForTwoDimensionalArray(NSArray* twoDimensionalArray) AS_WARN_UNUSED_RESULT; + +/** + * Return all the elements of a two-dimensional array, in ascending order. + */ +AS_EXTERN NSArray *ASElementsInTwoDimensionalArray(NSArray* twoDimensionalArray) AS_WARN_UNUSED_RESULT; + +/** + * Attempt to get the object at the given index path. Returns @c nil if the index path is out of bounds. + */ +AS_EXTERN id _Nullable ASGetElementInTwoDimensionalArray(NSArray *array, NSIndexPath *indexPath) AS_WARN_UNUSED_RESULT; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASTwoDimensionalArrayUtils.mm b/submodules/AsyncDisplayKit/Source/Private/ASTwoDimensionalArrayUtils.mm new file mode 100644 index 0000000000..67267b7a94 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASTwoDimensionalArrayUtils.mm @@ -0,0 +1,122 @@ +// +// ASTwoDimensionalArrayUtils.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#import + +// Import UIKit to get [NSIndexPath indexPathForItem:inSection:] which uses +// tagged pointers. +#import + +#pragma mark - Public Methods + +NSMutableArray *ASTwoDimensionalArrayDeepMutableCopy(NSArray *array) +{ + NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:array.count]; + NSInteger i = 0; + for (NSArray *subarray in array) { + ASDisplayNodeCAssert([subarray isKindOfClass:[NSArray class]], @"This function expects NSArray *"); + newArray[i++] = [subarray mutableCopy]; + } + return newArray; +} + +void ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths) +{ + if (indexPaths.count == 0) { + return; + } + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(asdk_inverseCompare:)]; + ASDisplayNodeCAssert([sortedIndexPaths isEqualToArray:indexPaths], @"Expected array of index paths to be sorted in descending order."); +#endif + + /** + * It is tempting to do something clever here and collect indexes into ranges or NSIndexSets + * but deep down, __NSArrayM only implements removeObjectAtIndex: and so doing all that extra + * work ends up running the same code. + */ + for (NSIndexPath *indexPath in indexPaths) { + NSInteger section = indexPath.section; + if (section >= mutableArray.count) { + ASDisplayNodeCFailAssert(@"Invalid section index %ld – only %ld sections", (long)section, (long)mutableArray.count); + continue; + } + + NSMutableArray *subarray = mutableArray[section]; + NSInteger item = indexPath.item; + if (item >= subarray.count) { + ASDisplayNodeCFailAssert(@"Invalid item index %ld – only %ld items in section %ld", (long)item, (long)subarray.count, (long)section); + continue; + } + [subarray removeObjectAtIndex:item]; + } +} + +NSArray *ASIndexPathsForTwoDimensionalArray(NSArray * twoDimensionalArray) +{ + NSInteger sectionCount = twoDimensionalArray.count; + NSInteger counts[sectionCount]; + NSInteger totalCount = 0; + NSInteger i = 0; + for (NSArray *subarray in twoDimensionalArray) { + NSInteger count = subarray.count; + counts[i++] = count; + totalCount += count; + } + + // Count could be huge. Use a reserved vector rather than VLA (stack.) + std::vector indexPaths; + indexPaths.reserve(totalCount); + for (NSInteger i = 0; i < sectionCount; i++) { + for (NSInteger j = 0; j < counts[i]; j++) { + indexPaths.push_back([NSIndexPath indexPathForItem:j inSection:i]); + } + } + return [NSArray arrayByTransferring:indexPaths.data() count:totalCount]; +} + +NSArray *ASElementsInTwoDimensionalArray(NSArray * twoDimensionalArray) +{ + NSInteger totalCount = 0; + for (NSArray *subarray in twoDimensionalArray) { + totalCount += subarray.count; + } + + std::vector elements; + elements.reserve(totalCount); + for (NSArray *subarray in twoDimensionalArray) { + for (id object in subarray) { + elements.push_back(object); + } + } + return [NSArray arrayByTransferring:elements.data() count:totalCount]; +} + +id ASGetElementInTwoDimensionalArray(NSArray *array, NSIndexPath *indexPath) +{ + ASDisplayNodeCAssertNotNil(indexPath, @"Expected non-nil index path"); + ASDisplayNodeCAssert(indexPath.length == 2, @"Expected index path of length 2. Index path: %@", indexPath); + NSInteger section = indexPath.section; + if (array.count <= section) { + return nil; + } + + NSArray *innerArray = array[section]; + NSInteger item = indexPath.item; + if (innerArray.count <= item) { + return nil; + } + return innerArray[item]; +} diff --git a/submodules/AsyncDisplayKit/Source/Private/ASWeakMap.h b/submodules/AsyncDisplayKit/Source/Private/ASWeakMap.h new file mode 100644 index 0000000000..1f413ca7ae --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASWeakMap.h @@ -0,0 +1,59 @@ +// +// ASWeakMap.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + + +/** + * This class is used in conjunction with ASWeakMap. Instances of this type are returned by an ASWeakMap, + * must retain this value for as long as they want the entry to exist in the map. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASWeakMapEntry : NSObject + +@property (readonly) Value value; + +@end + + +/** + * This is not a full-featured map. It does not support features like `count` and FastEnumeration because there + * is not currently a need. + * + * This is a map that does not retain keys or values added to it. When both getting and setting, the caller is + * returned a ASWeakMapEntry and must retain it for as long as it wishes the key/value to remain in the map. + * We return a single Entry value to the caller to avoid two potential problems: + * + * 1) It's easier for callers to retain one value (the Entry) and not two (a key and a value). + * 2) Key values are tested for `isEqual` equality. If if a caller asks for a key "A" that is equal to a key "B" + * already in the map, then we need the caller to retain key "B" and not key "A". Returning an Entry simplifies + * the semantics. + * + * The underlying storage is a hash table and the Key type should implement `hash` and `isEqual:`. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASWeakMap<__covariant Key, Value> : NSObject + +/** + * Read from the cache. The Value object is accessible from the returned ASWeakMapEntry. + */ +- (nullable ASWeakMapEntry *)entryForKey:(Key)key AS_WARN_UNUSED_RESULT; + +/** + * Put a value into the cache. If an entry with an equal key already exists, then the value is updated on the existing entry. + */ +- (ASWeakMapEntry *)setObject:(Value)value forKey:(Key)key AS_WARN_UNUSED_RESULT; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/ASWeakMap.mm b/submodules/AsyncDisplayKit/Source/Private/ASWeakMap.mm new file mode 100644 index 0000000000..3110b13d37 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/ASWeakMap.mm @@ -0,0 +1,78 @@ +// +// ASWeakMap.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASWeakMapEntry () +@property (nonatomic, readonly) id key; +@property id value; +@end + +@implementation ASWeakMapEntry + +- (instancetype)initWithKey:(id)key value:(id)value +{ + self = [super init]; + if (self) { + _key = key; + _value = value; + } + return self; +} + +@end + + +@interface ASWeakMap () +@property (nonatomic, readonly) NSMapTable *hashTable; +@end + +/** + * Implementation details: + * + * The retained size of our keys is potentially very large (for example, a UIImage is commonly part of a key). + * Unfortunately, NSMapTable does not make guarantees about how quickly it will dispose of entries where + * either the key or the value is weak and has been disposed. So, a NSMapTable with "strong key to weak value" is + * unsuitable for our purpose because the strong keys are retained longer than the value and for an indefininte period of time. + * More details here: http://cocoamine.net/blog/2013/12/13/nsmaptable-and-zeroing-weak-references/ + * + * Our NSMapTable is "weak key to weak value" where each key maps to an Entry. The Entry object is responsible + * for retaining both the key and value. Our convention is that the caller must retain the Entry object + * in order to keep the key and the value in the cache. + */ +@implementation ASWeakMap + +- (instancetype)init +{ + self = [super init]; + if (self) { + _hashTable = [NSMapTable weakToWeakObjectsMapTable]; + } + return self; +} + +- (ASWeakMapEntry *)entryForKey:(id)key +{ + return [self.hashTable objectForKey:key]; +} + +- (ASWeakMapEntry *)setObject:(id)value forKey:(id)key +{ + ASWeakMapEntry *entry = [self.hashTable objectForKey:key]; + if (entry != nil) { + // Update the value in the existing entry. + entry.value = value; + } else { + entry = [[ASWeakMapEntry alloc] initWithKey:key value:value]; + [self.hashTable setObject:entry forKey:key]; + } + return entry; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutElementStylePrivate.h b/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutElementStylePrivate.h new file mode 100644 index 0000000000..69e29824ef --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutElementStylePrivate.h @@ -0,0 +1,31 @@ +// +// ASLayoutElementStylePrivate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import +#import + +@interface ASLayoutElementStyle () + +/** + * @abstract The object that acts as the delegate of the style. + * + * @discussion The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained. + */ +@property (nullable, nonatomic, weak) id delegate; + +/** + * @abstract A size constraint that should apply to this ASLayoutElement. + */ +@property (nonatomic, readonly) ASLayoutElementSize size; + +@property (nonatomic, assign) ASStackLayoutAlignItems parentAlignStyle; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutSpecPrivate.h b/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutSpecPrivate.h new file mode 100644 index 0000000000..930232096c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutSpecPrivate.h @@ -0,0 +1,37 @@ +// +// ASLayoutSpecPrivate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if DEBUG + #define AS_DEDUPE_LAYOUT_SPEC_TREE 1 +#else + #define AS_DEDUPE_LAYOUT_SPEC_TREE 0 +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface ASLayoutSpec() { + AS::RecursiveMutex __instanceLock__; + std::atomic _primitiveTraitCollection; + ASLayoutElementStyle *_style; + NSMutableArray *_childrenArray; +} + +#if AS_DEDUPE_LAYOUT_SPEC_TREE +/** + * Recursively search the subtree for elements that occur more than once. + */ +- (nullable NSHashTable> *)findDuplicatedElementsInSubtree; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutSpecUtilities.h b/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutSpecUtilities.h new file mode 100644 index 0000000000..62a3d9177b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASLayoutSpecUtilities.h @@ -0,0 +1,103 @@ +// +// ASLayoutSpecUtilities.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import + +namespace AS { + // adopted from http://stackoverflow.com/questions/14945223/map-function-with-c11-constructs + // Takes an iterable, applies a function to every element, + // and returns a vector of the results + // + template + auto map(const T &iterable, Func &&func) -> std::vector()))> + { + // Some convenience type definitions + typedef decltype(func(std::declval())) value_type; + typedef std::vector result_type; + + // Prepares an output vector of the appropriate size + result_type res(iterable.size()); + + // Let std::transform apply `func` to all elements + // (use perfect forwarding for the function object) + std::transform( + begin(iterable), end(iterable), res.begin(), + std::forward(func) + ); + + return res; + } + + template + auto map(id collection, Func &&func) -> std::vector()))> + { + std::vector()))> to; + for (id obj in collection) { + to.push_back(func(obj)); + } + return to; + } + + template + auto filter(const T &iterable, Func &&func) -> std::vector + { + std::vector to; + for (auto obj : iterable) { + if (func(obj)) { + to.push_back(obj); + } + } + return to; + } +}; + +inline CGPoint operator+(const CGPoint &p1, const CGPoint &p2) +{ + return { p1.x + p2.x, p1.y + p2.y }; +} + +inline CGPoint operator-(const CGPoint &p1, const CGPoint &p2) +{ + return { p1.x - p2.x, p1.y - p2.y }; +} + +inline CGSize operator+(const CGSize &s1, const CGSize &s2) +{ + return { s1.width + s2.width, s1.height + s2.height }; +} + +inline CGSize operator-(const CGSize &s1, const CGSize &s2) +{ + return { s1.width - s2.width, s1.height - s2.height }; +} + +inline UIEdgeInsets operator+(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top + e2.top, e1.left + e2.left, e1.bottom + e2.bottom, e1.right + e2.right }; +} + +inline UIEdgeInsets operator-(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top - e2.top, e1.left - e2.left, e1.bottom - e2.bottom, e1.right - e2.right }; +} + +inline UIEdgeInsets operator*(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top * e2.top, e1.left * e2.left, e1.bottom * e2.bottom, e1.right * e2.right }; +} + +inline UIEdgeInsets operator-(const UIEdgeInsets &e) +{ + return { -e.top, -e.left, -e.bottom, -e.right }; +} diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackLayoutSpecUtilities.h b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackLayoutSpecUtilities.h new file mode 100644 index 0000000000..6c708e0408 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackLayoutSpecUtilities.h @@ -0,0 +1,135 @@ +// +// ASStackLayoutSpecUtilities.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +typedef struct { + ASStackLayoutDirection direction; + CGFloat spacing; + ASStackLayoutJustifyContent justifyContent; + ASStackLayoutAlignItems alignItems; + ASStackLayoutFlexWrap flexWrap; + ASStackLayoutAlignContent alignContent; + CGFloat lineSpacing; +} ASStackLayoutSpecStyle; + +inline CGFloat stackDimension(const ASStackLayoutDirection direction, const CGSize size) +{ + return (direction == ASStackLayoutDirectionVertical) ? size.height : size.width; +} + +inline CGFloat crossDimension(const ASStackLayoutDirection direction, const CGSize size) +{ + return (direction == ASStackLayoutDirectionVertical) ? size.width : size.height; +} + +inline BOOL compareCrossDimension(const ASStackLayoutDirection direction, const CGSize a, const CGSize b) +{ + return crossDimension(direction, a) < crossDimension(direction, b); +} + +inline CGPoint directionPoint(const ASStackLayoutDirection direction, const CGFloat stack, const CGFloat cross) +{ + return (direction == ASStackLayoutDirectionVertical) ? CGPointMake(cross, stack) : CGPointMake(stack, cross); +} + +inline CGSize directionSize(const ASStackLayoutDirection direction, const CGFloat stack, const CGFloat cross) +{ + return (direction == ASStackLayoutDirectionVertical) ? CGSizeMake(cross, stack) : CGSizeMake(stack, cross); +} + +inline void setStackValueToPoint(const ASStackLayoutDirection direction, const CGFloat stack, CGPoint &point) { + (direction == ASStackLayoutDirectionVertical) ? (point.y = stack) : (point.x = stack); +} + +inline ASSizeRange directionSizeRange(const ASStackLayoutDirection direction, + const CGFloat stackMin, + const CGFloat stackMax, + const CGFloat crossMin, + const CGFloat crossMax) +{ + return {directionSize(direction, stackMin, crossMin), directionSize(direction, stackMax, crossMax)}; +} + +inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment, ASStackLayoutAlignItems stackAlignment) +{ + switch (childAlignment) { + case ASStackLayoutAlignSelfCenter: + return ASStackLayoutAlignItemsCenter; + case ASStackLayoutAlignSelfEnd: + return ASStackLayoutAlignItemsEnd; + case ASStackLayoutAlignSelfStart: + return ASStackLayoutAlignItemsStart; + case ASStackLayoutAlignSelfStretch: + return ASStackLayoutAlignItemsStretch; + case ASStackLayoutAlignSelfAuto: + default: + return stackAlignment; + } +} + +inline ASStackLayoutAlignItems alignment(ASHorizontalAlignment alignment, ASStackLayoutAlignItems defaultAlignment) +{ + switch (alignment) { + case ASHorizontalAlignmentLeft: + return ASStackLayoutAlignItemsStart; + case ASHorizontalAlignmentMiddle: + return ASStackLayoutAlignItemsCenter; + case ASHorizontalAlignmentRight: + return ASStackLayoutAlignItemsEnd; + case ASHorizontalAlignmentNone: + default: + return defaultAlignment; + } +} + +inline ASStackLayoutAlignItems alignment(ASVerticalAlignment alignment, ASStackLayoutAlignItems defaultAlignment) +{ + switch (alignment) { + case ASVerticalAlignmentTop: + return ASStackLayoutAlignItemsStart; + case ASVerticalAlignmentCenter: + return ASStackLayoutAlignItemsCenter; + case ASVerticalAlignmentBottom: + return ASStackLayoutAlignItemsEnd; + case ASVerticalAlignmentNone: + default: + return defaultAlignment; + } +} + +inline ASStackLayoutJustifyContent justifyContent(ASHorizontalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent) +{ + switch (alignment) { + case ASHorizontalAlignmentLeft: + return ASStackLayoutJustifyContentStart; + case ASHorizontalAlignmentMiddle: + return ASStackLayoutJustifyContentCenter; + case ASHorizontalAlignmentRight: + return ASStackLayoutJustifyContentEnd; + case ASHorizontalAlignmentNone: + default: + return defaultJustifyContent; + } +} + +inline ASStackLayoutJustifyContent justifyContent(ASVerticalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent) +{ + switch (alignment) { + case ASVerticalAlignmentTop: + return ASStackLayoutJustifyContentStart; + case ASVerticalAlignmentCenter: + return ASStackLayoutJustifyContentCenter; + case ASVerticalAlignmentBottom: + return ASStackLayoutJustifyContentEnd; + case ASVerticalAlignmentNone: + default: + return defaultJustifyContent; + } +} diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackPositionedLayout.h b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackPositionedLayout.h new file mode 100644 index 0000000000..103ec3a280 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackPositionedLayout.h @@ -0,0 +1,24 @@ +// +// ASStackPositionedLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +/** Represents a set of laid out and positioned stack layout children. */ +struct ASStackPositionedLayout { + const std::vector items; + /** Final size of the stack */ + const CGSize size; + + /** Given an unpositioned layout, computes the positions each child should be placed at. */ + static ASStackPositionedLayout compute(const ASStackUnpositionedLayout &unpositionedLayout, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &constrainedSize); +}; diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackPositionedLayout.mm b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackPositionedLayout.mm new file mode 100644 index 0000000000..727db5d2dd --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackPositionedLayout.mm @@ -0,0 +1,186 @@ +// +// ASStackPositionedLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import + +static CGFloat crossOffsetForItem(const ASStackLayoutSpecItem &item, + const ASStackLayoutSpecStyle &style, + const CGFloat crossSize, + const CGFloat baseline) +{ + switch (alignment(item.child.style.alignSelf, style.alignItems)) { + case ASStackLayoutAlignItemsEnd: + return crossSize - crossDimension(style.direction, item.layout.size); + case ASStackLayoutAlignItemsCenter: + return ASFloorPixelValue((crossSize - crossDimension(style.direction, item.layout.size)) / 2); + case ASStackLayoutAlignItemsBaselineFirst: + case ASStackLayoutAlignItemsBaselineLast: + return baseline - ASStackUnpositionedLayout::baselineForItem(style, item); + case ASStackLayoutAlignItemsStart: + case ASStackLayoutAlignItemsStretch: + case ASStackLayoutAlignItemsNotSet: + return 0; + } +} + +static void crossOffsetAndSpacingForEachLine(const std::size_t numOfLines, + const CGFloat crossViolation, + ASStackLayoutAlignContent alignContent, + CGFloat &offset, + CGFloat &spacing) +{ + ASDisplayNodeCAssertTrue(numOfLines > 0); + + // Handle edge cases + if (alignContent == ASStackLayoutAlignContentSpaceBetween && (crossViolation < kViolationEpsilon || numOfLines == 1)) { + alignContent = ASStackLayoutAlignContentStart; + } else if (alignContent == ASStackLayoutAlignContentSpaceAround && (crossViolation < kViolationEpsilon || numOfLines == 1)) { + alignContent = ASStackLayoutAlignContentCenter; + } + + offset = 0; + spacing = 0; + + switch (alignContent) { + case ASStackLayoutAlignContentCenter: + offset = crossViolation / 2; + break; + case ASStackLayoutAlignContentEnd: + offset = crossViolation; + break; + case ASStackLayoutAlignContentSpaceBetween: + // Spacing between the items, no spaces at the edges, evenly distributed + spacing = crossViolation / (numOfLines - 1); + break; + case ASStackLayoutAlignContentSpaceAround: { + // Spacing between items are twice the spacing on the edges + CGFloat spacingUnit = crossViolation / (numOfLines * 2); + offset = spacingUnit; + spacing = spacingUnit * 2; + break; + } + case ASStackLayoutAlignContentStart: + case ASStackLayoutAlignContentStretch: + break; + } +} + +static void stackOffsetAndSpacingForEachItem(const std::size_t numOfItems, + const CGFloat stackViolation, + ASStackLayoutJustifyContent justifyContent, + CGFloat &offset, + CGFloat &spacing) +{ + ASDisplayNodeCAssertTrue(numOfItems > 0); + + // Handle edge cases + if (justifyContent == ASStackLayoutJustifyContentSpaceBetween && (stackViolation < kViolationEpsilon || numOfItems == 1)) { + justifyContent = ASStackLayoutJustifyContentStart; + } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (stackViolation < kViolationEpsilon || numOfItems == 1)) { + justifyContent = ASStackLayoutJustifyContentCenter; + } + + offset = 0; + spacing = 0; + + switch (justifyContent) { + case ASStackLayoutJustifyContentCenter: + offset = stackViolation / 2; + break; + case ASStackLayoutJustifyContentEnd: + offset = stackViolation; + break; + case ASStackLayoutJustifyContentSpaceBetween: + // Spacing between the items, no spaces at the edges, evenly distributed + spacing = stackViolation / (numOfItems - 1); + break; + case ASStackLayoutJustifyContentSpaceAround: { + // Spacing between items are twice the spacing on the edges + CGFloat spacingUnit = stackViolation / (numOfItems * 2); + offset = spacingUnit; + spacing = spacingUnit * 2; + break; + } + case ASStackLayoutJustifyContentStart: + break; + } +} + +static void positionItemsInLine(const ASStackUnpositionedLine &line, + const ASStackLayoutSpecStyle &style, + const CGPoint &startingPoint, + const CGFloat stackSpacing) +{ + CGPoint p = startingPoint; + BOOL first = YES; + + for (const auto &item : line.items) { + p = p + directionPoint(style.direction, item.child.style.spacingBefore, 0); + if (!first) { + p = p + directionPoint(style.direction, style.spacing + stackSpacing, 0); + } + first = NO; + item.layout.position = p + directionPoint(style.direction, 0, crossOffsetForItem(item, style, line.crossSize, line.baseline)); + + p = p + directionPoint(style.direction, stackDimension(style.direction, item.layout.size) + item.child.style.spacingAfter, 0); + } +} + +ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &layout, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const auto &lines = layout.lines; + if (lines.empty()) { + return {}; + } + + const auto numOfLines = lines.size(); + const auto direction = style.direction; + const auto alignContent = style.alignContent; + const auto lineSpacing = style.lineSpacing; + const auto justifyContent = style.justifyContent; + const auto crossViolation = ASStackUnpositionedLayout::computeCrossViolation(layout.crossDimensionSum, style, sizeRange); + CGFloat crossOffset; + CGFloat crossSpacing; + crossOffsetAndSpacingForEachLine(numOfLines, crossViolation, alignContent, crossOffset, crossSpacing); + + std::vector positionedItems; + CGPoint p = directionPoint(direction, 0, crossOffset); + BOOL first = YES; + for (const auto &line : lines) { + if (!first) { + p = p + directionPoint(direction, 0, crossSpacing + lineSpacing); + } + first = NO; + + const auto &items = line.items; + const auto stackViolation = ASStackUnpositionedLayout::computeStackViolation(line.stackDimensionSum, style, sizeRange); + CGFloat stackOffset; + CGFloat stackSpacing; + stackOffsetAndSpacingForEachItem(items.size(), stackViolation, justifyContent, stackOffset, stackSpacing); + + setStackValueToPoint(direction, stackOffset, p); + positionItemsInLine(line, style, p, stackSpacing); + std::move(items.begin(), items.end(), std::back_inserter(positionedItems)); + + p = p + directionPoint(direction, -stackOffset, line.crossSize); + } + + const CGSize finalSize = directionSize(direction, layout.stackDimensionSum, layout.crossDimensionSum); + return {std::move(positionedItems), ASSizeRangeClamp(sizeRange, finalSize)}; +} diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackUnpositionedLayout.h b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackUnpositionedLayout.h new file mode 100644 index 0000000000..989b4c2f11 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackUnpositionedLayout.h @@ -0,0 +1,73 @@ +// +// ASStackUnpositionedLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +/** The threshold that determines if a violation has actually occurred. */ +AS_EXTERN CGFloat const kViolationEpsilon; + +struct ASStackLayoutSpecChild { + /** The original source child. */ + id element; + /** Style object of element. */ + ASLayoutElementStyle *style; + /** Size object of the element */ + ASLayoutElementSize size; +}; + +struct ASStackLayoutSpecItem { + /** The original source child. */ + ASStackLayoutSpecChild child; + /** The proposed layout or nil if no is calculated yet. */ + ASLayout *layout; +}; + +struct ASStackUnpositionedLine { + /** The set of proposed children in this line, each contains child layout, not yet positioned. */ + std::vector items; + /** The total size of the children in the stack dimension, including all spacing. */ + CGFloat stackDimensionSum; + /** The size in the cross dimension */ + CGFloat crossSize; + /** The baseline of the stack which baseline aligned children should align to */ + CGFloat baseline; +}; + +/** Represents a set of stack layout children that have their final layout computed, but are not yet positioned. */ +struct ASStackUnpositionedLayout { + /** The set of proposed lines, each contains child layouts, not yet positioned. */ + const std::vector lines; + /** + * In a single line stack (e.g no wrao), this is the total size of the children in the stack dimension, including all spacing. + * In a multi-line stack, this is the largest stack dimension among lines. + */ + const CGFloat stackDimensionSum; + const CGFloat crossDimensionSum; + + /** Given a set of children, computes the unpositioned layouts for those children. */ + static ASStackUnpositionedLayout compute(const std::vector &children, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange, + const BOOL concurrent); + + static CGFloat baselineForItem(const ASStackLayoutSpecStyle &style, + const ASStackLayoutSpecItem &l); + + static CGFloat computeStackViolation(const CGFloat stackDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange); + + static CGFloat computeCrossViolation(const CGFloat crossDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange); +}; diff --git a/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackUnpositionedLayout.mm b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackUnpositionedLayout.mm new file mode 100644 index 0000000000..47141a7f7b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/Layout/ASStackUnpositionedLayout.mm @@ -0,0 +1,758 @@ +// +// ASStackUnpositionedLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import + +CGFloat const kViolationEpsilon = 0.01; + +static CGFloat resolveCrossDimensionMaxForStretchChild(const ASStackLayoutSpecStyle &style, + const ASStackLayoutSpecChild &child, + const CGFloat stackMax, + const CGFloat crossMax) +{ + // stretched children may have a cross direction max that is smaller than the minimum size constraint of the parent. + const CGFloat computedMax = (style.direction == ASStackLayoutDirectionVertical ? + ASLayoutElementSizeResolve(child.style.size, ASLayoutElementParentSizeUndefined).max.width : + ASLayoutElementSizeResolve(child.style.size, ASLayoutElementParentSizeUndefined).max.height); + return computedMax == INFINITY ? crossMax : computedMax; +} + +static CGFloat resolveCrossDimensionMinForStretchChild(const ASStackLayoutSpecStyle &style, + const ASStackLayoutSpecChild &child, + const CGFloat stackMax, + const CGFloat crossMin) +{ + // stretched children will have a cross dimension of at least crossMin, unless they explicitly define a child size + // that is smaller than the constraint of the parent. + return (style.direction == ASStackLayoutDirectionVertical ? + ASLayoutElementSizeResolve(child.style.size, ASLayoutElementParentSizeUndefined).min.width : + ASLayoutElementSizeResolve(child.style.size, ASLayoutElementParentSizeUndefined).min.height) ?: crossMin; +} + +/** + Sizes the child given the parameters specified, and returns the computed layout. + */ +static ASLayout *crossChildLayout(const ASStackLayoutSpecChild &child, + const ASStackLayoutSpecStyle &style, + const CGFloat stackMin, + const CGFloat stackMax, + const CGFloat crossMin, + const CGFloat crossMax, + const CGSize parentSize) +{ + const ASStackLayoutAlignItems alignItems = alignment(child.style.alignSelf, style.alignItems); + // stretched children will have a cross dimension of at least crossMin + const CGFloat childCrossMin = (alignItems == ASStackLayoutAlignItemsStretch ? + resolveCrossDimensionMinForStretchChild(style, child, stackMax, crossMin) : + 0); + const CGFloat childCrossMax = (alignItems == ASStackLayoutAlignItemsStretch ? + resolveCrossDimensionMaxForStretchChild(style, child, stackMax, crossMax) : + crossMax); + const ASSizeRange childSizeRange = directionSizeRange(style.direction, stackMin, stackMax, childCrossMin, childCrossMax); + ASLayout *layout = [child.element layoutThatFits:childSizeRange parentSize:parentSize]; + ASDisplayNodeCAssertNotNil(layout, @"ASLayout returned from -layoutThatFits:parentSize: must not be nil: %@", child.element); + return layout ? : [ASLayout layoutWithLayoutElement:child.element size:{0, 0}]; +} + +static void dispatchApplyIfNeeded(size_t iterationCount, BOOL forced, void(^work)(size_t i)) +{ + if (iterationCount == 0) { + return; + } + + if (iterationCount == 1) { + work(0); + return; + } + + // TODO Once the locking situation in ASDisplayNode has improved, always dispatch if on main + if (forced == NO) { + for (size_t i = 0; i < iterationCount; i++) { + work(i); + } + return; + } + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + ASDispatchApply(iterationCount, queue, 0, work); +} + +/** + Computes the consumed cross dimension length for the given vector of lines and stacking style. + + Cross Dimension + +---------------------> + +--------+ +--------+ +--------+ +---------+ + Vertical |Vertical| |Vertical| |Vertical| |Vertical | + Stack | Line 1 | | Line 2 | | Line 3 | | Line 4 | + | | | | | | | | + +--------+ +--------+ +--------+ +---------+ + crossDimensionSum + |------------------------------------------| + + @param lines unpositioned lines + */ +static CGFloat computeLinesCrossDimensionSum(const std::vector &lines, + const ASStackLayoutSpecStyle &style) +{ + return std::accumulate(lines.begin(), lines.end(), + // Start from default spacing between each line: + lines.empty() ? 0 : style.lineSpacing * (lines.size() - 1), + [&](CGFloat x, const ASStackUnpositionedLine &l) { + return x + l.crossSize; + }); +} + + +/** + Computes the violation by comparing a cross dimension sum with the overall allowable size range for the stack. + + Violation is the distance you would have to add to the unbounded cross-direction length of the stack spec's + lines in order to bring the stack within its allowed sizeRange. The diagram below shows 3 vertical stacks, each contains 3-5 vertical lines, + with the different types of violation. + + Cross Dimension + +---------------------> + cross size range + |------------| + +--------+ +--------+ +--------+ +---------+ - - - - - - - - + Vertical |Vertical| |Vertical| |Vertical| |Vertical | | ^ + Stack 1 | Line 1 | | Line 2 | | Line 3 | | Line 4 | (zero violation) | stack size range + | | | | | | | | | | v + +--------+ +--------+ +--------+ +---------+ - - - - - - - - + | | + +--------+ +--------+ +--------+ - - - - - - - - - - - - + Vertical | | | | | | | | ^ + Stack 2 | | | | | |<--> (positive violation) | stack size range + | | | | | | | | v + +--------+ +--------+ +--------+ - - - - - - - - - - - - + | |<------> (negative violation) + +--------+ +--------+ +--------+ +---------+ +-----------+ - - - + Vertical | | | | | | | | | | | | ^ + Stack 3 | | | | | | | | | | | stack size range + | | | | | | | | | | | | v + +--------+ +--------+ +--------+ +---------+ +-----------+ - - - + + @param crossDimensionSum the consumed length of the lines in the stack along the cross dimension + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout spec + */ +CGFloat ASStackUnpositionedLayout::computeCrossViolation(const CGFloat crossDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const CGFloat minCrossDimension = crossDimension(style.direction, sizeRange.min); + const CGFloat maxCrossDimension = crossDimension(style.direction, sizeRange.max); + if (crossDimensionSum < minCrossDimension) { + return minCrossDimension - crossDimensionSum; + } else if (crossDimensionSum > maxCrossDimension) { + return maxCrossDimension - crossDimensionSum; + } + return 0; +} + +/** + Stretches children to lay out along the cross axis according to the alignment stretch settings of the children + (child.alignSelf), and the stack layout's alignment settings (style.alignItems). This does not do the actual alignment + of the items once stretched though; ASStackPositionedLayout will do centering etc. + + Finds the maximum cross dimension among child layouts. If that dimension exceeds the minimum cross layout size then + we must stretch any children whose alignItems specify ASStackLayoutAlignItemsStretch. + + The diagram below shows 3 children in a horizontal stack. The second child is larger than the minCrossDimension, so + its height is used as the childCrossMax. Any children that are stretchable (which may be all children if + style.alignItems specifies stretch) like the first child must be stretched to match that maximum. All children must be + at least minCrossDimension in cross dimension size, which is shown by the sizing of the third child. + + Stack Dimension + +---------------------> + + +-+-------------+-+-------------+--+---------------+ + + + + | | child. | | | | | | | | + | | alignSelf | | | | | | | | + Cross | | = stretch | | | +-------+-------+ | | | + Dimension | +-----+-------+ | | | | | | | | + | | | | | | | | | | + | | | | | v | | | | + v +-+- - - - - - -+-+ - - - - - - +--+- - - - - - - -+ | | + minCrossDimension + | | | | | + | v | | | | | + +- - - - - - -+ +-------------+ | + childCrossMax + | + +--------------------------------------------------+ + crossMax + + @param items pre-computed items; modified in-place as needed + @param style the layout style of the overall stack layout + */ +static void stretchItemsAlongCrossDimension(std::vector &items, + const ASStackLayoutSpecStyle &style, + const BOOL concurrent, + const CGSize parentSize, + const CGFloat crossSize) +{ + dispatchApplyIfNeeded(items.size(), concurrent, ^(size_t i) { + auto &item = items[i]; + const ASStackLayoutAlignItems alignItems = alignment(item.child.style.alignSelf, style.alignItems); + if (alignItems == ASStackLayoutAlignItemsStretch) { + const CGFloat cross = crossDimension(style.direction, item.layout.size); + const CGFloat stack = stackDimension(style.direction, item.layout.size); + const CGFloat violation = crossSize - cross; + + // Only stretch if violation is positive. Compare against kViolationEpsilon here to avoid stretching against a tiny violation. + if (violation > kViolationEpsilon) { + item.layout = crossChildLayout(item.child, style, stack, stack, crossSize, crossSize, parentSize); + } + } + }); +} + +/** + * Stretch lines and their items according to alignContent, alignItems and alignSelf. + * https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch + * https://www.w3.org/TR/css-flexbox-1/#algo-stretch + */ +static void stretchLinesAlongCrossDimension(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const BOOL concurrent, + const ASSizeRange &sizeRange, + const CGSize parentSize) +{ + ASDisplayNodeCAssertFalse(lines.empty()); + const std::size_t numOfLines = lines.size(); + const CGFloat violation = ASStackUnpositionedLayout::computeCrossViolation(computeLinesCrossDimensionSum(lines, style), style, sizeRange); + // Don't stretch if the stack is single line, because the line's cross size was clamped against the stack's constrained size. + const BOOL shouldStretchLines = (numOfLines > 1 + && style.alignContent == ASStackLayoutAlignContentStretch + && violation > kViolationEpsilon); + + CGFloat extraCrossSizePerLine = violation / numOfLines; + for (auto &line : lines) { + if (shouldStretchLines) { + line.crossSize += extraCrossSizePerLine; + } + + stretchItemsAlongCrossDimension(line.items, style, concurrent, parentSize, line.crossSize); + } +} + +static BOOL itemIsBaselineAligned(const ASStackLayoutSpecStyle &style, + const ASStackLayoutSpecItem &l) +{ + ASStackLayoutAlignItems alignItems = alignment(l.child.style.alignSelf, style.alignItems); + return alignItems == ASStackLayoutAlignItemsBaselineFirst || alignItems == ASStackLayoutAlignItemsBaselineLast; +} + +CGFloat ASStackUnpositionedLayout::baselineForItem(const ASStackLayoutSpecStyle &style, + const ASStackLayoutSpecItem &item) +{ + switch (alignment(item.child.style.alignSelf, style.alignItems)) { + case ASStackLayoutAlignItemsBaselineFirst: + return item.child.style.ascender; + case ASStackLayoutAlignItemsBaselineLast: + return crossDimension(style.direction, item.layout.size) + item.child.style.descender; + default: + return 0; + } +} + +/** + * Computes cross size and baseline of each line. + * https://www.w3.org/TR/css-flexbox-1/#algo-cross-line + * + * @param lines All items to lay out + * @param style the layout style of the overall stack layout + * @param sizeRange the range of allowable sizes for the stack layout component + */ +static void computeLinesCrossSizeAndBaseline(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + ASDisplayNodeCAssertFalse(lines.empty()); + const BOOL isSingleLine = (lines.size() == 1); + + const auto minCrossSize = crossDimension(style.direction, sizeRange.min); + const auto maxCrossSize = crossDimension(style.direction, sizeRange.max); + const BOOL definiteCrossSize = (minCrossSize == maxCrossSize); + + // If the stack is single-line and has a definite cross size, the cross size of the line is the stack's definite cross size. + if (isSingleLine && definiteCrossSize) { + auto &line = lines[0]; + line.crossSize = minCrossSize; + + // We still need to determine the line's baseline + //TODO unit test + for (const auto &item : line.items) { + if (itemIsBaselineAligned(style, item)) { + CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); + line.baseline = MAX(line.baseline, baseline); + } + } + + return; + } + + for (auto &line : lines) { + const auto &items = line.items; + CGFloat maxStartToBaselineDistance = 0; + CGFloat maxBaselineToEndDistance = 0; + CGFloat maxItemCrossSize = 0; + + for (const auto &item : items) { + if (itemIsBaselineAligned(style, item)) { + // Step 1. Collect all the items whose align-self is baseline. Find the largest of the distances + // between each item’s baseline and its hypothetical outer cross-start edge (aka. its baseline value), + // and the largest of the distances between each item’s baseline and its hypothetical outer cross-end edge, + // and sum these two values. + CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); + maxStartToBaselineDistance = MAX(maxStartToBaselineDistance, baseline); + maxBaselineToEndDistance = MAX(maxBaselineToEndDistance, crossDimension(style.direction, item.layout.size) - baseline); + } else { + // Step 2. Among all the items not collected by the previous step, find the largest outer hypothetical cross size. + maxItemCrossSize = MAX(maxItemCrossSize, crossDimension(style.direction, item.layout.size)); + } + } + + // Step 3. The used cross-size of the flex line is the largest of the numbers found in the previous two steps and zero. + line.crossSize = MAX(maxStartToBaselineDistance + maxBaselineToEndDistance, maxItemCrossSize); + if (isSingleLine) { + // If the stack is single-line, then clamp the line’s cross-size to be within the stack's min and max cross-size properties. + line.crossSize = MIN(MAX(minCrossSize, line.crossSize), maxCrossSize); + } + + line.baseline = maxStartToBaselineDistance; + } +} + +/** + Returns a lambda that computes the relevant flex factor based on the given violation. + @param violation The amount that the stack layout violates its size range. See header for sign interpretation. + */ +static std::function flexFactorInViolationDirection(const CGFloat violation) +{ + if (std::fabs(violation) < kViolationEpsilon) { + return [](const ASStackLayoutSpecItem &item) { return 0.0; }; + } else if (violation > 0) { + return [](const ASStackLayoutSpecItem &item) { return item.child.style.flexGrow; }; + } else { + return [](const ASStackLayoutSpecItem &item) { return item.child.style.flexShrink; }; + } +} + +static inline CGFloat scaledFlexShrinkFactor(const ASStackLayoutSpecItem &item, + const ASStackLayoutSpecStyle &style, + const CGFloat flexFactorSum) +{ + return stackDimension(style.direction, item.layout.size) * (item.child.style.flexShrink / flexFactorSum); +} + +/** + Returns a lambda that computes a flex shrink adjustment for a given item based on the provided violation. + @param items The unpositioned items from the original unconstrained layout pass. + @param style The layout style to be applied to all children. + @param violation The amount that the stack layout violates its size range. + @param flexFactorSum The sum of each item's flex factor as determined by the provided violation. + @return A lambda capable of computing the flex shrink adjustment, if any, for a particular item. + */ +static std::function flexShrinkAdjustment(const std::vector &items, + const ASStackLayoutSpecStyle &style, + const CGFloat violation, + const CGFloat flexFactorSum) +{ + const CGFloat scaledFlexShrinkFactorSum = std::accumulate(items.begin(), items.end(), 0.0, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x + scaledFlexShrinkFactor(item, style, flexFactorSum); + }); + return [style, scaledFlexShrinkFactorSum, violation, flexFactorSum](const ASStackLayoutSpecItem &item) { + if (scaledFlexShrinkFactorSum == 0.0) { + return (CGFloat)0.0; + } + + const CGFloat scaledFlexShrinkFactorRatio = scaledFlexShrinkFactor(item, style, flexFactorSum) / scaledFlexShrinkFactorSum; + // The item should shrink proportionally to the scaled flex shrink factor ratio computed above. + // Unlike the flex grow adjustment the flex shrink adjustment needs to take the size of each item into account. + return -std::fabs(scaledFlexShrinkFactorRatio * violation); + }; +} + +/** + Returns a lambda that computes a flex grow adjustment for a given item based on the provided violation. + @param items The unpositioned items from the original unconstrained layout pass. + @param violation The amount that the stack layout violates its size range. + @param flexFactorSum The sum of each item's flex factor as determined by the provided violation. + @return A lambda capable of computing the flex grow adjustment, if any, for a particular item. + */ +static std::function flexGrowAdjustment(const std::vector &items, + const CGFloat violation, + const CGFloat flexFactorSum) +{ + // To compute the flex grow adjustment distribute the violation proportionally based on each item's flex grow factor. + return [violation, flexFactorSum](const ASStackLayoutSpecItem &item) { + return std::floor(violation * (item.child.style.flexGrow / flexFactorSum)); + }; +} + +/** + Returns a lambda that computes a flex adjustment for a given item based on the provided violation. + @param items The unpositioned items from the original unconstrained layout pass. + @param style The layout style to be applied to all children. + @param violation The amount that the stack layout violates its size range. + @param flexFactorSum The sum of each item's flex factor as determined by the provided violation. + @return A lambda capable of computing the flex adjustment for a particular item. + */ +static std::function flexAdjustmentInViolationDirection(const std::vector &items, + const ASStackLayoutSpecStyle &style, + const CGFloat violation, + const CGFloat flexFactorSum) +{ + if (violation > 0) { + return flexGrowAdjustment(items, violation, flexFactorSum); + } else { + return flexShrinkAdjustment(items, style, violation, flexFactorSum); + } +} + +ASDISPLAYNODE_INLINE BOOL isFlexibleInBothDirections(const ASStackLayoutSpecChild &child) +{ + return child.style.flexGrow > 0 && child.style.flexShrink > 0; +} + +/** + The flexible children may have been left not laid out in the initial layout pass, so we may have to go through and size + these children at zero size so that the children layouts are at least present. + */ +static void layoutFlexibleChildrenAtZeroSize(std::vector &items, + const ASStackLayoutSpecStyle &style, + const BOOL concurrent, + const ASSizeRange &sizeRange, + const CGSize parentSize) +{ + dispatchApplyIfNeeded(items.size(), concurrent, ^(size_t i) { + auto &item = items[i]; + if (isFlexibleInBothDirections(item.child)) { + item.layout = crossChildLayout(item.child, + style, + 0, + 0, + crossDimension(style.direction, sizeRange.min), + crossDimension(style.direction, sizeRange.max), + parentSize); + } + }); +} + +/** + Computes the consumed stack dimension length for the given vector of items and stacking style. + + stackDimensionSum + <-----------------------> + +-----+ +-------+ +---+ + | | | | | | + | | | | | | + +-----+ | | +---+ + +-------+ + + @param items unpositioned layouts for items + @param style the layout style of the overall stack layout + */ +static CGFloat computeItemsStackDimensionSum(const std::vector &items, + const ASStackLayoutSpecStyle &style) +{ + // Sum up the childrens' spacing + const CGFloat childSpacingSum = std::accumulate(items.begin(), items.end(), + // Start from default spacing between each child: + items.empty() ? 0 : style.spacing * (items.size() - 1), + [&](CGFloat x, const ASStackLayoutSpecItem &l) { + return x + l.child.style.spacingBefore + l.child.style.spacingAfter; + }); + + // Sum up the childrens' dimensions (including spacing) in the stack direction. + const CGFloat childStackDimensionSum = std::accumulate(items.begin(), items.end(), + childSpacingSum, + [&](CGFloat x, const ASStackLayoutSpecItem &l) { + return x + stackDimension(style.direction, l.layout.size); + }); + return childStackDimensionSum; +} + +//TODO move this up near computeCrossViolation and make both methods share the same code path, to make sure they share the same concept of "negative" and "positive" violations. +/** + Computes the violation by comparing a stack dimension sum with the overall allowable size range for the stack. + + Violation is the distance you would have to add to the unbounded stack-direction length of the stack spec's + children in order to bring the stack within its allowed sizeRange. The diagram below shows 3 horizontal stacks with + the different types of violation. + + sizeRange + |------------| + +------+ +-------+ +-------+ +---------+ + | | | | | | | | | | + | | | | | | | | (zero violation) + | | | | | | | | | | + +------+ +-------+ +-------+ +---------+ + | | + +------+ +-------+ +-------+ + | | | | | | | | + | | | | | |<--> (positive violation) + | | | | | | | | + +------+ +-------+ +-------+ + | |<------> (negative violation) + +------+ +-------+ +-------+ +---------+ +-----------+ + | | | | | | | | | | | | + | | | | | | | | | | + | | | | | | | | | | | | + +------+ +-------+ +-------+ +---------+ +-----------+ + + @param stackDimensionSum the consumed length of the children in the stack along the stack dimension + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout spec + */ +CGFloat ASStackUnpositionedLayout::computeStackViolation(const CGFloat stackDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const CGFloat minStackDimension = stackDimension(style.direction, sizeRange.min); + const CGFloat maxStackDimension = stackDimension(style.direction, sizeRange.max); + if (stackDimensionSum < minStackDimension) { + return minStackDimension - stackDimensionSum; + } else if (stackDimensionSum > maxStackDimension) { + return maxStackDimension - stackDimensionSum; + } + return 0; +} + +/** + If we have a single flexible (both shrinkable and growable) child, and our allowed size range is set to a specific + number then we may avoid the first "intrinsic" size calculation. + */ +ASDISPLAYNODE_INLINE BOOL useOptimizedFlexing(const std::vector &children, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const NSUInteger flexibleChildren = std::count_if(children.begin(), children.end(), isFlexibleInBothDirections); + return ((flexibleChildren == 1) + && (stackDimension(style.direction, sizeRange.min) == + stackDimension(style.direction, sizeRange.max))); +} + +/** + Flexes children in the stack axis to resolve a min or max stack size violation. First, determines which children are + flexible (see computeStackViolation and isFlexibleInViolationDirection). Then computes how much to flex each flexible child + and performs re-layout. Note that there may still be a non-zero violation even after flexing. + + The actual CSS flexbox spec describes an iterative looping algorithm here, which may be adopted in t5837937: + http://www.w3.org/TR/css3-flexbox/#resolve-flexible-lengths + + @param lines reference to unpositioned lines and items from the original, unconstrained layout pass; modified in-place + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout component + @param parentSize Size of the stack layout component. May be undefined in either or both directions. + */ +static void flexLinesAlongStackDimension(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const BOOL concurrent, + const ASSizeRange &sizeRange, + const CGSize parentSize, + const BOOL useOptimizedFlexing) +{ + for (auto &line : lines) { + auto &items = line.items; + const CGFloat violation = ASStackUnpositionedLayout::computeStackViolation(computeItemsStackDimensionSum(items, style), style, sizeRange); + std::function flexFactor = flexFactorInViolationDirection(violation); + // The flex factor sum is needed to determine if flexing is necessary. + // This value is also needed if the violation is positive and flexible items need to grow, so keep it around. + const CGFloat flexFactorSum = std::accumulate(items.begin(), items.end(), 0.0, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x + flexFactor(item); + }); + + // If no items are able to flex then there is nothing left to do with this line. Bail. + if (flexFactorSum == 0) { + // If optimized flexing was used then we have to clean up the unsized items and lay them out at zero size. + if (useOptimizedFlexing) { + layoutFlexibleChildrenAtZeroSize(items, style, concurrent, sizeRange, parentSize); + } + continue; + } + + std::function flexAdjustment = flexAdjustmentInViolationDirection(items, + style, + violation, + flexFactorSum); + // Compute any remaining violation to the first flexible item. + const CGFloat remainingViolation = std::accumulate(items.begin(), items.end(), violation, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x - flexAdjustment(item); + }); + + size_t firstFlexItem = -1; + for(size_t i = 0; i < items.size(); i++) { + // Items are consider inflexible if they do not need to make a flex adjustment. + if (flexAdjustment(items[i]) != 0) { + firstFlexItem = i; + break; + } + } + if (firstFlexItem == -1) { + continue; + } + + dispatchApplyIfNeeded(items.size(), concurrent, ^(size_t i) { + auto &item = items[i]; + const CGFloat currentFlexAdjustment = flexAdjustment(item); + // Items are consider inflexible if they do not need to make a flex adjustment. + if (currentFlexAdjustment != 0) { + const CGFloat originalStackSize = stackDimension(style.direction, item.layout.size); + // Only apply the remaining violation for the first flexible item that has a flex grow factor. + const CGFloat flexedStackSize = originalStackSize + currentFlexAdjustment + (i == firstFlexItem && item.child.style.flexGrow > 0 ? remainingViolation : 0); + item.layout = crossChildLayout(item.child, + style, + MAX(flexedStackSize, 0), + MAX(flexedStackSize, 0), + crossDimension(style.direction, sizeRange.min), + crossDimension(style.direction, sizeRange.max), + parentSize); + } + }); + } +} + +/** + https://www.w3.org/TR/css-flexbox-1/#algo-line-break + */ +static std::vector collectChildrenIntoLines(const std::vector &items, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + //TODO if infinite max stack size, fast path + if (style.flexWrap == ASStackLayoutFlexWrapNoWrap) { + return std::vector (1, {.items = std::move(items)}); + } + + std::vector lines; + std::vector lineItems; + CGFloat lineStackDimensionSum = 0; + CGFloat interitemSpacing = 0; + + for(auto it = items.begin(); it != items.end(); ++it) { + const auto &item = *it; + const CGFloat itemStackDimension = stackDimension(style.direction, item.layout.size); + const CGFloat itemAndSpacingStackDimension = item.child.style.spacingBefore + itemStackDimension + item.child.style.spacingAfter; + const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + interitemSpacing + itemAndSpacingStackDimension, style, sizeRange) < 0); + const BOOL breakCurrentLine = negativeViolationIfAddItem && !lineItems.empty(); + + if (breakCurrentLine) { + lines.push_back({.items = std::vector (lineItems)}); + lineItems.clear(); + lineStackDimensionSum = 0; + interitemSpacing = 0; + } + + lineItems.push_back(std::move(item)); + lineStackDimensionSum += interitemSpacing + itemAndSpacingStackDimension; + interitemSpacing = style.spacing; + } + + // Handle last line + lines.push_back({.items = std::vector (lineItems)}); + + return lines; +} + +/** + Performs the first unconstrained layout of the children, generating the unpositioned items that are then flexed and + stretched. + */ +static void layoutItemsAlongUnconstrainedStackDimension(std::vector &items, + const ASStackLayoutSpecStyle &style, + const BOOL concurrent, + const ASSizeRange &sizeRange, + const CGSize parentSize, + const BOOL useOptimizedFlexing) +{ + const CGFloat minCrossDimension = crossDimension(style.direction, sizeRange.min); + const CGFloat maxCrossDimension = crossDimension(style.direction, sizeRange.max); + + dispatchApplyIfNeeded(items.size(), concurrent, ^(size_t i) { + auto &item = items[i]; + if (useOptimizedFlexing && isFlexibleInBothDirections(item.child)) { + item.layout = [ASLayout layoutWithLayoutElement:item.child.element size:{0, 0}]; + } else { + item.layout = crossChildLayout(item.child, + style, + ASDimensionResolve(item.child.style.flexBasis, stackDimension(style.direction, parentSize), 0), + ASDimensionResolve(item.child.style.flexBasis, stackDimension(style.direction, parentSize), INFINITY), + minCrossDimension, + maxCrossDimension, + parentSize); + } + }); +} + +ASStackUnpositionedLayout ASStackUnpositionedLayout::compute(const std::vector &children, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange, + const BOOL concurrent) +{ + if (children.empty()) { + return {}; + } + + // If we have a fixed size in either dimension, pass it to children so they can resolve percentages against it. + // Otherwise, we pass ASLayoutElementParentDimensionUndefined since it will depend on the content. + const CGSize parentSize = { + (sizeRange.min.width == sizeRange.max.width) ? sizeRange.min.width : ASLayoutElementParentDimensionUndefined, + (sizeRange.min.height == sizeRange.max.height) ? sizeRange.min.height : ASLayoutElementParentDimensionUndefined, + }; + + // We may be able to avoid some redundant layout passes + const BOOL optimizedFlexing = useOptimizedFlexing(children, style, sizeRange); + + std::vector items = AS::map(children, [&](const ASStackLayoutSpecChild &child) -> ASStackLayoutSpecItem { + return {child, nil}; + }); + + // We do a first pass of all the children, generating an unpositioned layout for each with an unbounded range along + // the stack dimension. This allows us to compute the "intrinsic" size of each child and find the available violation + // which determines whether we must grow or shrink the flexible children. + layoutItemsAlongUnconstrainedStackDimension(items, + style, + concurrent, + sizeRange, + parentSize, + optimizedFlexing); + + // Collect items into lines (https://www.w3.org/TR/css-flexbox-1/#algo-line-break) + std::vector lines = collectChildrenIntoLines(items, style, sizeRange); + + // Resolve the flexible lengths (https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths) + flexLinesAlongStackDimension(lines, style, concurrent, sizeRange, parentSize, optimizedFlexing); + + // Calculate the cross size of each flex line (https://www.w3.org/TR/css-flexbox-1/#algo-cross-line) + computeLinesCrossSizeAndBaseline(lines, style, sizeRange); + + // Handle 'align-content: stretch' (https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch) + // Determine the used cross size of each item (https://www.w3.org/TR/css-flexbox-1/#algo-stretch) + stretchLinesAlongCrossDimension(lines, style, concurrent, sizeRange, parentSize); + + // Compute stack dimension sum of each line and the whole stack + CGFloat layoutStackDimensionSum = 0; + for (auto &line : lines) { + line.stackDimensionSum = computeItemsStackDimensionSum(line.items, style); + // layoutStackDimensionSum is the max stackDimensionSum among all lines + layoutStackDimensionSum = MAX(line.stackDimensionSum, layoutStackDimensionSum); + } + // Compute cross dimension sum of the stack. + // This should be done before `lines` are moved to a new ASStackUnpositionedLayout struct (i.e `std::move(lines)`) + CGFloat layoutCrossDimensionSum = computeLinesCrossDimensionSum(lines, style); + + return {.lines = std::move(lines), .stackDimensionSum = layoutStackDimensionSum, .crossDimensionSum = layoutCrossDimensionSum}; +} diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextDebugOption.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextDebugOption.h new file mode 100644 index 0000000000..efa7808c87 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextDebugOption.h @@ -0,0 +1,92 @@ +// +// ASTextDebugOption.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASTextDebugOption; + +NS_ASSUME_NONNULL_BEGIN + +/** + The ASTextDebugTarget protocol defines the method a debug target should implement. + A debug target can be add to the global container to receive the shared debug + option changed notification. + */ +@protocol ASTextDebugTarget + +@required +/** + When the shared debug option changed, this method would be called on main thread. + It should return as quickly as possible. The option's property should not be changed + in this method. + + @param option The shared debug option. + */ +- (void)setDebugOption:(nullable ASTextDebugOption *)option; +@end + + + +/** + The debug option for ASText. + */ +@interface ASTextDebugOption : NSObject +@property (nullable, nonatomic) UIColor *baselineColor; ///< baseline color +@property (nullable, nonatomic) UIColor *CTFrameBorderColor; ///< CTFrame path border color +@property (nullable, nonatomic) UIColor *CTFrameFillColor; ///< CTFrame path fill color +@property (nullable, nonatomic) UIColor *CTLineBorderColor; ///< CTLine bounds border color +@property (nullable, nonatomic) UIColor *CTLineFillColor; ///< CTLine bounds fill color +@property (nullable, nonatomic) UIColor *CTLineNumberColor; ///< CTLine line number color +@property (nullable, nonatomic) UIColor *CTRunBorderColor; ///< CTRun bounds border color +@property (nullable, nonatomic) UIColor *CTRunFillColor; ///< CTRun bounds fill color +@property (nullable, nonatomic) UIColor *CTRunNumberColor; ///< CTRun number color +@property (nullable, nonatomic) UIColor *CGGlyphBorderColor; ///< CGGlyph bounds border color +@property (nullable, nonatomic) UIColor *CGGlyphFillColor; ///< CGGlyph bounds fill color + +- (BOOL)needDrawDebug; ///< `YES`: at least one debug color is visible. `NO`: all debug color is invisible/nil. +- (void)clear; ///< Set all debug color to nil. + +/** + Add a debug target. + + @discussion When `setSharedDebugOption:` is called, all added debug target will + receive `setDebugOption:` in main thread. It maintains an unsafe_unretained + reference to this target. The target must to removed before dealloc. + + @param target A debug target. + */ ++ (void)addDebugTarget:(id)target; + +/** + Remove a debug target which is added by `addDebugTarget:`. + + @param target A debug target. + */ ++ (void)removeDebugTarget:(id)target; + +/** + Returns the shared debug option. + + @return The shared debug option, default is nil. + */ ++ (nullable ASTextDebugOption *)sharedDebugOption; + +/** + Set a debug option as shared debug option. + This method must be called on main thread. + + @discussion When call this method, the new option will set to all debug target + which is added by `addDebugTarget:`. + + @param option A new debug option (nil is valid). + */ ++ (void)setSharedDebugOption:(nullable ASTextDebugOption *)option; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextDebugOption.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextDebugOption.mm new file mode 100644 index 0000000000..2565b903c9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextDebugOption.mm @@ -0,0 +1,135 @@ +// +// ASTextDebugOption.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTextDebugOption.h" +#import + +static pthread_mutex_t _sharedDebugLock; +static CFMutableSetRef _sharedDebugTargets = nil; +static ASTextDebugOption *_sharedDebugOption = nil; + +static const void* _as_sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { + return value; +} + +static void _as_sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { +} + +void _as_sharedDebugSetFunction(const void *value, void *context) { + id target = (__bridge id)(value); + [target setDebugOption:_sharedDebugOption]; +} + +static void _initSharedDebug() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_mutex_init(&_sharedDebugLock, NULL); + CFSetCallBacks callbacks = kCFTypeSetCallBacks; + callbacks.retain = _as_sharedDebugSetRetain; + callbacks.release = _as_sharedDebugSetRelease; + _sharedDebugTargets = CFSetCreateMutable(CFAllocatorGetDefault(), 0, &callbacks); + }); +} + +static void _setSharedDebugOption(ASTextDebugOption *option) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + _sharedDebugOption = option.copy; + CFSetApplyFunction(_sharedDebugTargets, _as_sharedDebugSetFunction, NULL); + pthread_mutex_unlock(&_sharedDebugLock); +} + +static ASTextDebugOption *_getSharedDebugOption() { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + ASTextDebugOption *op = _sharedDebugOption; + pthread_mutex_unlock(&_sharedDebugLock); + return op; +} + +static void _addDebugTarget(id target) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + CFSetAddValue(_sharedDebugTargets, (__bridge const void *)(target)); + pthread_mutex_unlock(&_sharedDebugLock); +} + +static void _removeDebugTarget(id target) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + CFSetRemoveValue(_sharedDebugTargets, (__bridge const void *)(target)); + pthread_mutex_unlock(&_sharedDebugLock); +} + + +@implementation ASTextDebugOption + +- (id)copyWithZone:(NSZone *)zone { + ASTextDebugOption *op = [self.class new]; + op.baselineColor = self.baselineColor; + op.CTFrameBorderColor = self.CTFrameBorderColor; + op.CTFrameFillColor = self.CTFrameFillColor; + op.CTLineBorderColor = self.CTLineBorderColor; + op.CTLineFillColor = self.CTLineFillColor; + op.CTLineNumberColor = self.CTLineNumberColor; + op.CTRunBorderColor = self.CTRunBorderColor; + op.CTRunFillColor = self.CTRunFillColor; + op.CTRunNumberColor = self.CTRunNumberColor; + op.CGGlyphBorderColor = self.CGGlyphBorderColor; + op.CGGlyphFillColor = self.CGGlyphFillColor; + return op; +} + +- (BOOL)needDrawDebug { + if (self.baselineColor || + self.CTFrameBorderColor || + self.CTFrameFillColor || + self.CTLineBorderColor || + self.CTLineFillColor || + self.CTLineNumberColor || + self.CTRunBorderColor || + self.CTRunFillColor || + self.CTRunNumberColor || + self.CGGlyphBorderColor || + self.CGGlyphFillColor) return YES; + return NO; +} + +- (void)clear { + self.baselineColor = nil; + self.CTFrameBorderColor = nil; + self.CTFrameFillColor = nil; + self.CTLineBorderColor = nil; + self.CTLineFillColor = nil; + self.CTLineNumberColor = nil; + self.CTRunBorderColor = nil; + self.CTRunFillColor = nil; + self.CTRunNumberColor = nil; + self.CGGlyphBorderColor = nil; + self.CGGlyphFillColor = nil; +} + ++ (void)addDebugTarget:(id)target { + if (target) _addDebugTarget(target); +} + ++ (void)removeDebugTarget:(id)target { + if (target) _removeDebugTarget(target); +} + ++ (ASTextDebugOption *)sharedDebugOption { + return _getSharedDebugOption(); +} + ++ (void)setSharedDebugOption:(ASTextDebugOption *)option { + NSAssert([NSThread isMainThread], @"This method must be called on the main thread"); + _setSharedDebugOption(option); +} + +@end + diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextInput.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextInput.h new file mode 100644 index 0000000000..9a6cbd13d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextInput.h @@ -0,0 +1,85 @@ +// +// ASTextInput.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Text position affinity. For example, the offset appears after the last + character on a line is backward affinity, before the first character on + the following line is forward affinity. + */ +typedef NS_ENUM(NSInteger, ASTextAffinity) { + ASTextAffinityForward = 0, ///< offset appears before the character + ASTextAffinityBackward = 1, ///< offset appears after the character +}; + + +/** + A ASTextPosition object represents a position in a text container; in other words, + it is an index into the backing string in a text-displaying view. + + ASTextPosition has the same API as Apple's implementation in UITextView/UITextField, + so you can alse use it to interact with UITextView/UITextField. + */ +@interface ASTextPosition : UITextPosition + +@property (nonatomic, readonly) NSInteger offset; +@property (nonatomic, readonly) ASTextAffinity affinity; + ++ (instancetype)positionWithOffset:(NSInteger)offset NS_RETURNS_RETAINED; ++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity) affinity NS_RETURNS_RETAINED; + +- (NSComparisonResult)compare:(id)otherPosition; + +@end + + +/** + A ASTextRange object represents a range of characters in a text container; in other words, + it identifies a starting index and an ending index in string backing a text-displaying view. + + ASTextRange has the same API as Apple's implementation in UITextView/UITextField, + so you can alse use it to interact with UITextView/UITextField. + */ +@interface ASTextRange : UITextRange + +@property (nonatomic, readonly) ASTextPosition *start; +@property (nonatomic, readonly) ASTextPosition *end; +@property (nonatomic, readonly, getter=isEmpty) BOOL empty; + ++ (instancetype)rangeWithRange:(NSRange)range NS_RETURNS_RETAINED; ++ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity) affinity NS_RETURNS_RETAINED; ++ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end NS_RETURNS_RETAINED; ++ (instancetype)defaultRange NS_RETURNS_RETAINED; ///< <{0,0} Forward> + +- (NSRange)asRange; + +@end + + +/** + A ASTextSelectionRect object encapsulates information about a selected range of + text in a text-displaying view. + + ASTextSelectionRect has the same API as Apple's implementation in UITextView/UITextField, + so you can alse use it to interact with UITextView/UITextField. + */ +@interface ASTextSelectionRect : UITextSelectionRect + +@property (nonatomic) CGRect rect; +@property (nonatomic) UITextWritingDirection writingDirection; +@property (nonatomic) BOOL containsStart; +@property (nonatomic) BOOL containsEnd; +@property (nonatomic) BOOL isVertical; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextInput.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextInput.mm new file mode 100644 index 0000000000..1cdfe73858 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextInput.mm @@ -0,0 +1,150 @@ +// +// ASTextInput.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + + +@implementation ASTextPosition + ++ (instancetype)positionWithOffset:(NSInteger)offset NS_RETURNS_RETAINED { + return [self positionWithOffset:offset affinity:ASTextAffinityForward]; +} + ++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity)affinity NS_RETURNS_RETAINED { + ASTextPosition *p = [self new]; + p->_offset = offset; + p->_affinity = affinity; + return p; +} + +- (instancetype)copyWithZone:(NSZone *)zone { + return [self.class positionWithOffset:_offset affinity:_affinity]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> (%@%@)", self.class, self, @(_offset), _affinity == ASTextAffinityForward ? @"F":@"B"]; +} + +- (NSUInteger)hash { + return _offset * 2 + (_affinity == ASTextAffinityForward ? 1 : 0); +} + +- (BOOL)isEqual:(ASTextPosition *)object { + if (!object) return NO; + return _offset == object.offset && _affinity == object.affinity; +} + +- (NSComparisonResult)compare:(ASTextPosition *)otherPosition { + if (!otherPosition) return NSOrderedAscending; + if (_offset < otherPosition.offset) return NSOrderedAscending; + if (_offset > otherPosition.offset) return NSOrderedDescending; + if (_affinity == ASTextAffinityBackward && otherPosition.affinity == ASTextAffinityForward) return NSOrderedAscending; + if (_affinity == ASTextAffinityForward && otherPosition.affinity == ASTextAffinityBackward) return NSOrderedDescending; + return NSOrderedSame; +} + +@end + + + +@implementation ASTextRange { + ASTextPosition *_start; + ASTextPosition *_end; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + _start = [ASTextPosition positionWithOffset:0]; + _end = [ASTextPosition positionWithOffset:0]; + return self; +} + +- (ASTextPosition *)start { + return _start; +} + +- (ASTextPosition *)end { + return _end; +} + +- (BOOL)isEmpty { + return _start.offset == _end.offset; +} + +- (NSRange)asRange { + return NSMakeRange(_start.offset, _end.offset - _start.offset); +} + ++ (instancetype)rangeWithRange:(NSRange)range NS_RETURNS_RETAINED { + return [self rangeWithRange:range affinity:ASTextAffinityForward]; +} + ++ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity)affinity NS_RETURNS_RETAINED { + ASTextPosition *start = [ASTextPosition positionWithOffset:range.location affinity:affinity]; + ASTextPosition *end = [ASTextPosition positionWithOffset:range.location + range.length affinity:affinity]; + return [self rangeWithStart:start end:end]; +} + ++ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end NS_RETURNS_RETAINED { + if (!start || !end) return nil; + if ([start compare:end] == NSOrderedDescending) { + ASTEXT_SWAP(start, end); + } + ASTextRange *range = [ASTextRange new]; + range->_start = start; + range->_end = end; + return range; +} + ++ (instancetype)defaultRange NS_RETURNS_RETAINED { + return [self new]; +} + +- (instancetype)copyWithZone:(NSZone *)zone { + return [self.class rangeWithStart:_start end:_end]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> (%@, %@)%@", self.class, self, @(_start.offset), @(_end.offset - _start.offset), _end.affinity == ASTextAffinityForward ? @"F":@"B"]; +} + +- (NSUInteger)hash { + return (sizeof(NSUInteger) == 8 ? OSSwapInt64(_start.hash) : OSSwapInt32(_start.hash)) + _end.hash; +} + +- (BOOL)isEqual:(ASTextRange *)object { + if (!object) return NO; + return [_start isEqual:object.start] && [_end isEqual:object.end]; +} + +@end + + + +@implementation ASTextSelectionRect + +@synthesize rect = _rect; +@synthesize writingDirection = _writingDirection; +@synthesize containsStart = _containsStart; +@synthesize containsEnd = _containsEnd; +@synthesize isVertical = _isVertical; + +- (id)copyWithZone:(NSZone *)zone { + ASTextSelectionRect *one = [self.class new]; + one.rect = _rect; + one.writingDirection = _writingDirection; + one.containsStart = _containsStart; + one.containsEnd = _containsEnd; + one.isVertical = _isVertical; + return one; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLayout.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLayout.h new file mode 100644 index 0000000000..46bc8ccf52 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLayout.h @@ -0,0 +1,547 @@ +// +// ASTextLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import "ASTextDebugOption.h" +#import "ASTextLine.h" +#import "ASTextInput.h" + +@protocol ASTextLinePositionModifier; + +NS_ASSUME_NONNULL_BEGIN + +/** + The max text container size in layout. + */ +AS_EXTERN const CGSize ASTextContainerMaxSize; + +/** + The ASTextContainer class defines a region in which text is laid out. + ASTextLayout class uses one or more ASTextContainer objects to generate layouts. + + A ASTextContainer defines rectangular regions (`size` and `insets`) or + nonrectangular shapes (`path`), and you can define exclusion paths inside the + text container's bounding rectangle so that text flows around the exclusion + path as it is laid out. + + All methods in this class is thread-safe. + + Example: + + ┌─────────────────────────────┐ <------- container + │ │ + │ asdfasdfasdfasdfasdfa <------------ container insets + │ asdfasdfa asdfasdfa │ + │ asdfas asdasd │ + │ asdfa <----------------------- container exclusion path + │ asdfas adfasd │ + │ asdfasdfa asdfasdfa │ + │ asdfasdfasdfasdfasdfa │ + │ │ + └─────────────────────────────┘ + */ +@interface ASTextContainer : NSObject + +/// Creates a container with the specified size. @param size The size. ++ (instancetype)containerWithSize:(CGSize)size NS_RETURNS_RETAINED; + +/// Creates a container with the specified size and insets. @param size The size. @param insets The text insets. ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets NS_RETURNS_RETAINED; + +/// Creates a container with the specified path. @param path The path. ++ (instancetype)containerWithPath:(nullable UIBezierPath *)path NS_RETURNS_RETAINED; + +/// Mark this immutable, so you get free copies going forward. +- (void)makeImmutable; + +/// The constrained size. (if the size is larger than ASTextContainerMaxSize, it will be clipped) +@property CGSize size; + +/// The insets for constrained size. The inset value should not be negative. Default is UIEdgeInsetsZero. +@property UIEdgeInsets insets; + +/// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil. +@property (nullable, copy) UIBezierPath *path; + +/// An array of `UIBezierPath` for path exclusion. Default is nil. +@property (nullable, copy) NSArray *exclusionPaths; + +/// Path line width. Default is 0; +@property CGFloat pathLineWidth; + +/// YES:(PathFillEvenOdd) Text is filled in the area that would be painted if the path were given to CGContextEOFillPath. +/// NO: (PathFillWindingNumber) Text is fill in the area that would be painted if the path were given to CGContextFillPath. +/// Default is YES; +@property (getter=isPathFillEvenOdd) BOOL pathFillEvenOdd; + +/// Whether the text is vertical form (may used for CJK text layout). Default is NO. +@property (getter=isVerticalForm) BOOL verticalForm; + +/// Maximum number of rows, 0 means no limit. Default is 0. +@property NSUInteger maximumNumberOfRows; + +/// The line truncation type, default is none. +@property ASTextTruncationType truncationType; + +/// The truncation token. If nil, the layout will use "…" instead. Default is nil. +@property (nullable, copy) NSAttributedString *truncationToken; + +/// This modifier is applied to the lines before the layout is completed, +/// give you a chance to modify the line position. Default is nil. +@property (nullable, copy) id linePositionModifier; +@end + + +/** + The ASTextLinePositionModifier protocol declares the required method to modify + the line position in text layout progress. See `ASTextLinePositionSimpleModifier` for example. + */ +@protocol ASTextLinePositionModifier +@required +/** + This method will called before layout is completed. The method should be thread-safe. + @param lines An array of ASTextLine. + @param text The full text. + @param container The layout container. + */ +- (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(ASTextContainer *)container; +@end + + +/** + A simple implementation of `ASTextLinePositionModifier`. It can fix each line's position + to a specified value, lets each line of height be the same. + */ +@interface ASTextLinePositionSimpleModifier : NSObject +@property CGFloat fixedLineHeight; ///< The fixed line height (distance between two baseline). +@end + + + +/** + ASTextLayout class is a readonly class stores text layout result. + All the property in this class is readonly, and should not be changed. + The methods in this class is thread-safe (except some of the draw methods). + + example: (layout with a circle exclusion path) + + ┌──────────────────────────┐ <------ container + │ [--------Line0--------] │ <- Row0 + │ [--------Line1--------] │ <- Row1 + │ [-Line2-] [-Line3-] │ <- Row2 + │ [-Line4] [Line5-] │ <- Row3 + │ [-Line6-] [-Line7-] │ <- Row4 + │ [--------Line8--------] │ <- Row5 + │ [--------Line9--------] │ <- Row6 + └──────────────────────────┘ + */ +@interface ASTextLayout : NSObject + + +#pragma mark - Generate text layout +///============================================================================= +/// @name Generate text layout +///============================================================================= + +/** + Generate a layout with the given container size and text. + + @param size The text container's size + @param text The text (if nil, returns nil). + @return A new layout, or nil when an error occurs. + */ ++ (nullable ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text; + +/** + Generate a layout with the given container and text. + + @param container The text container (if nil, returns nil). + @param text The text (if nil, returns nil). + @return A new layout, or nil when an error occurs. + */ ++ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text; + +/** + Generate a layout with the given container and text. + + @param container The text container (if nil, returns nil). + @param text The text (if nil, returns nil). + @param range The text range (if out of range, returns nil). If the + length of the range is 0, it means the length is no limit. + @return A new layout, or nil when an error occurs. + */ ++ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range; + +/** + Generate layouts with the given containers and text. + + @param containers An array of ASTextContainer object (if nil, returns nil). + @param text The text (if nil, returns nil). + @return An array of ASTextLayout object (the count is same as containers), + or nil when an error occurs. + */ ++ (nullable NSArray *)layoutWithContainers:(NSArray *)containers + text:(NSAttributedString *)text; + +/** + Generate layouts with the given containers and text. + + @param containers An array of ASTextContainer object (if nil, returns nil). + @param text The text (if nil, returns nil). + @param range The text range (if out of range, returns nil). If the + length of the range is 0, it means the length is no limit. + @return An array of ASTextLayout object (the count is same as containers), + or nil when an error occurs. + */ ++ (nullable NSArray *)layoutWithContainers:(NSArray *)containers + text:(NSAttributedString *)text + range:(NSRange)range; + +- (instancetype)init UNAVAILABLE_ATTRIBUTE; ++ (instancetype)new UNAVAILABLE_ATTRIBUTE; + + +#pragma mark - Text layout attributes +///============================================================================= +/// @name Text layout attributes +///============================================================================= + +///< The text container +@property (nonatomic, readonly) ASTextContainer *container; +///< The full text +@property (nonatomic, readonly) NSAttributedString *text; +///< The text range in full text +@property (nonatomic, readonly) NSRange range; +///< CTFrame +@property (nonatomic, readonly) CTFrameRef frame; +///< Array of `ASTextLine`, no truncated +@property (nonatomic, readonly) NSArray *lines; +///< ASTextLine with truncated token, or nil +@property (nullable, nonatomic, readonly) ASTextLine *truncatedLine; +///< Array of `ASTextAttachment` +@property (nullable, nonatomic, readonly) NSArray *attachments; +///< Array of NSRange(wrapped by NSValue) in text +@property (nullable, nonatomic, readonly) NSArray *attachmentRanges; +///< Array of CGRect(wrapped by NSValue) in container +@property (nullable, nonatomic, readonly) NSArray *attachmentRects; +///< Set of Attachment (UIImage/UIView/CALayer) +@property (nullable, nonatomic, readonly) NSSet *attachmentContentsSet; +///< Number of rows +@property (nonatomic, readonly) NSUInteger rowCount; +///< Visible text range +@property (nonatomic, readonly) NSRange visibleRange; +///< Bounding rect (glyphs) +@property (nonatomic, readonly) CGRect textBoundingRect; +///< Bounding size (glyphs and insets, ceil to pixel) +@property (nonatomic, readonly) CGSize textBoundingSize; +///< Has highlight attribute +@property (nonatomic, readonly) BOOL containsHighlight; +///< Has block border attribute +@property (nonatomic, readonly) BOOL needDrawBlockBorder; +///< Has background border attribute +@property (nonatomic, readonly) BOOL needDrawBackgroundBorder; +///< Has shadow attribute +@property (nonatomic, readonly) BOOL needDrawShadow; +///< Has underline attribute +@property (nonatomic, readonly) BOOL needDrawUnderline; +///< Has visible text +@property (nonatomic, readonly) BOOL needDrawText; +///< Has attachment attribute +@property (nonatomic, readonly) BOOL needDrawAttachment; +///< Has inner shadow attribute +@property (nonatomic, readonly) BOOL needDrawInnerShadow; +///< Has strickthrough attribute +@property (nonatomic, readonly) BOOL needDrawStrikethrough; +///< Has border attribute +@property (nonatomic, readonly) BOOL needDrawBorder; + + +#pragma mark - Query information from text layout +///============================================================================= +/// @name Query information from text layout +///============================================================================= + +/** + The first line index for row. + + @param row A row index. + @return The line index, or NSNotFound if not found. + */ +- (NSUInteger)lineIndexForRow:(NSUInteger)row; + +/** + The number of lines for row. + + @param row A row index. + @return The number of lines, or NSNotFound when an error occurs. + */ +- (NSUInteger)lineCountForRow:(NSUInteger)row; + +/** + The row index for line. + + @param line A row index. + + @return The row index, or NSNotFound if not found. + */ +- (NSUInteger)rowIndexForLine:(NSUInteger)line; + +/** + The line index for a specified point. + + @discussion It returns NSNotFound if there's no text at the point. + + @param point A point in the container. + @return The line index, or NSNotFound if not found. + */ +- (NSUInteger)lineIndexForPoint:(CGPoint)point; + +/** + The line index closest to a specified point. + + @param point A point in the container. + @return The line index, or NSNotFound if no line exist in layout. + */ +- (NSUInteger)closestLineIndexForPoint:(CGPoint)point; + +/** + The offset in container for a text position in a specified line. + + @discussion The offset is the text position's baseline point.x. + If the container is vertical form, the offset is the baseline point.y; + + @param position The text position in string. + @param lineIndex The line index. + @return The offset in container, or CGFLOAT_MAX if not found. + */ +- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex; + +/** + The text position for a point in a specified line. + + @discussion This method just call CTLineGetStringIndexForPosition() and does + NOT consider the emoji, line break character, binding text... + + @param point A point in the container. + @param lineIndex The line index. + @return The text position, or NSNotFound if not found. + */ +- (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex; + +/** + The closest text position to a specified point. + + @discussion This method takes into account the restrict of emoji, line break + character, binding text and text affinity. + + @param point A point in the container. + @return A text position, or nil if not found. + */ +- (nullable ASTextPosition *)closestPositionToPoint:(CGPoint)point; + +/** + Returns the new position when moving selection grabber in text view. + + @discussion There are two grabber in the text selection period, user can only + move one grabber at the same time. + + @param point A point in the container. + @param oldPosition The old text position for the moving grabber. + @param otherPosition The other position in text selection view. + + @return A text position, or nil if not found. + */ +- (nullable ASTextPosition *)positionForPoint:(CGPoint)point + oldPosition:(ASTextPosition *)oldPosition + otherPosition:(ASTextPosition *)otherPosition; + +/** + Returns the character or range of characters that is at a given point in the container. + If there is no text at the point, returns nil. + + @discussion This method takes into account the restrict of emoji, line break + character, binding text and text affinity. + + @param point A point in the container. + @return An object representing a range that encloses a character (or characters) + at point. Or nil if not found. + */ +- (nullable ASTextRange *)textRangeAtPoint:(CGPoint)point; + +/** + Returns the closest character or range of characters that is at a given point in + the container. + + @discussion This method takes into account the restrict of emoji, line break + character, binding text and text affinity. + + @param point A point in the container. + @return An object representing a range that encloses a character (or characters) + at point. Or nil if not found. + */ +- (nullable ASTextRange *)closestTextRangeAtPoint:(CGPoint)point; + +/** + If the position is inside an emoji, composed character sequences, line break '\\r\\n' + or custom binding range, then returns the range by extend the position. Otherwise, + returns a zero length range from the position. + + @param position A text-position object that identifies a location in layout. + + @return A text-range object that extend the position. Or nil if an error occurs + */ +- (nullable ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position; + +/** + Returns a text range at a given offset in a specified direction from another + text position to its farthest extent in a certain direction of layout. + + @param position A text-position object that identifies a location in layout. + @param direction A constant that indicates a direction of layout (right, left, up, down). + @param offset A character offset from position. + + @return A text-range object that represents the distance from position to the + farthest extent in direction. Or nil if an error occurs. + */ +- (nullable ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position + inDirection:(UITextLayoutDirection)direction + offset:(NSInteger)offset; + +/** + Returns the line index for a given text position. + + @discussion This method takes into account the text affinity. + + @param position A text-position object that identifies a location in layout. + @return The line index, or NSNotFound if not found. + */ +- (NSUInteger)lineIndexForPosition:(ASTextPosition *)position; + +/** + Returns the baseline position for a given text position. + + @param position An object that identifies a location in the layout. + @return The baseline position for text, or CGPointZero if not found. + */ +- (CGPoint)linePositionForPosition:(ASTextPosition *)position; + +/** + Returns a rectangle used to draw the caret at a given insertion point. + + @param position An object that identifies a location in the layout. + @return A rectangle that defines the area for drawing the caret. The width is + always zero in normal container, the height is always zero in vertical form container. + If not found, it returns CGRectNull. + */ +- (CGRect)caretRectForPosition:(ASTextPosition *)position; + +/** + Returns the first rectangle that encloses a range of text in the layout. + + @param range An object that represents a range of text in layout. + + @return The first rectangle in a range of text. You might use this rectangle to + draw a correction rectangle. The "first" in the name refers the rectangle + enclosing the first line when the range encompasses multiple lines of text. + If not found, it returns CGRectNull. + */ +- (CGRect)firstRectForRange:(ASTextRange *)range; + +/** + Returns the rectangle union that encloses a range of text in the layout. + + @param range An object that represents a range of text in layout. + + @return A rectangle that defines the area than encloses the range. + If not found, it returns CGRectNull. + */ +- (CGRect)rectForRange:(ASTextRange *)range; + +/** + Returns an array of selection rects corresponding to the range of text. + The start and end rect can be used to show grabber. + + @param range An object representing a range in text. + @return An array of `ASTextSelectionRect` objects that encompass the selection. + If not found, the array is empty. + */ +- (NSArray *)selectionRectsForRange:(ASTextRange *)range; + +/** + Returns an array of selection rects corresponding to the range of text. + + @param range An object representing a range in text. + @return An array of `ASTextSelectionRect` objects that encompass the selection. + If not found, the array is empty. + */ +- (NSArray *)selectionRectsWithoutStartAndEndForRange:(ASTextRange *)range; + +/** + Returns the start and end selection rects corresponding to the range of text. + The start and end rect can be used to show grabber. + + @param range An object representing a range in text. + @return An array of `ASTextSelectionRect` objects contains the start and end to + the selection. If not found, the array is empty. + */ +- (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(ASTextRange *)range; + + +#pragma mark - Draw text layout +///============================================================================= +/// @name Draw text layout +///============================================================================= + +/** + Draw the layout and show the attachments. + + @discussion If the `view` parameter is not nil, then the attachment views will + add to this `view`, and if the `layer` parameter is not nil, then the attachment + layers will add to this `layer`. + + @warning This method should be called on main thread if `view` or `layer` parameter + is not nil and there's UIView or CALayer attachments in layout. + Otherwise, it can be called on any thread. + + @param context The draw context. Pass nil to avoid text and image drawing. + @param size The context size. + @param point The point at which to draw the layout. + @param view The attachment views will add to this view. + @param layer The attachment layers will add to this layer. + @param debug The debug option. Pass nil to avoid debug drawing. + @param cancel The cancel checker block. It will be called in drawing progress. + If it returns YES, the further draw progress will be canceled. + Pass nil to ignore this feature. + */ +- (void)drawInContext:(nullable CGContextRef)context + size:(CGSize)size + point:(CGPoint)point + view:(nullable UIView *)view + layer:(nullable CALayer *)layer + debug:(nullable ASTextDebugOption *)debug + cancel:(nullable BOOL (^)(void))cancel; + +/** + Draw the layout text and image (without view or layer attachments). + + @discussion This method is thread safe and can be called on any thread. + + @param context The draw context. Pass nil to avoid text and image drawing. + @param size The context size. + @param debug The debug option. Pass nil to avoid debug drawing. + */ +- (void)drawInContext:(nullable CGContextRef)context + size:(CGSize)size + debug:(nullable ASTextDebugOption *)debug; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLayout.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLayout.mm new file mode 100644 index 0000000000..135709a845 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLayout.mm @@ -0,0 +1,3485 @@ +// +// ASTextLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import +#import + +#import + +const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; + +typedef struct { + CGFloat head; + CGFloat foot; +} ASRowEdge; + +static inline CGSize ASTextClipCGSize(CGSize size) { + if (size.width > ASTextContainerMaxSize.width) size.width = ASTextContainerMaxSize.width; + if (size.height > ASTextContainerMaxSize.height) size.height = ASTextContainerMaxSize.height; + return size; +} + +static inline UIEdgeInsets UIEdgeInsetRotateVertical(UIEdgeInsets insets) { + UIEdgeInsets one; + one.top = insets.left; + one.left = insets.bottom; + one.bottom = insets.right; + one.right = insets.top; + return one; +} + +/** + Sometimes CoreText may convert CGColor to UIColor for `kCTForegroundColorAttributeName` + attribute in iOS7. This should be a bug of CoreText, and may cause crash. Here's a workaround. + */ +static CGColorRef ASTextGetCGColor(CGColorRef color) { + static UIColor *defaultColor; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultColor = [UIColor blackColor]; + }); + if (!color) return defaultColor.CGColor; + if ([((__bridge NSObject *)color) respondsToSelector:@selector(CGColor)]) { + return ((__bridge UIColor *)color).CGColor; + } + return color; +} + +@implementation ASTextLinePositionSimpleModifier +- (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(ASTextContainer *)container { + if (container.verticalForm) { + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + CGPoint pos = line.position; + pos.x = container.size.width - container.insets.right - line.row * _fixedLineHeight - _fixedLineHeight * 0.9; + line.position = pos; + } + } else { + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + CGPoint pos = line.position; + pos.y = line.row * _fixedLineHeight + _fixedLineHeight * 0.9 + container.insets.top; + line.position = pos; + } + } +} + +- (id)copyWithZone:(NSZone *)zone { + ASTextLinePositionSimpleModifier *one = [self.class new]; + one.fixedLineHeight = _fixedLineHeight; + return one; +} +@end + + +@implementation ASTextContainer { + @package + BOOL _readonly; ///< used only in ASTextLayout.implementation + dispatch_semaphore_t _lock; + + CGSize _size; + UIEdgeInsets _insets; + UIBezierPath *_path; + NSArray *_exclusionPaths; + BOOL _pathFillEvenOdd; + CGFloat _pathLineWidth; + BOOL _verticalForm; + NSUInteger _maximumNumberOfRows; + ASTextTruncationType _truncationType; + NSAttributedString *_truncationToken; + id _linePositionModifier; +} + +- (NSString *)description +{ + return [NSString + stringWithFormat:@"immutable: %@, insets: %@, size: %@", self->_readonly ? @"YES" : @"NO", + NSStringFromUIEdgeInsets(self->_insets), NSStringFromCGSize(self->_size)]; +} + ++ (instancetype)containerWithSize:(CGSize)size NS_RETURNS_RETAINED { + return [self containerWithSize:size insets:UIEdgeInsetsZero]; +} + ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets NS_RETURNS_RETAINED { + ASTextContainer *one = [self new]; + one.size = ASTextClipCGSize(size); + one.insets = insets; + return one; +} + ++ (instancetype)containerWithPath:(UIBezierPath *)path NS_RETURNS_RETAINED { + ASTextContainer *one = [self new]; + one.path = path; + return one; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + _lock = dispatch_semaphore_create(1); + _pathFillEvenOdd = YES; + return self; +} + +- (id)copyForced:(BOOL)forceCopy +{ + dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); + if (_readonly && !forceCopy) { + dispatch_semaphore_signal(_lock); + return self; + } + + ASTextContainer *one = [self.class new]; + one->_size = _size; + one->_insets = _insets; + one->_path = _path; + one->_exclusionPaths = [_exclusionPaths copy]; + one->_pathFillEvenOdd = _pathFillEvenOdd; + one->_pathLineWidth = _pathLineWidth; + one->_verticalForm = _verticalForm; + one->_maximumNumberOfRows = _maximumNumberOfRows; + one->_truncationType = _truncationType; + one->_truncationToken = [_truncationToken copy]; + one->_linePositionModifier = [(NSObject *)_linePositionModifier copy]; + dispatch_semaphore_signal(_lock); + return one; +} + +- (id)copyWithZone:(NSZone *)zone { + return [self copyForced:NO]; +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + return [self copyForced:YES]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:[NSValue valueWithCGSize:_size] forKey:@"size"]; + [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:_insets] forKey:@"insets"]; + [aCoder encodeObject:_path forKey:@"path"]; + [aCoder encodeObject:_exclusionPaths forKey:@"exclusionPaths"]; + [aCoder encodeBool:_pathFillEvenOdd forKey:@"pathFillEvenOdd"]; + [aCoder encodeDouble:_pathLineWidth forKey:@"pathLineWidth"]; + [aCoder encodeBool:_verticalForm forKey:@"verticalForm"]; + [aCoder encodeInteger:_maximumNumberOfRows forKey:@"maximumNumberOfRows"]; + [aCoder encodeInteger:_truncationType forKey:@"truncationType"]; + [aCoder encodeObject:_truncationToken forKey:@"truncationToken"]; + if ([_linePositionModifier respondsToSelector:@selector(encodeWithCoder:)] && + [_linePositionModifier respondsToSelector:@selector(initWithCoder:)]) { + [aCoder encodeObject:_linePositionModifier forKey:@"linePositionModifier"]; + } +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [self init]; + _size = ((NSValue *)[aDecoder decodeObjectForKey:@"size"]).CGSizeValue; + _insets = ((NSValue *)[aDecoder decodeObjectForKey:@"insets"]).UIEdgeInsetsValue; + _path = [aDecoder decodeObjectForKey:@"path"]; + _exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"]; + _pathFillEvenOdd = [aDecoder decodeBoolForKey:@"pathFillEvenOdd"]; + _pathLineWidth = [aDecoder decodeDoubleForKey:@"pathLineWidth"]; + _verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"]; + _maximumNumberOfRows = [aDecoder decodeIntegerForKey:@"maximumNumberOfRows"]; + _truncationType = (ASTextTruncationType)[aDecoder decodeIntegerForKey:@"truncationType"]; + _truncationToken = [aDecoder decodeObjectForKey:@"truncationToken"]; + _linePositionModifier = [aDecoder decodeObjectForKey:@"linePositionModifier"]; + return self; +} + +- (void)makeImmutable +{ + dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); + _readonly = YES; + dispatch_semaphore_signal(_lock); +} + +#define Getter(...) \ +dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +__VA_ARGS__; \ +dispatch_semaphore_signal(_lock); + +#define Setter(...) \ +dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +if (__builtin_expect(_readonly, NO)) { \ + ASDisplayNodeFailAssert(@"Attempt to modify immutable text container."); \ + dispatch_semaphore_signal(_lock); \ + return; \ +} \ +__VA_ARGS__; \ +dispatch_semaphore_signal(_lock); + +- (CGSize)size { + Getter(CGSize size = _size) return size; +} + +- (void)setSize:(CGSize)size { + Setter(if(!_path) _size = ASTextClipCGSize(size)); +} + +- (UIEdgeInsets)insets { + Getter(UIEdgeInsets insets = _insets) return insets; +} + +- (void)setInsets:(UIEdgeInsets)insets { + Setter(if(!_path){ + if (insets.top < 0) insets.top = 0; + if (insets.left < 0) insets.left = 0; + if (insets.bottom < 0) insets.bottom = 0; + if (insets.right < 0) insets.right = 0; + _insets = insets; + }); +} + +- (UIBezierPath *)path { + Getter(UIBezierPath *path = _path) return path; +} + +- (void)setPath:(UIBezierPath *)path { + Setter( + _path = path.copy; + if (_path) { + CGRect bounds = _path.bounds; + CGSize size = bounds.size; + UIEdgeInsets insets = UIEdgeInsetsZero; + if (bounds.origin.x < 0) size.width += bounds.origin.x; + if (bounds.origin.x > 0) insets.left = bounds.origin.x; + if (bounds.origin.y < 0) size.height += bounds.origin.y; + if (bounds.origin.y > 0) insets.top = bounds.origin.y; + _size = size; + _insets = insets; + } + ); +} + +- (NSArray *)exclusionPaths { + Getter(NSArray *paths = _exclusionPaths) return paths; +} + +- (void)setExclusionPaths:(NSArray *)exclusionPaths { + Setter(_exclusionPaths = exclusionPaths.copy); +} + +- (BOOL)isPathFillEvenOdd { + Getter(BOOL is = _pathFillEvenOdd) return is; +} + +- (void)setPathFillEvenOdd:(BOOL)pathFillEvenOdd { + Setter(_pathFillEvenOdd = pathFillEvenOdd); +} + +- (CGFloat)pathLineWidth { + Getter(CGFloat width = _pathLineWidth) return width; +} + +- (void)setPathLineWidth:(CGFloat)pathLineWidth { + Setter(_pathLineWidth = pathLineWidth); +} + +- (BOOL)isVerticalForm { + Getter(BOOL v = _verticalForm) return v; +} + +- (void)setVerticalForm:(BOOL)verticalForm { + Setter(_verticalForm = verticalForm); +} + +- (NSUInteger)maximumNumberOfRows { + Getter(NSUInteger num = _maximumNumberOfRows) return num; +} + +- (void)setMaximumNumberOfRows:(NSUInteger)maximumNumberOfRows { + Setter(_maximumNumberOfRows = maximumNumberOfRows); +} + +- (ASTextTruncationType)truncationType { + Getter(ASTextTruncationType type = _truncationType) return type; +} + +- (void)setTruncationType:(ASTextTruncationType)truncationType { + Setter(_truncationType = truncationType); +} + +- (NSAttributedString *)truncationToken { + Getter(NSAttributedString *token = _truncationToken) return token; +} + +- (void)setTruncationToken:(NSAttributedString *)truncationToken { + Setter(_truncationToken = truncationToken.copy); +} + +- (void)setLinePositionModifier:(id)linePositionModifier { + Setter(_linePositionModifier = [(NSObject *)linePositionModifier copy]); +} + +- (id)linePositionModifier { + Getter(id m = _linePositionModifier) return m; +} + +#undef Getter +#undef Setter +@end + + + + +@interface ASTextLayout () + +@property (nonatomic) ASTextContainer *container; +@property (nonatomic) NSAttributedString *text; +@property (nonatomic) NSRange range; + +@property (nonatomic) CTFrameRef frame; +@property (nonatomic) NSArray *lines; +@property (nonatomic) ASTextLine *truncatedLine; +@property (nonatomic) NSArray *attachments; +@property (nonatomic) NSArray *attachmentRanges; +@property (nonatomic) NSArray *attachmentRects; +@property (nonatomic) NSSet *attachmentContentsSet; +@property (nonatomic) NSUInteger rowCount; +@property (nonatomic) NSRange visibleRange; +@property (nonatomic) CGRect textBoundingRect; +@property (nonatomic) CGSize textBoundingSize; + +@property (nonatomic) BOOL containsHighlight; +@property (nonatomic) BOOL needDrawBlockBorder; +@property (nonatomic) BOOL needDrawBackgroundBorder; +@property (nonatomic) BOOL needDrawShadow; +@property (nonatomic) BOOL needDrawUnderline; +@property (nonatomic) BOOL needDrawText; +@property (nonatomic) BOOL needDrawAttachment; +@property (nonatomic) BOOL needDrawInnerShadow; +@property (nonatomic) BOOL needDrawStrikethrough; +@property (nonatomic) BOOL needDrawBorder; + +@property (nonatomic) NSUInteger *lineRowsIndex; +@property (nonatomic) ASRowEdge *lineRowsEdge; ///< top-left origin + +@end + + + +@implementation ASTextLayout + +#pragma mark - Layout + +- (instancetype)_init { + self = [super init]; + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"lines: %ld, visibleRange:%@, textBoundingRect:%@", + [self.lines count], + NSStringFromRange(self.visibleRange), + NSStringFromCGRect(self.textBoundingRect)]; +} + ++ (ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text { + ASTextContainer *container = [ASTextContainer containerWithSize:size]; + return [self layoutWithContainer:container text:text]; +} + ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text { + return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)]; +} + ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { + ASTextLayout *layout = NULL; + CGPathRef cgPath = nil; + CGRect cgPathBox = {0}; + BOOL isVerticalForm = NO; + BOOL rowMaySeparated = NO; + NSMutableDictionary *frameAttrs = nil; + CTFramesetterRef ctSetter = NULL; + CTFrameRef ctFrame = NULL; + CFArrayRef ctLines = nil; + CGPoint *lineOrigins = NULL; + NSUInteger lineCount = 0; + NSMutableArray *lines = nil; + NSMutableArray *attachments = nil; + NSMutableArray *attachmentRanges = nil; + NSMutableArray *attachmentRects = nil; + NSMutableSet *attachmentContentsSet = nil; + BOOL needTruncation = NO; + NSAttributedString *truncationToken = nil; + ASTextLine *truncatedLine = nil; + ASRowEdge *lineRowsEdge = NULL; + NSUInteger *lineRowsIndex = NULL; + NSRange visibleRange; + NSUInteger maximumNumberOfRows = 0; + BOOL constraintSizeIsExtended = NO; + CGRect constraintRectBeforeExtended = {0}; +#define FAIL_AND_RETURN {\ + if (cgPath) CFRelease(cgPath); \ + if (ctSetter) CFRelease(ctSetter); \ + if (ctFrame) CFRelease(ctFrame); \ + if (lineOrigins) free(lineOrigins); \ + if (lineRowsEdge) free(lineRowsEdge); \ + if (lineRowsIndex) free(lineRowsIndex); \ + return nil; } + + container = [container copy]; + if (!text || !container) return nil; + if (range.location + range.length > text.length) return nil; + [container makeImmutable]; + maximumNumberOfRows = container.maximumNumberOfRows; + + // It may use larger constraint size when create CTFrame with + // CTFramesetterCreateFrame in iOS 10. + BOOL needFixLayoutSizeBug = AS_AT_LEAST_IOS10; + + layout = [[ASTextLayout alloc] _init]; + layout.text = text; + layout.container = container; + layout.range = range; + isVerticalForm = container.verticalForm; + + // set cgPath and cgPathBox + if (container.path == nil && container.exclusionPaths.count == 0) { + if (container.size.width <= 0 || container.size.height <= 0) FAIL_AND_RETURN + CGRect rect = (CGRect) {CGPointZero, container.size }; + if (needFixLayoutSizeBug) { + constraintSizeIsExtended = YES; + constraintRectBeforeExtended = UIEdgeInsetsInsetRect(rect, container.insets); + constraintRectBeforeExtended = CGRectStandardize(constraintRectBeforeExtended); + if (container.isVerticalForm) { + rect.size.width = ASTextContainerMaxSize.width; + } else { + rect.size.height = ASTextContainerMaxSize.height; + } + } + rect = UIEdgeInsetsInsetRect(rect, container.insets); + rect = CGRectStandardize(rect); + cgPathBox = rect; + rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1, -1)); + cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true + } else if (container.path && CGPathIsRect(container.path.CGPath, &cgPathBox) && container.exclusionPaths.count == 0) { + CGRect rect = CGRectApplyAffineTransform(cgPathBox, CGAffineTransformMakeScale(1, -1)); + cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true + } else { + rowMaySeparated = YES; + CGMutablePathRef path = NULL; + if (container.path) { + path = CGPathCreateMutableCopy(container.path.CGPath); + } else { + CGRect rect = (CGRect) {CGPointZero, container.size }; + rect = UIEdgeInsetsInsetRect(rect, container.insets); + CGPathRef rectPath = CGPathCreateWithRect(rect, NULL); + if (rectPath) { + path = CGPathCreateMutableCopy(rectPath); + CGPathRelease(rectPath); + } + } + if (path) { + [layout.container.exclusionPaths enumerateObjectsUsingBlock: ^(UIBezierPath *onePath, NSUInteger idx, BOOL *stop) { + CGPathAddPath(path, NULL, onePath.CGPath); + }]; + + cgPathBox = CGPathGetPathBoundingBox(path); + CGAffineTransform trans = CGAffineTransformMakeScale(1, -1); + CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans); + CGPathRelease(path); + path = transPath; + } + cgPath = path; + } + if (!cgPath) FAIL_AND_RETURN + + // frame setter config + frameAttrs = [[NSMutableDictionary alloc] init]; + if (container.isPathFillEvenOdd == NO) { + frameAttrs[(id)kCTFramePathFillRuleAttributeName] = @(kCTFramePathFillWindingNumber); + } + if (container.pathLineWidth > 0) { + frameAttrs[(id)kCTFramePathWidthAttributeName] = @(container.pathLineWidth); + } + if (container.isVerticalForm == YES) { + frameAttrs[(id)kCTFrameProgressionAttributeName] = @(kCTFrameProgressionRightToLeft); + } + + /* + * Framesetter cache. + * Framesetters can only be used by one thread at a time. + * Create a CFSet with no callbacks (raw pointers) to keep track of which + * framesetters are in use on other threads. If the one for our string is already in use, + * just create a new one. This should be pretty rare. + */ + static pthread_mutex_t busyFramesettersLock = PTHREAD_MUTEX_INITIALIZER; + static NSCache *framesetterCache; + static CFMutableSetRef busyFramesetters; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (ASActivateExperimentalFeature(ASExperimentalFramesetterCache)) { + framesetterCache = [[NSCache alloc] init]; + framesetterCache.name = @"org.TextureGroup.Texture.framesetterCache"; + busyFramesetters = CFSetCreateMutable(NULL, 0, NULL); + } + }); + + BOOL haveCached = NO, useCached = NO; + if (framesetterCache) { + // Check if there's one in the cache. + ctSetter = (__bridge_retained CTFramesetterRef)[framesetterCache objectForKey:text]; + + if (ctSetter) { + haveCached = YES; + + // Check-and-set busy on the cached one. + pthread_mutex_lock(&busyFramesettersLock); + BOOL busy = CFSetContainsValue(busyFramesetters, ctSetter); + if (!busy) { + CFSetAddValue(busyFramesetters, ctSetter); + useCached = YES; + } + pthread_mutex_unlock(&busyFramesettersLock); + + // Release if it was busy. + if (busy) { + CFRelease(ctSetter); + ctSetter = NULL; + } + } + } + + // Create a framesetter if needed. + if (!ctSetter) { + ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); + } + + if (!ctSetter) FAIL_AND_RETURN + ctFrame = CTFramesetterCreateFrame(ctSetter, ASTextCFRangeFromNSRange(range), cgPath, (CFDictionaryRef)frameAttrs); + + // Return to cache. + if (framesetterCache) { + if (useCached) { + // If reused: mark available. + pthread_mutex_lock(&busyFramesettersLock); + CFSetRemoveValue(busyFramesetters, ctSetter); + pthread_mutex_unlock(&busyFramesettersLock); + } else if (!haveCached) { + // If first framesetter, add to cache. + [framesetterCache setObject:(__bridge id)ctSetter forKey:text]; + } + } + + if (!ctFrame) FAIL_AND_RETURN + lines = [NSMutableArray new]; + ctLines = CTFrameGetLines(ctFrame); + lineCount = CFArrayGetCount(ctLines); + if (lineCount > 0) { + lineOrigins = (CGPoint *)malloc(lineCount * sizeof(CGPoint)); + if (lineOrigins == NULL) FAIL_AND_RETURN + CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, lineCount), lineOrigins); + } + + CGRect textBoundingRect = CGRectZero; + CGSize textBoundingSize = CGSizeZero; + NSInteger rowIdx = -1; + NSUInteger rowCount = 0; + CGRect lastRect = CGRectMake(0, -FLT_MAX, 0, 0); + CGPoint lastPosition = CGPointMake(0, -FLT_MAX); + if (isVerticalForm) { + lastRect = CGRectMake(FLT_MAX, 0, 0, 0); + lastPosition = CGPointMake(FLT_MAX, 0); + } + + // calculate line frame + NSUInteger lineCurrentIdx = 0; + BOOL measuringBeyondConstraints = NO; + for (NSUInteger i = 0; i < lineCount; i++) { + CTLineRef ctLine = (CTLineRef)CFArrayGetValueAtIndex(ctLines, i); + CFArrayRef ctRuns = CTLineGetGlyphRuns(ctLine); + if (!ctRuns || CFArrayGetCount(ctRuns) == 0) continue; + + // CoreText coordinate system + CGPoint ctLineOrigin = lineOrigins[i]; + + // UIKit coordinate system + CGPoint position; + position.x = cgPathBox.origin.x + ctLineOrigin.x; + position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y; + + ASTextLine *line = [ASTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm]; + + [lines addObject:line]; + } + + // Give user a chance to modify the line's position. + [container.linePositionModifier modifyLines:lines fromText:text inContainer:container]; + + BOOL first = YES; + for (ASTextLine *line in lines) { + CGPoint position = line.position; + CGRect rect = line.bounds; + if (constraintSizeIsExtended) { + if (isVerticalForm) { + if (rect.origin.x + rect.size.width > + constraintRectBeforeExtended.origin.x + + constraintRectBeforeExtended.size.width) { + measuringBeyondConstraints = YES; + } + } else { + if (rect.origin.y + rect.size.height > + constraintRectBeforeExtended.origin.y + + constraintRectBeforeExtended.size.height) { + measuringBeyondConstraints = YES; + } + } + } + + BOOL newRow = !measuringBeyondConstraints; + if (newRow && rowMaySeparated && position.x != lastPosition.x) { + if (isVerticalForm) { + if (rect.size.width > lastRect.size.width) { + if (rect.origin.x > lastPosition.x && lastPosition.x > rect.origin.x - rect.size.width) newRow = NO; + } else { + if (lastRect.origin.x > position.x && position.x > lastRect.origin.x - lastRect.size.width) newRow = NO; + } + } else { + if (rect.size.height > lastRect.size.height) { + if (rect.origin.y < lastPosition.y && lastPosition.y < rect.origin.y + rect.size.height) newRow = NO; + } else { + if (lastRect.origin.y < position.y && position.y < lastRect.origin.y + lastRect.size.height) newRow = NO; + } + } + } + + if (newRow) rowIdx++; + lastRect = rect; + lastPosition = position; + + line.index = lineCurrentIdx; + line.row = rowIdx; + + rowCount = rowIdx + 1; + lineCurrentIdx ++; + + if (first) { + first = NO; + textBoundingRect = rect; + } else if (!measuringBeyondConstraints) { + if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) { + textBoundingRect = CGRectUnion(textBoundingRect, rect); + } + } + } + + { + NSMutableArray *removedLines = [NSMutableArray new]; + if (rowCount > 0) { + if (maximumNumberOfRows > 0) { + if (rowCount > maximumNumberOfRows) { + needTruncation = YES; + rowCount = maximumNumberOfRows; + do { + ASTextLine *line = lines.lastObject; + if (!line) break; + if (line.row < rowCount) break; // we have removed down to an allowed # of lines now + [lines removeLastObject]; + [removedLines addObject:line]; + } while (1); + } + } + ASTextLine *lastLine = rowCount < lines.count ? lines[rowCount - 1] : lines.lastObject; + if (!needTruncation && lastLine.range.location + lastLine.range.length < text.length) { + needTruncation = YES; + while (lines.count > rowCount) { + ASTextLine *line = lines.lastObject; + [lines removeLastObject]; + [removedLines addObject:line]; + } + } + + lineRowsEdge = (ASRowEdge *) calloc(rowCount, sizeof(ASRowEdge)); + if (lineRowsEdge == NULL) FAIL_AND_RETURN + lineRowsIndex = (NSUInteger *) calloc(rowCount, sizeof(NSUInteger)); + if (lineRowsIndex == NULL) FAIL_AND_RETURN + NSInteger lastRowIdx = -1; + CGFloat lastHead = 0; + CGFloat lastFoot = 0; + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + CGRect rect = line.bounds; + if ((NSInteger) line.row != lastRowIdx) { + if (lastRowIdx >= 0) { + lineRowsEdge[lastRowIdx] = (ASRowEdge) {.head = lastHead, .foot = lastFoot}; + } + lastRowIdx = line.row; + lineRowsIndex[lastRowIdx] = i; + if (isVerticalForm) { + lastHead = rect.origin.x + rect.size.width; + lastFoot = lastHead - rect.size.width; + } else { + lastHead = rect.origin.y; + lastFoot = lastHead + rect.size.height; + } + } else { + if (isVerticalForm) { + lastHead = MAX(lastHead, rect.origin.x + rect.size.width); + lastFoot = MIN(lastFoot, rect.origin.x); + } else { + lastHead = MIN(lastHead, rect.origin.y); + lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height); + } + } + } + lineRowsEdge[lastRowIdx] = (ASRowEdge) {.head = lastHead, .foot = lastFoot}; + + for (NSUInteger i = 1; i < rowCount; i++) { + ASRowEdge v0 = lineRowsEdge[i - 1]; + ASRowEdge v1 = lineRowsEdge[i]; + lineRowsEdge[i - 1].foot = lineRowsEdge[i].head = (v0.foot + v1.head) * 0.5; + } + } + + { // calculate bounding size + CGRect rect = textBoundingRect; + if (container.path) { + if (container.pathLineWidth > 0) { + CGFloat inset = container.pathLineWidth / 2; + rect = CGRectInset(rect, -inset, -inset); + } + } else { + rect = UIEdgeInsetsInsetRect(rect, ASTextUIEdgeInsetsInvert(container.insets)); + } + rect = CGRectStandardize(rect); + CGSize size = rect.size; + if (container.verticalForm) { + size.width += container.size.width - (rect.origin.x + rect.size.width); + } else { + size.width += rect.origin.x; + } + size.height += rect.origin.y; + if (size.width < 0) size.width = 0; + if (size.height < 0) size.height = 0; + size.width = ceil(size.width); + size.height = ceil(size.height); + textBoundingSize = size; + } + + visibleRange = ASTextNSRangeFromCFRange(CTFrameGetVisibleStringRange(ctFrame)); + if (needTruncation) { + ASTextLine *lastLine = lines.lastObject; + NSRange lastRange = lastLine.range; + visibleRange.length = lastRange.location + lastRange.length - visibleRange.location; + + // create truncated line + if (container.truncationType != ASTextTruncationTypeNone) { + CTLineRef truncationTokenLine = NULL; + if (container.truncationToken) { + truncationToken = container.truncationToken; + truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationToken); + } else { + CFArrayRef runs = CTLineGetGlyphRuns(lastLine.CTLine); + NSUInteger runCount = CFArrayGetCount(runs); + NSMutableDictionary *attrs = nil; + if (runCount > 0) { + CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex(runs, runCount - 1); + attrs = (id) CTRunGetAttributes(run); + attrs = attrs ? attrs.mutableCopy : [NSMutableArray new]; + [attrs removeObjectsForKeys:[NSMutableAttributedString as_allDiscontinuousAttributeKeys]]; + CTFontRef font = (__bridge CTFontRef) attrs[(id) kCTFontAttributeName]; + CGFloat fontSize = font ? CTFontGetSize(font) : 12.0; + UIFont *uiFont = [UIFont systemFontOfSize:fontSize * 0.9]; + if (uiFont) { + font = CTFontCreateWithName((__bridge CFStringRef) uiFont.fontName, uiFont.pointSize, NULL); + } else { + font = NULL; + } + if (font) { + attrs[(id) kCTFontAttributeName] = (__bridge id) (font); + uiFont = nil; + CFRelease(font); + } + CGColorRef color = (__bridge CGColorRef) (attrs[(id) kCTForegroundColorAttributeName]); + if (color && CFGetTypeID(color) == CGColorGetTypeID() && CGColorGetAlpha(color) == 0) { + // ignore clear color + [attrs removeObjectForKey:(id) kCTForegroundColorAttributeName]; + } + if (!attrs) attrs = [NSMutableDictionary new]; + } + truncationToken = [[NSAttributedString alloc] initWithString:ASTextTruncationToken attributes:attrs]; + truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationToken); + } + if (truncationTokenLine) { + CTLineTruncationType type = kCTLineTruncationEnd; + if (container.truncationType == ASTextTruncationTypeStart) { + type = kCTLineTruncationStart; + } else if (container.truncationType == ASTextTruncationTypeMiddle) { + type = kCTLineTruncationMiddle; + } + NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy; + CGFloat truncatedWidth = lastLine.width; + CGFloat atLeastOneLine = lastLine.width; + CGRect cgPathRect = CGRectZero; + if (CGPathIsRect(cgPath, &cgPathRect)) { + if (isVerticalForm) { + truncatedWidth = cgPathRect.size.height; + } else { + truncatedWidth = cgPathRect.size.width; + } + } + int i = 0; + if (type != kCTLineTruncationStart) { // Middle or End/Tail wants to collect some text (at least one line's + // worth) preceding the truncated content, with which to construct a "truncated line". + i = (int)removedLines.count - 1; + while (atLeastOneLine < truncatedWidth && i >= 0) { + if (lastLineText.length > 0 && [lastLineText.string characterAtIndex:lastLineText.string.length - 1] == '\n') { // Explicit newlines are always "long enough". + [lastLineText deleteCharactersInRange:NSMakeRange(lastLineText.string.length - 1, 1)]; + break; + } + [lastLineText appendAttributedString:[text attributedSubstringFromRange:removedLines[i].range]]; + atLeastOneLine += removedLines[i--].width; + } + [lastLineText appendAttributedString:truncationToken]; + } + if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants to collect some + // text following the truncated content. + i = 0; + atLeastOneLine = removedLines[i].width; + while (atLeastOneLine < truncatedWidth && i < removedLines.count) { + atLeastOneLine += removedLines[i++].width; + } + for (i--; i >= 0; i--) { + NSAttributedString *nextLine = [text attributedSubstringFromRange:removedLines[i].range]; + if ([nextLine.string characterAtIndex:nextLine.string.length - 1] == '\n') { // Explicit newlines are always "long enough". + lastLineText = [NSMutableAttributedString new]; + } else { + [lastLineText appendAttributedString:nextLine]; + } + } + if (type == kCTLineTruncationStart) { + [lastLineText insertAttributedString:truncationToken atIndex:0]; + } + } + + CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef) lastLineText); + if (ctLastLineExtend) { + CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, type, truncationTokenLine); + CFRelease(ctLastLineExtend); + if (ctTruncatedLine) { + truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:isVerticalForm]; + truncatedLine.index = lastLine.index; + truncatedLine.row = lastLine.row; + CFRelease(ctTruncatedLine); + } + } + CFRelease(truncationTokenLine); + } + } + } + } + + if (isVerticalForm) { + NSCharacterSet *rotateCharset = ASTextVerticalFormRotateCharacterSet(); + NSCharacterSet *rotateMoveCharset = ASTextVerticalFormRotateAndMoveCharacterSet(); + + void (^lineBlock)(ASTextLine *) = ^(ASTextLine *line){ + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + if (!runs) return; + NSUInteger runCount = CFArrayGetCount(runs); + if (runCount == 0) return; + NSMutableArray *lineRunRanges = [NSMutableArray new]; + line.verticalRotateRange = lineRunRanges; + for (NSUInteger r = 0; r < runCount; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + NSMutableArray *runRanges = [NSMutableArray new]; + [lineRunRanges addObject:runRanges]; + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + CFIndex runStrIdx[glyphCount + 1]; + CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); + CFRange runStrRange = CTRunGetStringRange(run); + runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; + CFDictionaryRef runAttrs = CTRunGetAttributes(run); + CTFontRef font = (CTFontRef)CFDictionaryGetValue(runAttrs, kCTFontAttributeName); + BOOL isColorGlyph = ASTextCTFontContainsColorBitmapGlyphs(font); + + NSUInteger prevIdx = 0; + ASTextRunGlyphDrawMode prevMode = ASTextRunGlyphDrawModeHorizontal; + NSString *layoutStr = layout.text.string; + for (NSUInteger g = 0; g < glyphCount; g++) { + BOOL glyphRotate = 0, glyphRotateMove = NO; + CFIndex runStrLen = runStrIdx[g + 1] - runStrIdx[g]; + if (isColorGlyph) { + glyphRotate = YES; + } else if (runStrLen == 1) { + unichar c = [layoutStr characterAtIndex:runStrIdx[g]]; + glyphRotate = [rotateCharset characterIsMember:c]; + if (glyphRotate) glyphRotateMove = [rotateMoveCharset characterIsMember:c]; + } else if (runStrLen > 1){ + NSString *glyphStr = [layoutStr substringWithRange:NSMakeRange(runStrIdx[g], runStrLen)]; + BOOL glyphRotate = [glyphStr rangeOfCharacterFromSet:rotateCharset].location != NSNotFound; + if (glyphRotate) glyphRotateMove = [glyphStr rangeOfCharacterFromSet:rotateMoveCharset].location != NSNotFound; + } + + ASTextRunGlyphDrawMode mode = glyphRotateMove ? ASTextRunGlyphDrawModeVerticalRotateMove : (glyphRotate ? ASTextRunGlyphDrawModeVerticalRotate : ASTextRunGlyphDrawModeHorizontal); + if (g == 0) { + prevMode = mode; + } else if (mode != prevMode) { + ASTextRunGlyphRange *aRange = [ASTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, g - prevIdx) drawMode:prevMode]; + [runRanges addObject:aRange]; + prevIdx = g; + prevMode = mode; + } + } + if (prevIdx < glyphCount) { + ASTextRunGlyphRange *aRange = [ASTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, glyphCount - prevIdx) drawMode:prevMode]; + [runRanges addObject:aRange]; + } + + } + }; + for (ASTextLine *line in lines) { + lineBlock(line); + } + if (truncatedLine) lineBlock(truncatedLine); + } + + if (visibleRange.length > 0) { + layout.needDrawText = YES; + + void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) { + if (attrs[ASTextHighlightAttributeName]) layout.containsHighlight = YES; + if (attrs[ASTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES; + if (attrs[ASTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES; + if (attrs[ASTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES; + if (attrs[ASTextUnderlineAttributeName]) layout.needDrawUnderline = YES; + if (attrs[ASTextAttachmentAttributeName]) layout.needDrawAttachment = YES; + if (attrs[ASTextInnerShadowAttributeName]) layout.needDrawInnerShadow = YES; + if (attrs[ASTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES; + if (attrs[ASTextBorderAttributeName]) layout.needDrawBorder = YES; + }; + + [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; + if (truncatedLine) { + [truncationToken enumerateAttributesInRange:NSMakeRange(0, truncationToken.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; + } + } + + attachments = [NSMutableArray new]; + attachmentRanges = [NSMutableArray new]; + attachmentRects = [NSMutableArray new]; + attachmentContentsSet = [NSMutableSet new]; + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine; + if (line.attachments.count > 0) { + [attachments addObjectsFromArray:line.attachments]; + [attachmentRanges addObjectsFromArray:line.attachmentRanges]; + [attachmentRects addObjectsFromArray:line.attachmentRects]; + for (ASTextAttachment *attachment in line.attachments) { + if (attachment.content) { + [attachmentContentsSet addObject:attachment.content]; + } + } + } + } + if (attachments.count == 0) { + attachments = attachmentRanges = attachmentRects = nil; + } + + layout.frame = ctFrame; + layout.lines = lines; + layout.truncatedLine = truncatedLine; + layout.attachments = attachments; + layout.attachmentRanges = attachmentRanges; + layout.attachmentRects = attachmentRects; + layout.attachmentContentsSet = attachmentContentsSet; + layout.rowCount = rowCount; + layout.visibleRange = visibleRange; + layout.textBoundingRect = textBoundingRect; + layout.textBoundingSize = textBoundingSize; + layout.lineRowsEdge = lineRowsEdge; + layout.lineRowsIndex = lineRowsIndex; + CFRelease(cgPath); + CFRelease(ctSetter); + CFRelease(ctFrame); + if (lineOrigins) free(lineOrigins); + return layout; +} + ++ (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text { + return [self layoutWithContainers:containers text:text range:NSMakeRange(0, text.length)]; +} + ++ (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text range:(NSRange)range { + if (!containers || !text) return nil; + if (range.location + range.length > text.length) return nil; + NSMutableArray *layouts = [[NSMutableArray alloc] init]; + for (NSUInteger i = 0, max = containers.count; i < max; i++) { + ASTextContainer *container = containers[i]; + ASTextLayout *layout = [self layoutWithContainer:container text:text range:range]; + if (!layout) return nil; + NSInteger length = (NSInteger)range.length - (NSInteger)layout.visibleRange.length; + if (length <= 0) { + range.length = 0; + range.location = text.length; + } else { + range.length = length; + range.location += layout.visibleRange.length; + } + } + return layouts; +} + +- (void)setFrame:(CTFrameRef)frame { + if (_frame != frame) { + if (frame) CFRetain(frame); + if (_frame) CFRelease(_frame); + _frame = frame; + } +} + +- (void)dealloc { + if (_frame) CFRelease(_frame); + if (_lineRowsIndex) free(_lineRowsIndex); + if (_lineRowsEdge) free(_lineRowsEdge); +} + +#pragma mark - Copying + +- (id)copyWithZone:(NSZone *)zone { + return self; // readonly object +} + + +#pragma mark - Query + +/** + Get the row index with 'edge' distance. + + @param edge The distance from edge to the point. + If vertical form, the edge is left edge, otherwise the edge is top edge. + + @return Returns NSNotFound if there's no row at the point. + */ +- (NSUInteger)_rowIndexForEdge:(CGFloat)edge { + if (_rowCount == 0) return NSNotFound; + BOOL isVertical = _container.verticalForm; + NSUInteger lo = 0, hi = _rowCount - 1, mid = 0; + NSUInteger rowIdx = NSNotFound; + while (lo <= hi) { + mid = (lo + hi) / 2; + ASRowEdge oneEdge = _lineRowsEdge[mid]; + if (isVertical ? + (oneEdge.foot <= edge && edge <= oneEdge.head) : + (oneEdge.head <= edge && edge <= oneEdge.foot)) { + rowIdx = mid; + break; + } + if ((isVertical ? (edge > oneEdge.head) : (edge < oneEdge.head))) { + if (mid == 0) break; + hi = mid - 1; + } else { + lo = mid + 1; + } + } + return rowIdx; +} + +/** + Get the closest row index with 'edge' distance. + + @param edge The distance from edge to the point. + If vertical form, the edge is left edge, otherwise the edge is top edge. + + @return Returns NSNotFound if there's no line. + */ +- (NSUInteger)_closestRowIndexForEdge:(CGFloat)edge { + if (_rowCount == 0) return NSNotFound; + NSUInteger rowIdx = [self _rowIndexForEdge:edge]; + if (rowIdx == NSNotFound) { + if (_container.verticalForm) { + if (edge > _lineRowsEdge[0].head) { + rowIdx = 0; + } else if (edge < _lineRowsEdge[_rowCount - 1].foot) { + rowIdx = _rowCount - 1; + } + } else { + if (edge < _lineRowsEdge[0].head) { + rowIdx = 0; + } else if (edge > _lineRowsEdge[_rowCount - 1].foot) { + rowIdx = _rowCount - 1; + } + } + } + return rowIdx; +} + +/** + Get a CTRun from a line position. + + @param line The text line. + @param position The position in the whole text. + + @return Returns NULL if not found (no CTRun at the position). + */ +- (CTRunRef)_runForLine:(ASTextLine *)line position:(ASTextPosition *)position { + if (!line || !position) return NULL; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger i = 0, max = CFArrayGetCount(runs); i < max; i++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, i); + CFRange range = CTRunGetStringRange(run); + if (position.affinity == ASTextAffinityBackward) { + if (range.location < position.offset && position.offset <= range.location + range.length) { + return run; + } + } else { + if (range.location <= position.offset && position.offset < range.location + range.length) { + return run; + } + } + } + return NULL; +} + +/** + Whether the position is inside a composed character sequence. + + @param line The text line. + @param position Text text position in whole text. + @param block The block to be executed before returns YES. + left: left X offset + right: right X offset + prev: left position + next: right position + */ +- (BOOL)_insideComposedCharacterSequences:(ASTextLine *)line position:(NSUInteger)position block:(void (^)(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next))block { + NSRange range = line.range; + if (range.length == 0) return NO; + __block BOOL inside = NO; + __block NSUInteger _prev, _next; + [_text.string enumerateSubstringsInRange:range options:NSStringEnumerationByComposedCharacterSequences usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { + NSUInteger prev = substringRange.location; + NSUInteger next = substringRange.location + substringRange.length; + if (prev == position || next == position) { + *stop = YES; + } + if (prev < position && position < next) { + inside = YES; + _prev = prev; + _next = next; + *stop = YES; + } + }]; + if (inside && block) { + CGFloat left = [self offsetForTextPosition:_prev lineIndex:line.index]; + CGFloat right = [self offsetForTextPosition:_next lineIndex:line.index]; + block(left, right, _prev, _next); + } + return inside; +} + +/** + Whether the position is inside an emoji (such as National Flag Emoji). + + @param line The text line. + @param position Text text position in whole text. + @param block Yhe block to be executed before returns YES. + left: emoji's left X offset + right: emoji's right X offset + prev: emoji's left position + next: emoji's right position + */ +- (BOOL)_insideEmoji:(ASTextLine *)line position:(NSUInteger)position block:(void (^)(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next))block { + if (!line) return NO; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + CFRange range = CTRunGetStringRange(run); + if (range.length <= 1) continue; + if (position <= range.location || position >= range.location + range.length) continue; + CFDictionaryRef attrs = CTRunGetAttributes(run); + CTFontRef font = (CTFontRef)CFDictionaryGetValue(attrs, kCTFontAttributeName); + if (!ASTextCTFontContainsColorBitmapGlyphs(font)) continue; + + // Here's Emoji runs (larger than 1 unichar), and position is inside the range. + CFIndex indices[glyphCount]; + CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices); + for (NSUInteger g = 0; g < glyphCount; g++) { + CFIndex prev = indices[g]; + CFIndex next = g + 1 < glyphCount ? indices[g + 1] : range.location + range.length; + if (position == prev) break; // Emoji edge + if (prev < position && position < next) { // inside an emoji (such as National Flag Emoji) + CGPoint pos = CGPointZero; + CGSize adv = CGSizeZero; + CTRunGetPositions(run, CFRangeMake(g, 1), &pos); + CTRunGetAdvances(run, CFRangeMake(g, 1), &adv); + if (block) { + block(line.position.x + pos.x, + line.position.x + pos.x + adv.width, + prev, next); + } + return YES; + } + } + } + return NO; +} +/** + Whether the write direction is RTL at the specified point + + @param line The text line + @param point The point in layout. + + @return YES if RTL. + */ +- (BOOL)_isRightToLeftInLine:(ASTextLine *)line atPoint:(CGPoint)point { + if (!line) return NO; + // get write direction + BOOL RTL = NO; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, max = CFArrayGetCount(runs); r < max; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CGPoint glyphPosition; + CTRunGetPositions(run, CFRangeMake(0, 1), &glyphPosition); + if (_container.verticalForm) { + CGFloat runX = glyphPosition.x; + runX += line.position.y; + CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + if (runX <= point.y && point.y <= runX + runWidth) { + if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; + break; + } + } else { + CGFloat runX = glyphPosition.x; + runX += line.position.x; + CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + if (runX <= point.x && point.x <= runX + runWidth) { + if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; + break; + } + } + } + return RTL; +} + +/** + Correct the range's edge. + */ +- (ASTextRange *)_correctedRangeWithEdge:(ASTextRange *)range { + NSRange visibleRange = self.visibleRange; + ASTextPosition *start = range.start; + ASTextPosition *end = range.end; + + if (start.offset == visibleRange.location && start.affinity == ASTextAffinityBackward) { + start = [ASTextPosition positionWithOffset:start.offset affinity:ASTextAffinityForward]; + } + + if (end.offset == visibleRange.location + visibleRange.length && start.affinity == ASTextAffinityForward) { + end = [ASTextPosition positionWithOffset:end.offset affinity:ASTextAffinityBackward]; + } + + if (start != range.start || end != range.end) { + range = [ASTextRange rangeWithStart:start end:end]; + } + return range; +} + +- (NSUInteger)lineIndexForRow:(NSUInteger)row { + if (row >= _rowCount) return NSNotFound; + return _lineRowsIndex[row]; +} + +- (NSUInteger)lineCountForRow:(NSUInteger)row { + if (row >= _rowCount) return NSNotFound; + if (row == _rowCount - 1) { + return _lines.count - _lineRowsIndex[row]; + } else { + return _lineRowsIndex[row + 1] - _lineRowsIndex[row]; + } +} + +- (NSUInteger)rowIndexForLine:(NSUInteger)line { + if (line >= _lines.count) return NSNotFound; + return ((ASTextLine *)_lines[line]).row; +} + +- (NSUInteger)lineIndexForPoint:(CGPoint)point { + if (_lines.count == 0 || _rowCount == 0) return NSNotFound; + NSUInteger rowIdx = [self _rowIndexForEdge:_container.verticalForm ? point.x : point.y]; + if (rowIdx == NSNotFound) return NSNotFound; + + NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; + NSUInteger lineIdx1 = rowIdx == _rowCount - 1 ? _lines.count - 1 : _lineRowsIndex[rowIdx + 1] - 1; + for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) { + CGRect bounds = ((ASTextLine *)_lines[i]).bounds; + if (CGRectContainsPoint(bounds, point)) return i; + } + + return NSNotFound; +} + +- (NSUInteger)closestLineIndexForPoint:(CGPoint)point { + BOOL isVertical = _container.verticalForm; + if (_lines.count == 0 || _rowCount == 0) return NSNotFound; + NSUInteger rowIdx = [self _closestRowIndexForEdge:isVertical ? point.x : point.y]; + if (rowIdx == NSNotFound) return NSNotFound; + + NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; + NSUInteger lineIdx1 = rowIdx == _rowCount - 1 ? _lines.count - 1 : _lineRowsIndex[rowIdx + 1] - 1; + if (lineIdx0 == lineIdx1) return lineIdx0; + + CGFloat minDistance = CGFLOAT_MAX; + NSUInteger minIndex = lineIdx0; + for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) { + CGRect bounds = ((ASTextLine *)_lines[i]).bounds; + if (isVertical) { + if (bounds.origin.y <= point.y && point.y <= bounds.origin.y + bounds.size.height) return i; + CGFloat distance; + if (point.y < bounds.origin.y) { + distance = bounds.origin.y - point.y; + } else { + distance = point.y - (bounds.origin.y + bounds.size.height); + } + if (distance < minDistance) { + minDistance = distance; + minIndex = i; + } + } else { + if (bounds.origin.x <= point.x && point.x <= bounds.origin.x + bounds.size.width) return i; + CGFloat distance; + if (point.x < bounds.origin.x) { + distance = bounds.origin.x - point.x; + } else { + distance = point.x - (bounds.origin.x + bounds.size.width); + } + if (distance < minDistance) { + minDistance = distance; + minIndex = i; + } + } + } + return minIndex; +} + +- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex { + if (lineIndex >= _lines.count) return CGFLOAT_MAX; + ASTextLine *line = _lines[lineIndex]; + CFRange range = CTLineGetStringRange(line.CTLine); + if (position < range.location || position > range.location + range.length) return CGFLOAT_MAX; + + CGFloat offset = CTLineGetOffsetForStringIndex(line.CTLine, position, NULL); + return _container.verticalForm ? (offset + line.position.y) : (offset + line.position.x); +} + +- (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex { + if (lineIndex >= _lines.count) return NSNotFound; + ASTextLine *line = _lines[lineIndex]; + if (_container.verticalForm) { + point.x = point.y - line.position.y; + point.y = 0; + } else { + point.x -= line.position.x; + point.y = 0; + } + CFIndex idx = CTLineGetStringIndexForPosition(line.CTLine, point); + if (idx == kCFNotFound) return NSNotFound; + + /* + If the emoji contains one or more variant form (such as ☔️ "\u2614\uFE0F") + and the font size is smaller than 379/15, then each variant form ("\uFE0F") + will rendered as a single blank glyph behind the emoji glyph. Maybe it's a + bug in CoreText? Seems iOS8.3 fixes this problem. + + If the point hit the blank glyph, the CTLineGetStringIndexForPosition() + returns the position before the emoji glyph, but it should returns the + position after the emoji and variant form. + + Here's a workaround. + */ + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, max = CFArrayGetCount(runs); r < max; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CFRange range = CTRunGetStringRange(run); + if (range.location <= idx && idx < range.location + range.length) { + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) break; + CFDictionaryRef attrs = CTRunGetAttributes(run); + CTFontRef font = (CTFontRef)CFDictionaryGetValue(attrs, kCTFontAttributeName); + if (!ASTextCTFontContainsColorBitmapGlyphs(font)) break; + + CFIndex indices[glyphCount]; + CGPoint positions[glyphCount]; + CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices); + CTRunGetPositions(run, CFRangeMake(0, glyphCount), positions); + for (NSUInteger g = 0; g < glyphCount; g++) { + NSUInteger gIdx = indices[g]; + if (gIdx == idx && g + 1 < glyphCount) { + CGFloat right = positions[g + 1].x; + if (point.x < right) break; + NSUInteger next = indices[g + 1]; + do { + if (next == range.location + range.length) break; + unichar c = [_text.string characterAtIndex:next]; + if ((c == 0xFE0E || c == 0xFE0F)) { // unicode variant form for emoji style + next++; + } else break; + } + while (1); + if (next != indices[g + 1]) idx = next; + break; + } + } + break; + } + } + return idx; +} + +- (ASTextPosition *)closestPositionToPoint:(CGPoint)point { + BOOL isVertical = _container.verticalForm; + // When call CTLineGetStringIndexForPosition() on ligature such as 'fi', + // and the point `hit` the glyph's left edge, it may get the ligature inside offset. + // I don't know why, maybe it's a bug of CoreText. Try to avoid it. + if (isVertical) point.y += 0.00001234; + else point.x += 0.00001234; + + NSUInteger lineIndex = [self closestLineIndexForPoint:point]; + if (lineIndex == NSNotFound) return nil; + ASTextLine *line = _lines[lineIndex]; + __block NSUInteger position = [self textPositionForPoint:point lineIndex:lineIndex]; + if (position == NSNotFound) position = line.range.location; + if (position <= _visibleRange.location) { + return [ASTextPosition positionWithOffset:_visibleRange.location affinity:ASTextAffinityForward]; + } else if (position >= _visibleRange.location + _visibleRange.length) { + return [ASTextPosition positionWithOffset:_visibleRange.location + _visibleRange.length affinity:ASTextAffinityBackward]; + } + + ASTextAffinity finalAffinity = ASTextAffinityForward; + BOOL finalAffinityDetected = NO; + + // binding range + NSRange bindingRange; + ASTextBinding *binding = [_text attribute:ASTextBindingAttributeName atIndex:position longestEffectiveRange:&bindingRange inRange:NSMakeRange(0, _text.length)]; + if (binding && bindingRange.length > 0) { + NSUInteger headLineIdx = [self lineIndexForPosition:[ASTextPosition positionWithOffset:bindingRange.location]]; + NSUInteger tailLineIdx = [self lineIndexForPosition:[ASTextPosition positionWithOffset:bindingRange.location + bindingRange.length affinity:ASTextAffinityBackward]]; + if (headLineIdx == lineIndex && lineIndex == tailLineIdx) { // all in same line + CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex]; + CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex]; + if (left != CGFLOAT_MAX && right != CGFLOAT_MAX) { + if (_container.isVerticalForm) { + if (fabs(point.y - left) < fabs(point.y - right)) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + } else { + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + } + } else { + if (fabs(point.x - left) < fabs(point.x - right)) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + } else { + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + } + } + } else if (left != CGFLOAT_MAX) { + position = left; + finalAffinity = ASTextAffinityForward; + } else if (right != CGFLOAT_MAX) { + position = right; + finalAffinity = ASTextAffinityBackward; + } + finalAffinityDetected = YES; + } else if (headLineIdx == lineIndex) { + CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex]; + if (left != CGFLOAT_MAX) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + finalAffinityDetected = YES; + } + } else if (tailLineIdx == lineIndex) { + CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex]; + if (right != CGFLOAT_MAX) { + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + finalAffinityDetected = YES; + } + } else { + BOOL onLeft = NO, onRight = NO; + if (headLineIdx != NSNotFound && tailLineIdx != NSNotFound) { + if (abs((int)headLineIdx - (int)lineIndex) < abs((int)tailLineIdx - (int)lineIndex)) onLeft = YES; + else onRight = YES; + } else if (headLineIdx != NSNotFound) { + onLeft = YES; + } else if (tailLineIdx != NSNotFound) { + onRight = YES; + } + + if (onLeft) { + CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:headLineIdx]; + if (left != CGFLOAT_MAX) { + lineIndex = headLineIdx; + line = _lines[headLineIdx]; + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; + finalAffinityDetected = YES; + } + } else if (onRight) { + CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:tailLineIdx]; + if (right != CGFLOAT_MAX) { + lineIndex = tailLineIdx; + line = _lines[tailLineIdx]; + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; + finalAffinityDetected = YES; + } + } + } + } + + // empty line + if (line.range.length == 0) { + BOOL behind = (_lines.count > 1 && lineIndex == _lines.count - 1); //end line + return [ASTextPosition positionWithOffset:line.range.location affinity:behind ? ASTextAffinityBackward:ASTextAffinityForward]; + } + + // detect weather the line is a linebreak token + if (line.range.length <= 2) { + NSString *str = [_text.string substringWithRange:line.range]; + if (ASTextIsLinebreakString(str)) { // an empty line ("\r", "\n", "\r\n") + return [ASTextPosition positionWithOffset:line.range.location]; + } + } + + // above whole text frame + if (lineIndex == 0 && (isVertical ? (point.x > line.right) : (point.y < line.top))) { + position = 0; + finalAffinity = ASTextAffinityForward; + finalAffinityDetected = YES; + } + // below whole text frame + if (lineIndex == _lines.count - 1 && (isVertical ? (point.x < line.left) : (point.y > line.bottom))) { + position = line.range.location + line.range.length; + finalAffinity = ASTextAffinityBackward; + finalAffinityDetected = YES; + } + + // There must be at least one non-linebreak char, + // ignore the linebreak characters at line end if exists. + if (position >= line.range.location + line.range.length - 1) { + if (position > line.range.location) { + unichar c1 = [_text.string characterAtIndex:position - 1]; + if (ASTextIsLinebreakChar(c1)) { + position--; + if (position > line.range.location) { + unichar c0 = [_text.string characterAtIndex:position - 1]; + if (ASTextIsLinebreakChar(c0)) { + position--; + } + } + } + } + } + if (position == line.range.location) { + return [ASTextPosition positionWithOffset:position]; + } + if (position == line.range.location + line.range.length) { + return [ASTextPosition positionWithOffset:position affinity:ASTextAffinityBackward]; + } + + [self _insideComposedCharacterSequences:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { + if (isVertical) { + position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next); + } else { + position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); + } + }]; + + [self _insideEmoji:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { + if (isVertical) { + position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next); + } else { + position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); + } + }]; + + if (position < _visibleRange.location) position = _visibleRange.location; + else if (position > _visibleRange.location + _visibleRange.length) position = _visibleRange.location + _visibleRange.length; + + if (!finalAffinityDetected) { + CGFloat ofs = [self offsetForTextPosition:position lineIndex:lineIndex]; + if (ofs != CGFLOAT_MAX) { + BOOL RTL = [self _isRightToLeftInLine:line atPoint:point]; + if (position >= line.range.location + line.range.length) { + finalAffinity = RTL ? ASTextAffinityForward : ASTextAffinityBackward; + } else if (position <= line.range.location) { + finalAffinity = RTL ? ASTextAffinityBackward : ASTextAffinityForward; + } else { + finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? ASTextAffinityForward : ASTextAffinityBackward; + } + } + } + + return [ASTextPosition positionWithOffset:position affinity:finalAffinity]; +} + +- (ASTextPosition *)positionForPoint:(CGPoint)point + oldPosition:(ASTextPosition *)oldPosition + otherPosition:(ASTextPosition *)otherPosition { + if (!oldPosition || !otherPosition) { + return oldPosition; + } + ASTextPosition *newPos = [self closestPositionToPoint:point]; + if (!newPos) return oldPosition; + if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] && + newPos.offset != otherPosition.offset) { + return newPos; + } + NSUInteger lineIndex = [self lineIndexForPosition:otherPosition]; + if (lineIndex == NSNotFound) return oldPosition; + ASTextLine *line = _lines[lineIndex]; + ASRowEdge vertical = _lineRowsEdge[line.row]; + if (_container.verticalForm) { + point.x = (vertical.head + vertical.foot) * 0.5; + } else { + point.y = (vertical.head + vertical.foot) * 0.5; + } + newPos = [self closestPositionToPoint:point]; + if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] && + newPos.offset != otherPosition.offset) { + return newPos; + } + + if (_container.isVerticalForm) { + if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionUp offset:1]; + if (range) return range.start; + } else { // search forward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionDown offset:1]; + if (range) return range.end; + } + } else { + if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1]; + if (range) return range.start; + } else { // search forward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1]; + if (range) return range.end; + } + } + + return oldPosition; +} + +- (ASTextRange *)textRangeAtPoint:(CGPoint)point { + NSUInteger lineIndex = [self lineIndexForPoint:point]; + if (lineIndex == NSNotFound) return nil; + NSUInteger textPosition = [self textPositionForPoint:point lineIndex:[self lineIndexForPoint:point]]; + if (textPosition == NSNotFound) return nil; + ASTextPosition *pos = [self closestPositionToPoint:point]; + if (!pos) return nil; + + // get write direction + BOOL RTL = [self _isRightToLeftInLine:_lines[lineIndex] atPoint:point]; + CGRect rect = [self caretRectForPosition:pos]; + if (CGRectIsNull(rect)) return nil; + + if (_container.verticalForm) { + ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown offset:1]; + return range; + } else { + ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1]; + return range; + } +} + +- (ASTextRange *)closestTextRangeAtPoint:(CGPoint)point { + ASTextPosition *pos = [self closestPositionToPoint:point]; + if (!pos) return nil; + NSUInteger lineIndex = [self lineIndexForPosition:pos]; + if (lineIndex == NSNotFound) return nil; + ASTextLine *line = _lines[lineIndex]; + BOOL RTL = [self _isRightToLeftInLine:line atPoint:point]; + CGRect rect = [self caretRectForPosition:pos]; + if (CGRectIsNull(rect)) return nil; + + UITextLayoutDirection direction = UITextLayoutDirectionRight; + if (pos.offset >= line.range.location + line.range.length) { + if (direction != RTL) { + direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + } else { + direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + } + } else if (pos.offset <= line.range.location) { + if (direction != RTL) { + direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + } else { + direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + } + } else { + if (_container.verticalForm) { + direction = (rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown; + } else { + direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight; + } + } + + ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:direction offset:1]; + return range; +} + +- (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position { + NSUInteger visibleStart = _visibleRange.location; + NSUInteger visibleEnd = _visibleRange.location + _visibleRange.length; + + if (!position) return nil; + if (position.offset < visibleStart || position.offset > visibleEnd) return nil; + + // head or tail, returns immediately + if (position.offset == visibleStart) { + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0)]; + } else if (position.offset == visibleEnd) { + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:ASTextAffinityBackward]; + } + + // binding range + NSRange tRange; + ASTextBinding *binding = [_text attribute:ASTextBindingAttributeName atIndex:position.offset longestEffectiveRange:&tRange inRange:_visibleRange]; + if (binding && tRange.length > 0 && tRange.location < position.offset) { + return [ASTextRange rangeWithRange:tRange]; + } + + // inside emoji or composed character sequences + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex != NSNotFound) { + __block NSUInteger _prev, _next; + BOOL emoji = NO, seq = NO; + + ASTextLine *line = _lines[lineIndex]; + emoji = [self _insideEmoji:line position:position.offset block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { + _prev = prev; + _next = next; + }]; + if (!emoji) { + seq = [self _insideComposedCharacterSequences:line position:position.offset block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { + _prev = prev; + _next = next; + }]; + } + if (emoji || seq) { + return [ASTextRange rangeWithRange:NSMakeRange(_prev, _next - _prev)]; + } + } + + // inside linebreak '\r\n' + if (position.offset > visibleStart && position.offset < visibleEnd) { + unichar c0 = [_text.string characterAtIndex:position.offset - 1]; + if ((c0 == '\r') && position.offset < visibleEnd) { + unichar c1 = [_text.string characterAtIndex:position.offset]; + if (c1 == '\n') { + return [ASTextRange rangeWithStart:[ASTextPosition positionWithOffset:position.offset - 1] end:[ASTextPosition positionWithOffset:position.offset + 1]]; + } + } + if (ASTextIsLinebreakChar(c0) && position.affinity == ASTextAffinityBackward) { + NSString *str = [_text.string substringToIndex:position.offset]; + NSUInteger len = ASTextLinebreakTailLength(str); + return [ASTextRange rangeWithStart:[ASTextPosition positionWithOffset:position.offset - len] end:[ASTextPosition positionWithOffset:position.offset]]; + } + } + + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:position.affinity]; +} + +- (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position + inDirection:(UITextLayoutDirection)direction + offset:(NSInteger)offset { + NSInteger visibleStart = _visibleRange.location; + NSInteger visibleEnd = _visibleRange.location + _visibleRange.length; + + if (!position) return nil; + if (position.offset < visibleStart || position.offset > visibleEnd) return nil; + if (offset == 0) return [self textRangeByExtendingPosition:position]; + + BOOL isVerticalForm = _container.verticalForm; + BOOL verticalMove, forwardMove; + + if (isVerticalForm) { + verticalMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionRight; + forwardMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown; + } else { + verticalMove = direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown; + forwardMove = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight; + } + + if (offset < 0) { + forwardMove = !forwardMove; + offset = -offset; + } + + // head or tail, returns immediately + if (!forwardMove && position.offset == visibleStart) { + return [ASTextRange rangeWithRange:NSMakeRange(_visibleRange.location, 0)]; + } else if (forwardMove && position.offset == visibleEnd) { + return [ASTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:ASTextAffinityBackward]; + } + + // extend from position + ASTextRange *fromRange = [self textRangeByExtendingPosition:position]; + if (!fromRange) return nil; + ASTextRange *allForward = [ASTextRange rangeWithStart:fromRange.start end:[ASTextPosition positionWithOffset:visibleEnd]]; + ASTextRange *allBackward = [ASTextRange rangeWithStart:[ASTextPosition positionWithOffset:visibleStart] end:fromRange.end]; + + if (verticalMove) { // up/down in text layout + NSInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return nil; + + ASTextLine *line = _lines[lineIndex]; + NSInteger moveToRowIndex = (NSInteger)line.row + (forwardMove ? offset : -offset); + if (moveToRowIndex < 0) return allBackward; + else if (moveToRowIndex >= (NSInteger)_rowCount) return allForward; + + CGFloat ofs = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; + if (ofs == CGFLOAT_MAX) return nil; + + NSUInteger moveToLineFirstIndex = [self lineIndexForRow:moveToRowIndex]; + NSUInteger moveToLineCount = [self lineCountForRow:moveToRowIndex]; + if (moveToLineFirstIndex == NSNotFound || moveToLineCount == NSNotFound || moveToLineCount == 0) return nil; + CGFloat mostLeft = CGFLOAT_MAX, mostRight = -CGFLOAT_MAX; + ASTextLine *mostLeftLine = nil, *mostRightLine = nil; + NSUInteger insideIndex = NSNotFound; + for (NSUInteger i = 0; i < moveToLineCount; i++) { + NSUInteger lineIndex = moveToLineFirstIndex + i; + ASTextLine *line = _lines[lineIndex]; + if (isVerticalForm) { + if (line.top <= ofs && ofs <= line.bottom) { + insideIndex = line.index; + break; + } + if (line.top < mostLeft) { + mostLeft = line.top; + mostLeftLine = line; + } + if (line.bottom > mostRight) { + mostRight = line.bottom; + mostRightLine = line; + } + } else { + if (line.left <= ofs && ofs <= line.right) { + insideIndex = line.index; + break; + } + if (line.left < mostLeft) { + mostLeft = line.left; + mostLeftLine = line; + } + if (line.right > mostRight) { + mostRight = line.right; + mostRightLine = line; + } + } + } + BOOL afinityEdge = NO; + if (insideIndex == NSNotFound) { + if (ofs <= mostLeft) { + insideIndex = mostLeftLine.index; + } else { + insideIndex = mostRightLine.index; + } + afinityEdge = YES; + } + ASTextLine *insideLine = _lines[insideIndex]; + NSUInteger pos; + if (isVerticalForm) { + pos = [self textPositionForPoint:CGPointMake(insideLine.position.x, ofs) lineIndex:insideIndex]; + } else { + pos = [self textPositionForPoint:CGPointMake(ofs, insideLine.position.y) lineIndex:insideIndex]; + } + if (pos == NSNotFound) return nil; + ASTextPosition *extPos; + if (afinityEdge) { + if (pos == insideLine.range.location + insideLine.range.length) { + NSString *subStr = [_text.string substringWithRange:insideLine.range]; + NSUInteger lineBreakLen = ASTextLinebreakTailLength(subStr); + extPos = [ASTextPosition positionWithOffset:pos - lineBreakLen]; + } else { + extPos = [ASTextPosition positionWithOffset:pos]; + } + } else { + extPos = [ASTextPosition positionWithOffset:pos]; + } + ASTextRange *ext = [self textRangeByExtendingPosition:extPos]; + if (!ext) return nil; + if (forwardMove) { + return [ASTextRange rangeWithStart:fromRange.start end:ext.end]; + } else { + return [ASTextRange rangeWithStart:ext.start end:fromRange.end]; + } + + } else { // left/right in text layout + ASTextPosition *toPosition = [ASTextPosition positionWithOffset:position.offset + (forwardMove ? offset : -offset)]; + if (toPosition.offset <= visibleStart) return allBackward; + else if (toPosition.offset >= visibleEnd) return allForward; + + ASTextRange *toRange = [self textRangeByExtendingPosition:toPosition]; + if (!toRange) return nil; + + NSInteger start = MIN(fromRange.start.offset, toRange.start.offset); + NSInteger end = MAX(fromRange.end.offset, toRange.end.offset); + return [ASTextRange rangeWithRange:NSMakeRange(start, end - start)]; + } +} + +- (NSUInteger)lineIndexForPosition:(ASTextPosition *)position { + if (!position) return NSNotFound; + if (_lines.count == 0) return NSNotFound; + NSUInteger location = position.offset; + NSInteger lo = 0, hi = _lines.count - 1, mid = 0; + if (position.affinity == ASTextAffinityBackward) { + while (lo <= hi) { + mid = (lo + hi) / 2; + ASTextLine *line = _lines[mid]; + NSRange range = line.range; + if (range.location < location && location <= range.location + range.length) { + return mid; + } + if (location <= range.location) { + hi = mid - 1; + } else { + lo = mid + 1; + } + } + } else { + while (lo <= hi) { + mid = (lo + hi) / 2; + ASTextLine *line = _lines[mid]; + NSRange range = line.range; + if (range.location <= location && location < range.location + range.length) { + return mid; + } + if (location < range.location) { + hi = mid - 1; + } else { + lo = mid + 1; + } + } + } + return NSNotFound; +} + +- (CGPoint)linePositionForPosition:(ASTextPosition *)position { + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return CGPointZero; + ASTextLine *line = _lines[lineIndex]; + CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; + if (offset == CGFLOAT_MAX) return CGPointZero; + if (_container.verticalForm) { + return CGPointMake(line.position.x, offset); + } else { + return CGPointMake(offset, line.position.y); + } +} + +- (CGRect)caretRectForPosition:(ASTextPosition *)position { + NSUInteger lineIndex = [self lineIndexForPosition:position]; + if (lineIndex == NSNotFound) return CGRectNull; + ASTextLine *line = _lines[lineIndex]; + CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; + if (offset == CGFLOAT_MAX) return CGRectNull; + if (_container.verticalForm) { + return CGRectMake(line.bounds.origin.x, offset, line.bounds.size.width, 0); + } else { + return CGRectMake(offset, line.bounds.origin.y, 0, line.bounds.size.height); + } +} + +- (CGRect)firstRectForRange:(ASTextRange *)range { + range = [self _correctedRangeWithEdge:range]; + + NSUInteger startLineIndex = [self lineIndexForPosition:range.start]; + NSUInteger endLineIndex = [self lineIndexForPosition:range.end]; + if (startLineIndex == NSNotFound || endLineIndex == NSNotFound) return CGRectNull; + if (startLineIndex > endLineIndex) return CGRectNull; + ASTextLine *startLine = _lines[startLineIndex]; + ASTextLine *endLine = _lines[endLineIndex]; + NSMutableArray *lines = [NSMutableArray new]; + for (NSUInteger i = startLineIndex; i <= startLineIndex; i++) { + ASTextLine *line = _lines[i]; + if (line.row != startLine.row) break; + [lines addObject:line]; + } + if (_container.verticalForm) { + if (lines.count == 1) { + CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat bottom; + if (startLine == endLine) { + bottom = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; + } else { + bottom = startLine.bottom; + } + if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull; + if (top > bottom) ASTEXT_SWAP(top, bottom); + return CGRectMake(startLine.left, top, startLine.width, bottom - top); + } else { + CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat bottom = startLine.bottom; + if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull; + if (top > bottom) ASTEXT_SWAP(top, bottom); + CGRect rect = CGRectMake(startLine.left, top, startLine.width, bottom - top); + for (NSUInteger i = 1; i < lines.count; i++) { + ASTextLine *line = lines[i]; + rect = CGRectUnion(rect, line.bounds); + } + return rect; + } + } else { + if (lines.count == 1) { + CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat right; + if (startLine == endLine) { + right = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; + } else { + right = startLine.right; + } + if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; + if (left > right) ASTEXT_SWAP(left, right); + return CGRectMake(left, startLine.top, right - left, startLine.height); + } else { + CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat right = startLine.right; + if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; + if (left > right) ASTEXT_SWAP(left, right); + CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height); + for (NSUInteger i = 1; i < lines.count; i++) { + ASTextLine *line = lines[i]; + rect = CGRectUnion(rect, line.bounds); + } + return rect; + } + } +} + +- (CGRect)rectForRange:(ASTextRange *)range { + NSArray *rects = [self selectionRectsForRange:range]; + if (rects.count == 0) return CGRectNull; + CGRect rectUnion = ((ASTextSelectionRect *)rects.firstObject).rect; + for (NSUInteger i = 1; i < rects.count; i++) { + ASTextSelectionRect *rect = rects[i]; + rectUnion = CGRectUnion(rectUnion, rect.rect); + } + return rectUnion; +} + +- (NSArray *)selectionRectsForRange:(ASTextRange *)range { + range = [self _correctedRangeWithEdge:range]; + + BOOL isVertical = _container.verticalForm; + NSMutableArray *rects = [[NSMutableArray alloc] init]; + if (!range) return rects; + + NSUInteger startLineIndex = [self lineIndexForPosition:range.start]; + NSUInteger endLineIndex = [self lineIndexForPosition:range.end]; + if (startLineIndex == NSNotFound || endLineIndex == NSNotFound) return rects; + if (startLineIndex > endLineIndex) ASTEXT_SWAP(startLineIndex, endLineIndex); + ASTextLine *startLine = _lines[startLineIndex]; + ASTextLine *endLine = _lines[endLineIndex]; + CGFloat offsetStart = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat offsetEnd = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; + + ASTextSelectionRect *start = [ASTextSelectionRect new]; + if (isVertical) { + start.rect = CGRectMake(startLine.left, offsetStart, startLine.width, 0); + } else { + start.rect = CGRectMake(offsetStart, startLine.top, 0, startLine.height); + } + start.containsStart = YES; + start.isVertical = isVertical; + [rects addObject:start]; + + ASTextSelectionRect *end = [ASTextSelectionRect new]; + if (isVertical) { + end.rect = CGRectMake(endLine.left, offsetEnd, endLine.width, 0); + } else { + end.rect = CGRectMake(offsetEnd, endLine.top, 0, endLine.height); + } + end.containsEnd = YES; + end.isVertical = isVertical; + [rects addObject:end]; + + if (startLine.row == endLine.row) { // same row + if (offsetStart > offsetEnd) ASTEXT_SWAP(offsetStart, offsetEnd); + ASTextSelectionRect *rect = [ASTextSelectionRect new]; + if (isVertical) { + rect.rect = CGRectMake(startLine.bounds.origin.x, offsetStart, MAX(startLine.width, endLine.width), offsetEnd - offsetStart); + } else { + rect.rect = CGRectMake(offsetStart, startLine.bounds.origin.y, offsetEnd - offsetStart, MAX(startLine.height, endLine.height)); + } + rect.isVertical = isVertical; + [rects addObject:rect]; + + } else { // more than one row + + // start line select rect + ASTextSelectionRect *topRect = [ASTextSelectionRect new]; + topRect.isVertical = isVertical; + CGFloat topOffset = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CTRunRef topRun = [self _runForLine:startLine position:range.start]; + if (topRun && (CTRunGetStatus(topRun) & kCTRunStatusRightToLeft)) { + if (isVertical) { + topRect.rect = CGRectMake(startLine.left, _container.path ? startLine.top : _container.insets.top, startLine.width, topOffset - startLine.top); + } else { + topRect.rect = CGRectMake(_container.path ? startLine.left : _container.insets.left, startLine.top, topOffset - startLine.left, startLine.height); + } + topRect.writingDirection = UITextWritingDirectionRightToLeft; + } else { + if (isVertical) { + topRect.rect = CGRectMake(startLine.left, topOffset, startLine.width, (_container.path ? startLine.bottom : _container.size.height - _container.insets.bottom) - topOffset); + } else { + topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset, startLine.height); + } + } + [rects addObject:topRect]; + + // end line select rect + ASTextSelectionRect *bottomRect = [ASTextSelectionRect new]; + bottomRect.isVertical = isVertical; + CGFloat bottomOffset = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; + CTRunRef bottomRun = [self _runForLine:endLine position:range.end]; + if (bottomRun && (CTRunGetStatus(bottomRun) & kCTRunStatusRightToLeft)) { + if (isVertical) { + bottomRect.rect = CGRectMake(endLine.left, bottomOffset, endLine.width, (_container.path ? endLine.bottom : _container.size.height - _container.insets.bottom) - bottomOffset); + } else { + bottomRect.rect = CGRectMake(bottomOffset, endLine.top, (_container.path ? endLine.right : _container.size.width - _container.insets.right) - bottomOffset, endLine.height); + } + bottomRect.writingDirection = UITextWritingDirectionRightToLeft; + } else { + if (isVertical) { + CGFloat top = _container.path ? endLine.top : _container.insets.top; + bottomRect.rect = CGRectMake(endLine.left, top, endLine.width, bottomOffset - top); + } else { + CGFloat left = _container.path ? endLine.left : _container.insets.left; + bottomRect.rect = CGRectMake(left, endLine.top, bottomOffset - left, endLine.height); + } + } + [rects addObject:bottomRect]; + + if (endLineIndex - startLineIndex >= 2) { + CGRect r = CGRectZero; + BOOL startLineDetected = NO; + for (NSUInteger l = startLineIndex + 1; l < endLineIndex; l++) { + ASTextLine *line = _lines[l]; + if (line.row == startLine.row || line.row == endLine.row) continue; + if (!startLineDetected) { + r = line.bounds; + startLineDetected = YES; + } else { + r = CGRectUnion(r, line.bounds); + } + } + if (startLineDetected) { + if (isVertical) { + if (!_container.path) { + r.origin.y = _container.insets.top; + r.size.height = _container.size.height - _container.insets.bottom - _container.insets.top; + } + r.size.width = CGRectGetMinX(topRect.rect) - CGRectGetMaxX(bottomRect.rect); + r.origin.x = CGRectGetMaxX(bottomRect.rect); + } else { + if (!_container.path) { + r.origin.x = _container.insets.left; + r.size.width = _container.size.width - _container.insets.right - _container.insets.left; + } + r.origin.y = CGRectGetMaxY(topRect.rect); + r.size.height = bottomRect.rect.origin.y - r.origin.y; + } + + ASTextSelectionRect *rect = [ASTextSelectionRect new]; + rect.rect = r; + rect.isVertical = isVertical; + [rects addObject:rect]; + } + } else { + if (isVertical) { + CGRect r0 = bottomRect.rect; + CGRect r1 = topRect.rect; + CGFloat mid = (CGRectGetMaxX(r0) + CGRectGetMinX(r1)) * 0.5; + r0.size.width = mid - r0.origin.x; + CGFloat r1ofs = r1.origin.x - mid; + r1.origin.x -= r1ofs; + r1.size.width += r1ofs; + topRect.rect = r1; + bottomRect.rect = r0; + } else { + CGRect r0 = topRect.rect; + CGRect r1 = bottomRect.rect; + CGFloat mid = (CGRectGetMaxY(r0) + CGRectGetMinY(r1)) * 0.5; + r0.size.height = mid - r0.origin.y; + CGFloat r1ofs = r1.origin.y - mid; + r1.origin.y -= r1ofs; + r1.size.height += r1ofs; + topRect.rect = r0; + bottomRect.rect = r1; + } + } + } + return rects; +} + +- (NSArray *)selectionRectsWithoutStartAndEndForRange:(ASTextRange *)range { + NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + for (NSInteger i = 0, max = rects.count; i < max; i++) { + ASTextSelectionRect *rect = rects[i]; + if (rect.containsStart || rect.containsEnd) { + [rects removeObjectAtIndex:i]; + i--; + max--; + } + } + return rects; +} + +- (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(ASTextRange *)range { + NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + for (NSInteger i = 0, max = rects.count; i < max; i++) { + ASTextSelectionRect *rect = rects[i]; + if (!rect.containsStart && !rect.containsEnd) { + [rects removeObjectAtIndex:i]; + i--; + max--; + } + } + return rects; +} + + +#pragma mark - Draw + + +typedef NS_OPTIONS(NSUInteger, ASTextDecorationType) { + ASTextDecorationTypeUnderline = 1 << 0, + ASTextDecorationTypeStrikethrough = 1 << 1, +}; + +typedef NS_OPTIONS(NSUInteger, ASTextBorderType) { + ASTextBorderTypeBackgound = 1 << 0, + ASTextBorderTypeNormal = 1 << 1, +}; + +static CGRect ASTextMergeRectInSameLine(CGRect rect1, CGRect rect2, BOOL isVertical) { + if (isVertical) { + CGFloat top = MIN(rect1.origin.y, rect2.origin.y); + CGFloat bottom = MAX(rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height); + CGFloat width = MAX(rect1.size.width, rect2.size.width); + return CGRectMake(rect1.origin.x, top, width, bottom - top); + } else { + CGFloat left = MIN(rect1.origin.x, rect2.origin.x); + CGFloat right = MAX(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); + CGFloat height = MAX(rect1.size.height, rect2.size.height); + return CGRectMake(left, rect1.origin.y, right - left, height); + } +} + +static void ASTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *underlinePosition, CGFloat *lineThickness) { + CGFloat maxXHeight = 0; + CGFloat maxUnderlinePos = 0; + CGFloat maxLineThickness = 0; + for (NSUInteger i = 0, max = CFArrayGetCount(runs); i < max; i++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, i); + CFDictionaryRef attrs = CTRunGetAttributes(run); + if (attrs) { + CTFontRef font = (CTFontRef)CFDictionaryGetValue(attrs, kCTFontAttributeName); + if (font) { + CGFloat xHeight = CTFontGetXHeight(font); + if (xHeight > maxXHeight) maxXHeight = xHeight; + CGFloat underlinePos = CTFontGetUnderlinePosition(font); + if (underlinePos < maxUnderlinePos) maxUnderlinePos = underlinePos; + CGFloat lineThickness = CTFontGetUnderlineThickness(font); + if (lineThickness > maxLineThickness) maxLineThickness = lineThickness; + } + } + } + if (xHeight) *xHeight = maxXHeight; + if (underlinePosition) *underlinePosition = maxUnderlinePos; + if (lineThickness) *lineThickness = maxLineThickness; +} + +static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGSize size, BOOL isVertical, NSArray *runRanges, CGFloat verticalOffset) { + CGAffineTransform runTextMatrix = CTRunGetTextMatrix(run); + BOOL runTextMatrixIsID = CGAffineTransformIsIdentity(runTextMatrix); + + CFDictionaryRef runAttrs = CTRunGetAttributes(run); + NSValue *glyphTransformValue = (NSValue *)CFDictionaryGetValue(runAttrs, (__bridge const void *)(ASTextGlyphTransformAttributeName)); + if (!isVertical && !glyphTransformValue) { // draw run + if (!runTextMatrixIsID) { + CGContextSaveGState(context); + CGAffineTransform trans = CGContextGetTextMatrix(context); + CGContextSetTextMatrix(context, CGAffineTransformConcat(trans, runTextMatrix)); + } + CTRunDraw(run, context, CFRangeMake(0, 0)); + if (!runTextMatrixIsID) { + CGContextRestoreGState(context); + } + } else { // draw glyph + CTFontRef runFont = (CTFontRef)CFDictionaryGetValue(runAttrs, kCTFontAttributeName); + if (!runFont) return; + NSUInteger glyphCount = CTRunGetGlyphCount(run); + if (glyphCount <= 0) return; + + CGGlyph glyphs[glyphCount]; + CGPoint glyphPositions[glyphCount]; + CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); + CTRunGetPositions(run, CFRangeMake(0, 0), glyphPositions); + + CGColorRef fillColor = (CGColorRef)CFDictionaryGetValue(runAttrs, kCTForegroundColorAttributeName); + fillColor = ASTextGetCGColor(fillColor); + NSNumber *strokeWidth = (NSNumber *)CFDictionaryGetValue(runAttrs, kCTStrokeWidthAttributeName); + + CGContextSaveGState(context); { + CGContextSetFillColorWithColor(context, fillColor); + if (!strokeWidth || strokeWidth.floatValue == 0) { + CGContextSetTextDrawingMode(context, kCGTextFill); + } else { + CGColorRef strokeColor = (CGColorRef)CFDictionaryGetValue(runAttrs, kCTStrokeColorAttributeName); + if (!strokeColor) strokeColor = fillColor; + CGContextSetStrokeColorWithColor(context, strokeColor); + CGContextSetLineWidth(context, CTFontGetSize(runFont) * fabs(strokeWidth.floatValue * 0.01)); + if (strokeWidth.floatValue > 0) { + CGContextSetTextDrawingMode(context, kCGTextStroke); + } else { + CGContextSetTextDrawingMode(context, kCGTextFillStroke); + } + } + + if (isVertical) { + CFIndex runStrIdx[glyphCount + 1]; + CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); + CFRange runStrRange = CTRunGetStringRange(run); + runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; + CGSize glyphAdvances[glyphCount]; + CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances); + CGFloat ascent = CTFontGetAscent(runFont); + CGFloat descent = CTFontGetDescent(runFont); + CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue; + CGPoint zeroPoint = CGPointZero; + + for (ASTextRunGlyphRange *oneRange in runRanges) { + NSRange range = oneRange.glyphRangeInRun; + NSUInteger rangeMax = range.location + range.length; + ASTextRunGlyphDrawMode mode = oneRange.drawMode; + + for (NSUInteger g = range.location; g < rangeMax; g++) { + CGContextSaveGState(context); { + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + if (glyphTransformValue) { + CGContextSetTextMatrix(context, glyphTransform); + } + if (mode) { // CJK glyph, need rotated + CGFloat ofs = (ascent - descent) * 0.5; + CGFloat w = glyphAdvances[g].width * 0.5; + CGFloat x = x = line.position.x + verticalOffset + glyphPositions[g].y + (ofs - w); + CGFloat y = -line.position.y + size.height - glyphPositions[g].x - (ofs + w); + if (mode == ASTextRunGlyphDrawModeVerticalRotateMove) { + x += w; + y += w; + } + CGContextSetTextPosition(context, x, y); + } else { + CGContextRotateCTM(context, -M_PI_2); + CGContextSetTextPosition(context, + line.position.y - size.height + glyphPositions[g].x, + line.position.x + verticalOffset + glyphPositions[g].y); + } + + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); + } else { + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); + CGFontRelease(cgFont); + } + } CGContextRestoreGState(context); + } + } + } else { // not vertical + if (glyphTransformValue) { + CFIndex runStrIdx[glyphCount + 1]; + CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); + CFRange runStrRange = CTRunGetStringRange(run); + runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; + CGSize glyphAdvances[glyphCount]; + CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances); + CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue; + CGPoint zeroPoint = CGPointZero; + + for (NSUInteger g = 0; g < glyphCount; g++) { + CGContextSaveGState(context); { + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextMatrix(context, glyphTransform); + CGContextSetTextPosition(context, + line.position.x + glyphPositions[g].x, + size.height - (line.position.y + glyphPositions[g].y)); + + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); + } else { + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); + CGFontRelease(cgFont); + } + } CGContextRestoreGState(context); + } + } else { + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context); + } else { + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount); + CGFontRelease(cgFont); + } + } + } + + } CGContextRestoreGState(context); + } +} + +static void ASTextSetLinePatternInContext(ASTextLineStyle style, CGFloat width, CGFloat phase, CGContextRef context){ + CGContextSetLineWidth(context, width); + CGContextSetLineCap(context, kCGLineCapButt); + CGContextSetLineJoin(context, kCGLineJoinMiter); + + CGFloat dash = 12, dot = 5, space = 3; + NSUInteger pattern = style & 0xF00; + if (pattern == ASTextLineStylePatternSolid) { + CGContextSetLineDash(context, phase, NULL, 0); + } else if (pattern == ASTextLineStylePatternDot) { + CGFloat lengths[2] = {width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 2); + } else if (pattern == ASTextLineStylePatternDash) { + CGFloat lengths[2] = {width * dash, width * space}; + CGContextSetLineDash(context, phase, lengths, 2); + } else if (pattern == ASTextLineStylePatternDashDot) { + CGFloat lengths[4] = {width * dash, width * space, width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 4); + } else if (pattern == ASTextLineStylePatternDashDotDot) { + CGFloat lengths[6] = {width * dash, width * space,width * dot, width * space, width * dot, width * space}; + CGContextSetLineDash(context, phase, lengths, 6); + } else if (pattern == ASTextLineStylePatternCircleDot) { + CGFloat lengths[2] = {width * 0, width * 3}; + CGContextSetLineDash(context, phase, lengths, 2); + CGContextSetLineCap(context, kCGLineCapRound); + CGContextSetLineJoin(context, kCGLineJoinRound); + } +} + + +static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorder *border, NSArray *rects, BOOL isVertical) { + if (rects.count == 0) return; + + ASTextShadow *shadow = border.shadow; + if (shadow.color) { + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, shadow.offset, shadow.radius, shadow.color.CGColor); + CGContextBeginTransparencyLayer(context, NULL); + } + + NSMutableArray *paths = [NSMutableArray new]; + for (NSValue *value in rects) { + CGRect rect = value.CGRectValue; + if (isVertical) { + rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets)); + } else { + rect = UIEdgeInsetsInsetRect(rect, border.insets); + } + rect = ASTextCGRectPixelRound(rect); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius]; + [path closePath]; + [paths addObject:path]; + } + + if (border.fillColor) { + CGContextSaveGState(context); + CGContextSetFillColorWithColor(context, border.fillColor.CGColor); + for (UIBezierPath *path in paths) { + CGContextAddPath(context, path.CGPath); + } + CGContextFillPath(context); + CGContextRestoreGState(context); + } + + if (border.strokeColor && border.lineStyle > 0 && border.strokeWidth > 0) { + + //-------------------------- single line ------------------------------// + CGContextSaveGState(context); + for (UIBezierPath *path in paths) { + CGRect bounds = CGRectUnion(path.bounds, (CGRect){CGPointZero, size}); + bounds = CGRectInset(bounds, -2 * border.strokeWidth, -2 * border.strokeWidth); + CGContextAddRect(context, bounds); + CGContextAddPath(context, path.CGPath); + CGContextEOClip(context); + } + [border.strokeColor setStroke]; + ASTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context); + CGFloat inset = -border.strokeWidth * 0.5; + if ((border.lineStyle & 0xFF) == ASTextLineStyleThick) { + inset *= 2; + CGContextSetLineWidth(context, border.strokeWidth * 2); + } + CGFloat radiusDelta = -inset; + if (border.cornerRadius <= 0) { + radiusDelta = 0; + } + CGContextSetLineJoin(context, border.lineJoin); + for (NSValue *value in rects) { + CGRect rect = value.CGRectValue; + if (isVertical) { + rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets)); + } else { + rect = UIEdgeInsetsInsetRect(rect, border.insets); + } + rect = CGRectInset(rect, inset, inset); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta]; + [path closePath]; + CGContextAddPath(context, path.CGPath); + } + CGContextStrokePath(context); + CGContextRestoreGState(context); + + //------------------------- second line ------------------------------// + if ((border.lineStyle & 0xFF) == ASTextLineStyleDouble) { + CGContextSaveGState(context); + CGFloat inset = -border.strokeWidth * 2; + for (NSValue *value in rects) { + CGRect rect = value.CGRectValue; + rect = UIEdgeInsetsInsetRect(rect, border.insets); + rect = CGRectInset(rect, inset, inset); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + 2 * border.strokeWidth]; + [path closePath]; + + CGRect bounds = CGRectUnion(path.bounds, (CGRect){CGPointZero, size}); + bounds = CGRectInset(bounds, -2 * border.strokeWidth, -2 * border.strokeWidth); + CGContextAddRect(context, bounds); + CGContextAddPath(context, path.CGPath); + CGContextEOClip(context); + } + CGContextSetStrokeColorWithColor(context, border.strokeColor.CGColor); + ASTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context); + CGContextSetLineJoin(context, border.lineJoin); + inset = -border.strokeWidth * 2.5; + radiusDelta = border.strokeWidth * 2; + if (border.cornerRadius <= 0) { + radiusDelta = 0; + } + for (NSValue *value in rects) { + CGRect rect = value.CGRectValue; + rect = UIEdgeInsetsInsetRect(rect, border.insets); + rect = CGRectInset(rect, inset, inset); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta]; + [path closePath]; + CGContextAddPath(context, path.CGPath); + } + CGContextStrokePath(context); + CGContextRestoreGState(context); + } + } + + if (shadow.color) { + CGContextEndTransparencyLayer(context); + CGContextRestoreGState(context); + } +} + +static void ASTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, ASTextLineStyle style, CGPoint position, CGColorRef color, BOOL isVertical) { + NSUInteger styleBase = style & 0xFF; + if (styleBase == 0) return; + + CGContextSaveGState(context); { + if (isVertical) { + CGFloat x, y1, y2, w; + y1 = ASRoundPixelValue(position.y); + y2 = ASRoundPixelValue(position.y + length); + w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); + + CGFloat linePixel = ASTextCGFloatToPixel(w); + if (fabs(linePixel - floor(linePixel)) < 0.1) { + int iPixel = linePixel; + if (iPixel == 0 || (iPixel % 2)) { // odd line pixel + x = ASTextCGFloatPixelHalf(position.x); + } else { + x = ASFloorPixelValue(position.x); + } + } else { + x = position.x; + } + + CGContextSetStrokeColorWithColor(context, color); + ASTextSetLinePatternInContext(style, lineWidth, position.y, context); + CGContextSetLineWidth(context, w); + if (styleBase == ASTextLineStyleSingle) { + CGContextMoveToPoint(context, x, y1); + CGContextAddLineToPoint(context, x, y2); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleThick) { + CGContextMoveToPoint(context, x, y1); + CGContextAddLineToPoint(context, x, y2); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleDouble) { + CGContextMoveToPoint(context, x - w, y1); + CGContextAddLineToPoint(context, x - w, y2); + CGContextStrokePath(context); + CGContextMoveToPoint(context, x + w, y1); + CGContextAddLineToPoint(context, x + w, y2); + CGContextStrokePath(context); + } + } else { + CGFloat x1, x2, y, w; + x1 = ASRoundPixelValue(position.x); + x2 = ASRoundPixelValue(position.x + length); + w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); + + CGFloat linePixel = ASTextCGFloatToPixel(w); + if (fabs(linePixel - floor(linePixel)) < 0.1) { + int iPixel = linePixel; + if (iPixel == 0 || (iPixel % 2)) { // odd line pixel + y = ASTextCGFloatPixelHalf(position.y); + } else { + y = ASFloorPixelValue(position.y); + } + } else { + y = position.y; + } + + CGContextSetStrokeColorWithColor(context, color); + ASTextSetLinePatternInContext(style, lineWidth, position.x, context); + CGContextSetLineWidth(context, w); + if (styleBase == ASTextLineStyleSingle) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleThick) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleDouble) { + CGContextMoveToPoint(context, x1, y - w); + CGContextAddLineToPoint(context, x2, y - w); + CGContextStrokePath(context); + CGContextMoveToPoint(context, x1, y + w); + CGContextAddLineToPoint(context, x2, y + w); + CGContextStrokePath(context); + } + } + } CGContextRestoreGState(context); +} + +static void ASTextDrawText(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + CGContextSaveGState(context); { + + CGContextTranslateCTM(context, point.x, point.y); + CGContextTranslateCTM(context, 0, size.height); + CGContextScaleCTM(context, 1, -1); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + NSArray *lineRunRanges = line.verticalRotateRange; + CGFloat posX = line.position.x + verticalOffset; + CGFloat posY = size.height - line.position.y; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextPosition(context, posX, posY); + ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } + if (cancel && cancel()) break; + } + + // Use this to draw frame for test/debug. + // CGContextTranslateCTM(context, verticalOffset, size.height); + // CTFrameDraw(layout.frame, context); + + } CGContextRestoreGState(context); +} + +static void ASTextDrawBlockBorder(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextBorder *border = attrs[ASTextBlockBorderAttributeName]; + if (!border) continue; + + NSUInteger lineStartIndex = line.index; + while (lineStartIndex > 0) { + if (((ASTextLine *)lines[lineStartIndex - 1]).row == line.row) lineStartIndex--; + else break; + } + + CGRect unionRect = CGRectZero; + NSUInteger lineStartRow = ((ASTextLine *)lines[lineStartIndex]).row; + NSUInteger lineContinueIndex = lineStartIndex; + NSUInteger lineContinueRow = lineStartRow; + do { + ASTextLine *one = lines[lineContinueIndex]; + if (lineContinueIndex == lineStartIndex) { + unionRect = one.bounds; + } else { + unionRect = CGRectUnion(unionRect, one.bounds); + } + if (lineContinueIndex + 1 == lMax) break; + ASTextLine *next = lines[lineContinueIndex + 1]; + if (next.row != lineContinueRow) { + ASTextBorder *nextBorder = [layout.text as_attribute:ASTextBlockBorderAttributeName atIndex:next.range.location]; + if ([nextBorder isEqual:border]) { + lineContinueRow++; + } else { + break; + } + } + lineContinueIndex++; + } while (true); + + if (isVertical) { + UIEdgeInsets insets = layout.container.insets; + unionRect.origin.y = insets.top; + unionRect.size.height = layout.container.size.height -insets.top - insets.bottom; + } else { + UIEdgeInsets insets = layout.container.insets; + unionRect.origin.x = insets.left; + unionRect.size.width = layout.container.size.width -insets.left - insets.right; + } + unionRect.origin.x += verticalOffset; + ASTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]], isVertical); + + l = lineContinueIndex; + break; + } + } + + + CGContextRestoreGState(context); +} + +static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, ASTextBorderType type, BOOL (^cancel)(void)) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + NSString *borderKey = (type == ASTextBorderTypeNormal ? ASTextBorderAttributeName : ASTextBackgroundBorderAttributeName); + + BOOL needJumpRun = NO; + NSUInteger jumpRunIndex = 0; + + for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + if (needJumpRun) { + needJumpRun = NO; + r = jumpRunIndex + 1; + if (r >= rMax) break; + } + + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextBorder *border = attrs[borderKey]; + if (!border) continue; + + CFRange runRange = CTRunGetStringRange(run); + if (runRange.location == kCFNotFound || runRange.length == 0) continue; + if (runRange.location + runRange.length > layout.text.length) continue; + + NSMutableArray *runRects = [NSMutableArray new]; + NSInteger endLineIndex = l; + NSInteger endRunIndex = r; + BOOL endFound = NO; + for (NSInteger ll = l; ll < lMax; ll++) { + if (endFound) break; + ASTextLine *iLine = lines[ll]; + CFArrayRef iRuns = CTLineGetGlyphRuns(iLine.CTLine); + + CGRect extLineRect = CGRectNull; + for (NSInteger rr = (ll == l) ? r : 0, rrMax = CFArrayGetCount(iRuns); rr < rrMax; rr++) { + CTRunRef iRun = (CTRunRef)CFArrayGetValueAtIndex(iRuns, rr); + NSDictionary *iAttrs = (id)CTRunGetAttributes(iRun); + ASTextBorder *iBorder = iAttrs[borderKey]; + if (![border isEqual:iBorder]) { + endFound = YES; + break; + } + endLineIndex = ll; + endRunIndex = rr; + + CGPoint iRunPosition = CGPointZero; + CTRunGetPositions(iRun, CFRangeMake(0, 1), &iRunPosition); + CGFloat ascent, descent; + CGFloat iRunWidth = CTRunGetTypographicBounds(iRun, CFRangeMake(0, 0), &ascent, &descent, NULL); + + if (isVertical) { + ASTEXT_SWAP(iRunPosition.x, iRunPosition.y); + iRunPosition.y += iLine.position.y; + CGRect iRect = CGRectMake(verticalOffset + line.position.x - descent, iRunPosition.y, ascent + descent, iRunWidth); + if (CGRectIsNull(extLineRect)) { + extLineRect = iRect; + } else { + extLineRect = CGRectUnion(extLineRect, iRect); + } + } else { + iRunPosition.x += iLine.position.x; + CGRect iRect = CGRectMake(iRunPosition.x, iLine.position.y - ascent, iRunWidth, ascent + descent); + if (CGRectIsNull(extLineRect)) { + extLineRect = iRect; + } else { + extLineRect = CGRectUnion(extLineRect, iRect); + } + } + } + + if (!CGRectIsNull(extLineRect)) { + [runRects addObject:[NSValue valueWithCGRect:extLineRect]]; + } + } + + NSMutableArray *drawRects = [NSMutableArray new]; + CGRect curRect= ((NSValue *)[runRects firstObject]).CGRectValue; + for (NSInteger re = 0, reMax = runRects.count; re < reMax; re++) { + CGRect rect = ((NSValue *)runRects[re]).CGRectValue; + if (isVertical) { + if (fabs(rect.origin.x - curRect.origin.x) < 1) { + curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); + } else { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; + } + } else { + if (fabs(rect.origin.y - curRect.origin.y) < 1) { + curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); + } else { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; + } + } + } + if (!CGRectEqualToRect(curRect, CGRectZero)) { + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + } + + ASTextDrawBorderRects(context, size, border, drawRects, isVertical); + + if (l == endLineIndex) { + r = endRunIndex; + } else { + l = endLineIndex - 1; + needJumpRun = YES; + jumpRunIndex = endRunIndex; + break; + } + + } + } + + CGContextRestoreGState(context); +} + +static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, ASTextDecorationType type, BOOL (^cancel)(void)) { + NSArray *lines = layout.lines; + + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGContextTranslateCTM(context, verticalOffset, 0); + + for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextDecoration *underline = attrs[ASTextUnderlineAttributeName]; + ASTextDecoration *strikethrough = attrs[ASTextStrikethroughAttributeName]; + + BOOL needDrawUnderline = NO, needDrawStrikethrough = NO; + if ((type & ASTextDecorationTypeUnderline) && underline.style > 0) { + needDrawUnderline = YES; + } + if ((type & ASTextDecorationTypeStrikethrough) && strikethrough.style > 0) { + needDrawStrikethrough = YES; + } + if (!needDrawUnderline && !needDrawStrikethrough) continue; + + CFRange runRange = CTRunGetStringRange(run); + if (runRange.location == kCFNotFound || runRange.length == 0) continue; + if (runRange.location + runRange.length > layout.text.length) continue; + NSString *runStr = [layout.text attributedSubstringFromRange:NSMakeRange(runRange.location, runRange.length)].string; + if (ASTextIsLinebreakString(runStr)) continue; // may need more checks... + + CGFloat xHeight, underlinePosition, lineThickness; + ASTextGetRunsMaxMetric(runs, &xHeight, &underlinePosition, &lineThickness); + + CGPoint underlineStart, strikethroughStart; + CGFloat length; + + if (isVertical) { + underlineStart.x = line.position.x + underlinePosition; + strikethroughStart.x = line.position.x + xHeight / 2; + + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + underlineStart.y = strikethroughStart.y = runPosition.x + line.position.y; + length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + + } else { + underlineStart.y = line.position.y - underlinePosition; + strikethroughStart.y = line.position.y - xHeight / 2; + + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + underlineStart.x = strikethroughStart.x = runPosition.x + line.position.x; + length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + } + + if (needDrawUnderline) { + CGColorRef color = underline.color.CGColor; + if (!color) { + color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); + color = ASTextGetCGColor(color); + } + CGFloat thickness = underline.width ? underline.width.floatValue : lineThickness; + ASTextShadow *shadow = underline.shadow; + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGFloat offsetAlterX = size.width + 0xFFFF; + CGContextSaveGState(context); { + CGSize offset = shadow.offset; + offset.width -= offsetAlterX; + CGContextSaveGState(context); { + CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); + CGContextSetBlendMode(context, shadow.blendMode); + CGContextTranslateCTM(context, offsetAlterX, 0); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } CGContextRestoreGState(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } + + if (needDrawStrikethrough) { + CGColorRef color = strikethrough.color.CGColor; + if (!color) { + color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); + color = ASTextGetCGColor(color); + } + CGFloat thickness = strikethrough.width ? strikethrough.width.floatValue : lineThickness; + ASTextShadow *shadow = underline.shadow; + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGFloat offsetAlterX = size.width + 0xFFFF; + CGContextSaveGState(context); { + CGSize offset = shadow.offset; + offset.width -= offsetAlterX; + CGContextSaveGState(context); { + CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); + CGContextSetBlendMode(context, shadow.blendMode); + CGContextTranslateCTM(context, offsetAlterX, 0); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + } CGContextRestoreGState(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + ASTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color, isVertical); + } + } + } + CGContextRestoreGState(context); +} + +static void ASTextDrawAttachment(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) { + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) { + ASTextAttachment *a = layout.attachments[i]; + if (!a.content) continue; + + UIImage *image = nil; + UIView *view = nil; + CALayer *layer = nil; + if ([a.content isKindOfClass:[UIImage class]]) { + image = a.content; + } else if ([a.content isKindOfClass:[UIView class]]) { + view = a.content; + } else if ([a.content isKindOfClass:[CALayer class]]) { + layer = a.content; + } + if (!image && !view && !layer) continue; + if (image && !context) continue; + if (view && !targetView) continue; + if (layer && !targetLayer) continue; + if (cancel && cancel()) break; + + CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size; + CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue; + if (isVertical) { + rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets)); + } else { + rect = UIEdgeInsetsInsetRect(rect, a.contentInsets); + } + rect = ASTextCGRectFitWithContentMode(rect, asize, a.contentMode); + rect = ASTextCGRectPixelRound(rect); + rect = CGRectStandardize(rect); + rect.origin.x += point.x + verticalOffset; + rect.origin.y += point.y; + if (image) { + CGImageRef ref = image.CGImage; + if (ref) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect)); + CGContextScaleCTM(context, 1, -1); + CGContextDrawImage(context, rect, ref); + CGContextRestoreGState(context); + } + } else if (view) { + view.frame = rect; + [targetView addSubview:view]; + } else if (layer) { + layer.frame = rect; + [targetLayer addSublayer:layer]; + } + } +} + +static void ASTextDrawShadow(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + //move out of context. (0xFFFF is just a random large number) + CGFloat offsetAlterX = size.width + 0xFFFF; + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + CGContextSaveGState(context); { + CGContextTranslateCTM(context, point.x, point.y); + CGContextTranslateCTM(context, 0, size.height); + CGContextScaleCTM(context, 1, -1); + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + NSArray *lineRunRanges = line.verticalRotateRange; + CGFloat linePosX = line.position.x; + CGFloat linePosY = size.height - line.position.y; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextPosition(context, linePosX, linePosY); + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextShadow *shadow = attrs[ASTextShadowAttributeName]; + ASTextShadow *nsShadow = [ASTextShadow shadowWithNSShadow:attrs[NSShadowAttributeName]]; // NSShadow compatible + if (nsShadow) { + nsShadow.subShadow = shadow; + shadow = nsShadow; + } + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGSize offset = shadow.offset; + offset.width -= offsetAlterX; + CGContextSaveGState(context); { + CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); + CGContextSetBlendMode(context, shadow.blendMode); + CGContextTranslateCTM(context, offsetAlterX, 0); + ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + } + } + } CGContextRestoreGState(context); +} + +static void ASTextDrawInnerShadow(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) { + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + CGContextTranslateCTM(context, 0, size.height); + CGContextScaleCTM(context, 1, -1); + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { + if (cancel && cancel()) break; + + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + NSArray *lineRunRanges = line.verticalRotateRange; + CGFloat linePosX = line.position.x; + CGFloat linePosY = size.height - line.position.y; + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + if (CTRunGetGlyphCount(run) == 0) continue; + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextPosition(context, linePosX, linePosY); + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextShadow *shadow = attrs[ASTextInnerShadowAttributeName]; + while (shadow) { + if (!shadow.color) { + shadow = shadow.subShadow; + continue; + } + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + CGRect runImageBounds = CTRunGetImageBounds(run, context, CFRangeMake(0, 0)); + runImageBounds.origin.x += runPosition.x; + if (runImageBounds.size.width < 0.1 || runImageBounds.size.height < 0.1) continue; + + CFDictionaryRef runAttrs = CTRunGetAttributes(run); + NSValue *glyphTransformValue = (NSValue *)CFDictionaryGetValue(runAttrs, (__bridge const void *)(ASTextGlyphTransformAttributeName)); + if (glyphTransformValue) { + runImageBounds = CGRectMake(0, 0, size.width, size.height); + } + + // text inner shadow + CGContextSaveGState(context); { + CGContextSetBlendMode(context, shadow.blendMode); + CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL); + CGContextSetAlpha(context, CGColorGetAlpha(shadow.color.CGColor)); + CGContextClipToRect(context, runImageBounds); + CGContextBeginTransparencyLayer(context, NULL); { + UIColor *opaqueShadowColor = [shadow.color colorWithAlphaComponent:1]; + CGContextSetShadowWithColor(context, shadow.offset, shadow.radius, opaqueShadowColor.CGColor); + CGContextSetFillColorWithColor(context, opaqueShadowColor.CGColor); + CGContextSetBlendMode(context, kCGBlendModeSourceOut); + CGContextBeginTransparencyLayer(context, NULL); { + CGContextFillRect(context, runImageBounds); + CGContextSetBlendMode(context, kCGBlendModeDestinationIn); + CGContextBeginTransparencyLayer(context, NULL); { + ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + } CGContextEndTransparencyLayer(context); + } CGContextEndTransparencyLayer(context); + } CGContextEndTransparencyLayer(context); + } CGContextRestoreGState(context); + shadow = shadow.subShadow; + } + } + } + + CGContextRestoreGState(context); +} + +static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, ASTextDebugOption *op) { + UIGraphicsPushContext(context); + CGContextSaveGState(context); + CGContextTranslateCTM(context, point.x, point.y); + CGContextSetLineWidth(context, 1.0 / ASScreenScale()); + CGContextSetLineDash(context, 0, NULL, 0); + CGContextSetLineJoin(context, kCGLineJoinMiter); + CGContextSetLineCap(context, kCGLineCapButt); + + BOOL isVertical = layout.container.verticalForm; + CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGContextTranslateCTM(context, verticalOffset, 0); + + if (op.CTFrameBorderColor || op.CTFrameFillColor) { + UIBezierPath *path = layout.container.path; + if (!path) { + CGRect rect = (CGRect){CGPointZero, layout.container.size}; + rect = UIEdgeInsetsInsetRect(rect, layout.container.insets); + if (op.CTFrameBorderColor) rect = ASTextCGRectPixelHalf(rect); + else rect = ASTextCGRectPixelRound(rect); + path = [UIBezierPath bezierPathWithRect:rect]; + } + [path closePath]; + + for (UIBezierPath *ex in layout.container.exclusionPaths) { + [path appendPath:ex]; + } + if (op.CTFrameFillColor) { + [op.CTFrameFillColor setFill]; + if (layout.container.pathLineWidth > 0) { + CGContextSaveGState(context); { + CGContextBeginTransparencyLayer(context, NULL); { + CGContextAddPath(context, path.CGPath); + if (layout.container.pathFillEvenOdd) { + CGContextEOFillPath(context); + } else { + CGContextFillPath(context); + } + CGContextSetBlendMode(context, kCGBlendModeDestinationOut); + [[UIColor blackColor] setFill]; + CGPathRef cgPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, layout.container.pathLineWidth, kCGLineCapButt, kCGLineJoinMiter, 0); + if (cgPath) { + CGContextAddPath(context, cgPath); + CGContextFillPath(context); + } + CGPathRelease(cgPath); + } CGContextEndTransparencyLayer(context); + } CGContextRestoreGState(context); + } else { + CGContextAddPath(context, path.CGPath); + if (layout.container.pathFillEvenOdd) { + CGContextEOFillPath(context); + } else { + CGContextFillPath(context); + } + } + } + if (op.CTFrameBorderColor) { + CGContextSaveGState(context); { + if (layout.container.pathLineWidth > 0) { + CGContextSetLineWidth(context, layout.container.pathLineWidth); + } + [op.CTFrameBorderColor setStroke]; + CGContextAddPath(context, path.CGPath); + CGContextStrokePath(context); + } CGContextRestoreGState(context); + } + } + + NSArray *lines = layout.lines; + for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { + ASTextLine *line = lines[l]; + if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine; + CGRect lineBounds = line.bounds; + if (op.CTLineFillColor) { + [op.CTLineFillColor setFill]; + CGContextAddRect(context, ASTextCGRectPixelRound(lineBounds)); + CGContextFillPath(context); + } + if (op.CTLineBorderColor) { + [op.CTLineBorderColor setStroke]; + CGContextAddRect(context, ASTextCGRectPixelHalf(lineBounds)); + CGContextStrokePath(context); + } + if (op.baselineColor) { + [op.baselineColor setStroke]; + if (isVertical) { + CGFloat x = ASTextCGFloatPixelHalf(line.position.x); + CGFloat y1 = ASTextCGFloatPixelHalf(line.top); + CGFloat y2 = ASTextCGFloatPixelHalf(line.bottom); + CGContextMoveToPoint(context, x, y1); + CGContextAddLineToPoint(context, x, y2); + CGContextStrokePath(context); + } else { + CGFloat x1 = ASTextCGFloatPixelHalf(lineBounds.origin.x); + CGFloat x2 = ASTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width); + CGFloat y = ASTextCGFloatPixelHalf(line.position.y); + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } + } + if (op.CTLineNumberColor) { + [op.CTLineNumberColor set]; + NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(l).description]; + num.as_color = op.CTLineNumberColor; + num.as_font = [UIFont systemFontOfSize:6]; + [num drawAtPoint:CGPointMake(line.position.x, line.position.y - (isVertical ? 1 : 6))]; + } + if (op.CTRunFillColor || op.CTRunBorderColor || op.CTRunNumberColor || op.CGGlyphFillColor || op.CGGlyphBorderColor) { + CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); + for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + CGPoint glyphPositions[glyphCount]; + CTRunGetPositions(run, CFRangeMake(0, glyphCount), glyphPositions); + + CGSize glyphAdvances[glyphCount]; + CTRunGetAdvances(run, CFRangeMake(0, glyphCount), glyphAdvances); + + CGPoint runPosition = glyphPositions[0]; + if (isVertical) { + ASTEXT_SWAP(runPosition.x, runPosition.y); + runPosition.x = line.position.x; + runPosition.y += line.position.y; + } else { + runPosition.x += line.position.x; + runPosition.y = line.position.y - runPosition.y; + } + + CGFloat ascent, descent, leading; + CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); + CGRect runTypoBounds; + if (isVertical) { + runTypoBounds = CGRectMake(runPosition.x - descent, runPosition.y, ascent + descent, width); + } else { + runTypoBounds = CGRectMake(runPosition.x, line.position.y - ascent, width, ascent + descent); + } + + if (op.CTRunFillColor) { + [op.CTRunFillColor setFill]; + CGContextAddRect(context, ASTextCGRectPixelRound(runTypoBounds)); + CGContextFillPath(context); + } + if (op.CTRunBorderColor) { + [op.CTRunBorderColor setStroke]; + CGContextAddRect(context, ASTextCGRectPixelHalf(runTypoBounds)); + CGContextStrokePath(context); + } + if (op.CTRunNumberColor) { + [op.CTRunNumberColor set]; + NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(r).description]; + num.as_color = op.CTRunNumberColor; + num.as_font = [UIFont systemFontOfSize:6]; + [num drawAtPoint:CGPointMake(runTypoBounds.origin.x, runTypoBounds.origin.y - 1)]; + } + if (op.CGGlyphBorderColor || op.CGGlyphFillColor) { + for (NSUInteger g = 0; g < glyphCount; g++) { + CGPoint pos = glyphPositions[g]; + CGSize adv = glyphAdvances[g]; + CGRect rect; + if (isVertical) { + ASTEXT_SWAP(pos.x, pos.y); + pos.x = runPosition.x; + pos.y += line.position.y; + rect = CGRectMake(pos.x - descent, pos.y, runTypoBounds.size.width, adv.width); + } else { + pos.x += line.position.x; + pos.y = runPosition.y; + rect = CGRectMake(pos.x, pos.y - ascent, adv.width, runTypoBounds.size.height); + } + if (op.CGGlyphFillColor) { + [op.CGGlyphFillColor setFill]; + CGContextAddRect(context, ASTextCGRectPixelRound(rect)); + CGContextFillPath(context); + } + if (op.CGGlyphBorderColor) { + [op.CGGlyphBorderColor setStroke]; + CGContextAddRect(context, ASTextCGRectPixelHalf(rect)); + CGContextStrokePath(context); + } + } + } + } + } + } + CGContextRestoreGState(context); + UIGraphicsPopContext(); +} + + +- (void)drawInContext:(CGContextRef)context + size:(CGSize)size + point:(CGPoint)point + view:(UIView *)view + layer:(CALayer *)layer + debug:(ASTextDebugOption *)debug + cancel:(BOOL (^)(void))cancel{ + @autoreleasepool { + if (self.needDrawBlockBorder && context) { + if (cancel && cancel()) return; + ASTextDrawBlockBorder(self, context, size, point, cancel); + } + if (self.needDrawBackgroundBorder && context) { + if (cancel && cancel()) return; + ASTextDrawBorder(self, context, size, point, ASTextBorderTypeBackgound, cancel); + } + if (self.needDrawShadow && context) { + if (cancel && cancel()) return; + ASTextDrawShadow(self, context, size, point, cancel); + } + if (self.needDrawUnderline && context) { + if (cancel && cancel()) return; + ASTextDrawDecoration(self, context, size, point, ASTextDecorationTypeUnderline, cancel); + } + if (self.needDrawText && context) { + if (cancel && cancel()) return; + ASTextDrawText(self, context, size, point, cancel); + } + if (self.needDrawAttachment && (context || view || layer)) { + if (cancel && cancel()) return; + ASTextDrawAttachment(self, context, size, point, view, layer, cancel); + } + if (self.needDrawInnerShadow && context) { + if (cancel && cancel()) return; + ASTextDrawInnerShadow(self, context, size, point, cancel); + } + if (self.needDrawStrikethrough && context) { + if (cancel && cancel()) return; + ASTextDrawDecoration(self, context, size, point, ASTextDecorationTypeStrikethrough, cancel); + } + if (self.needDrawBorder && context) { + if (cancel && cancel()) return; + ASTextDrawBorder(self, context, size, point, ASTextBorderTypeNormal, cancel); + } + if (debug.needDrawDebug && context) { + if (cancel && cancel()) return; + ASTextDrawDebug(self, context, size, point, debug); + } + } +} + +- (void)drawInContext:(CGContextRef)context + size:(CGSize)size + debug:(ASTextDebugOption *)debug { + [self drawInContext:context size:size point:CGPointZero view:nil layer:nil debug:debug cancel:nil]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLine.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLine.h new file mode 100644 index 0000000000..acb02e991b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLine.h @@ -0,0 +1,77 @@ +// +// ASTextLine.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@class ASTextRunGlyphRange; + +NS_ASSUME_NONNULL_BEGIN + +/** + A text line object wrapped `CTLineRef`, see `ASTextLayout` for more. + */ +@interface ASTextLine : NSObject + ++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical NS_RETURNS_RETAINED; + +@property (nonatomic) NSUInteger index; ///< line index +@property (nonatomic) NSUInteger row; ///< line row +@property (nullable, nonatomic) NSArray *> *verticalRotateRange; ///< Run rotate range + +@property (nonatomic, readonly) CTLineRef CTLine; ///< CoreText line +@property (nonatomic, readonly) NSRange range; ///< string range +@property (nonatomic, readonly) BOOL vertical; ///< vertical form + +@property (nonatomic, readonly) CGRect bounds; ///< bounds (ascent + descent) +@property (nonatomic, readonly) CGSize size; ///< bounds.size +@property (nonatomic, readonly) CGFloat width; ///< bounds.size.width +@property (nonatomic, readonly) CGFloat height; ///< bounds.size.height +@property (nonatomic, readonly) CGFloat top; ///< bounds.origin.y +@property (nonatomic, readonly) CGFloat bottom; ///< bounds.origin.y + bounds.size.height +@property (nonatomic, readonly) CGFloat left; ///< bounds.origin.x +@property (nonatomic, readonly) CGFloat right; ///< bounds.origin.x + bounds.size.width + +@property (nonatomic) CGPoint position; ///< baseline position +@property (nonatomic, readonly) CGFloat ascent; ///< line ascent +@property (nonatomic, readonly) CGFloat descent; ///< line descent +@property (nonatomic, readonly) CGFloat leading; ///< line leading +@property (nonatomic, readonly) CGFloat lineWidth; ///< line width +@property (nonatomic, readonly) CGFloat trailingWhitespaceWidth; + +@property (nullable, nonatomic, readonly) NSArray *attachments; ///< ASTextAttachment +@property (nullable, nonatomic, readonly) NSArray *attachmentRanges; ///< NSRange(NSValue) +@property (nullable, nonatomic, readonly) NSArray *attachmentRects; ///< CGRect(NSValue) + +@end + + +typedef NS_ENUM(NSUInteger, ASTextRunGlyphDrawMode) { + /// No rotate. + ASTextRunGlyphDrawModeHorizontal = 0, + + /// Rotate vertical for single glyph. + ASTextRunGlyphDrawModeVerticalRotate = 1, + + /// Rotate vertical for single glyph, and move the glyph to a better position, + /// such as fullwidth punctuation. + ASTextRunGlyphDrawModeVerticalRotateMove = 2, +}; + +/** + A range in CTRun, used for vertical form. + */ +@interface ASTextRunGlyphRange : NSObject +@property (nonatomic) NSRange glyphRangeInRun; +@property (nonatomic) ASTextRunGlyphDrawMode drawMode; ++ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode NS_RETURNS_RETAINED; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLine.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLine.mm new file mode 100644 index 0000000000..a01311d83c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Component/ASTextLine.mm @@ -0,0 +1,164 @@ +// +// ASTextLine.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASTextLine { + CGFloat _firstGlyphPos; // first glyph position for baseline, typically 0. +} + ++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical NS_RETURNS_RETAINED { + if (!CTLine) return nil; + ASTextLine *line = [self new]; + line->_position = position; + line->_vertical = isVertical; + [line setCTLine:CTLine]; + return line; +} + +- (void)dealloc { + if (_CTLine) CFRelease(_CTLine); +} + +- (void)setCTLine:(_Nonnull CTLineRef)CTLine { + if (_CTLine != CTLine) { + if (CTLine) CFRetain(CTLine); + if (_CTLine) CFRelease(_CTLine); + _CTLine = CTLine; + if (_CTLine) { + _lineWidth = CTLineGetTypographicBounds(_CTLine, &_ascent, &_descent, &_leading); + CFRange range = CTLineGetStringRange(_CTLine); + _range = NSMakeRange(range.location, range.length); + if (CTLineGetGlyphCount(_CTLine) > 0) { + CFArrayRef runs = CTLineGetGlyphRuns(_CTLine); + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, 0); + CGPoint pos; + CTRunGetPositions(run, CFRangeMake(0, 1), &pos); + _firstGlyphPos = pos.x; + } else { + _firstGlyphPos = 0; + } + _trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_CTLine); + } else { + _lineWidth = _ascent = _descent = _leading = _firstGlyphPos = _trailingWhitespaceWidth = 0; + _range = NSMakeRange(0, 0); + } + [self reloadBounds]; + } +} + +- (void)setPosition:(CGPoint)position { + _position = position; + [self reloadBounds]; +} + +- (void)reloadBounds { + if (_vertical) { + _bounds = CGRectMake(_position.x - _descent, _position.y, _ascent + _descent, _lineWidth); + _bounds.origin.y += _firstGlyphPos; + } else { + _bounds = CGRectMake(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent); + _bounds.origin.x += _firstGlyphPos; + } + + _attachments = nil; + _attachmentRanges = nil; + _attachmentRects = nil; + if (!_CTLine) return; + CFArrayRef runs = CTLineGetGlyphRuns(_CTLine); + NSUInteger runCount = CFArrayGetCount(runs); + if (runCount == 0) return; + + NSMutableArray *attachments = [NSMutableArray new]; + NSMutableArray *attachmentRanges = [NSMutableArray new]; + NSMutableArray *attachmentRects = [NSMutableArray new]; + for (NSUInteger r = 0; r < runCount; r++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + NSDictionary *attrs = (id)CTRunGetAttributes(run); + ASTextAttachment *attachment = attrs[ASTextAttachmentAttributeName]; + if (attachment) { + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + + CGFloat ascent, descent, leading, runWidth; + CGRect runTypoBounds; + runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); + + if (_vertical) { + ASTEXT_SWAP(runPosition.x, runPosition.y); + runPosition.y = _position.y + runPosition.y; + runTypoBounds = CGRectMake(_position.x + runPosition.x - descent, runPosition.y , ascent + descent, runWidth); + } else { + runPosition.x += _position.x; + runPosition.y = _position.y - runPosition.y; + runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent); + } + + NSRange runRange = ASTextNSRangeFromCFRange(CTRunGetStringRange(run)); + [attachments addObject:attachment]; + [attachmentRanges addObject:[NSValue valueWithRange:runRange]]; + [attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]]; + } + } + _attachments = attachments.count ? attachments : nil; + _attachmentRanges = attachmentRanges.count ? attachmentRanges : nil; + _attachmentRects = attachmentRects.count ? attachmentRects : nil; +} + +- (CGSize)size { + return _bounds.size; +} + +- (CGFloat)width { + return CGRectGetWidth(_bounds); +} + +- (CGFloat)height { + return CGRectGetHeight(_bounds); +} + +- (CGFloat)top { + return CGRectGetMinY(_bounds); +} + +- (CGFloat)bottom { + return CGRectGetMaxY(_bounds); +} + +- (CGFloat)left { + return CGRectGetMinX(_bounds); +} + +- (CGFloat)right { + return CGRectGetMaxX(_bounds); +} + +- (NSString *)description { + NSMutableString *desc = @"".mutableCopy; + NSRange range = self.range; + [desc appendFormat:@" row:%ld range:%tu,%tu", self, (long)self.row, range.location, range.length]; + [desc appendFormat:@" position:%@",NSStringFromCGPoint(self.position)]; + [desc appendFormat:@" bounds:%@",NSStringFromCGRect(self.bounds)]; + return desc; +} + +@end + + +@implementation ASTextRunGlyphRange ++ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode NS_RETURNS_RETAINED { + ASTextRunGlyphRange *one = [self new]; + one.glyphRangeInRun = range; + one.drawMode = mode; + return one; +} +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextAttribute.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextAttribute.h new file mode 100644 index 0000000000..2d9e3771a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextAttribute.h @@ -0,0 +1,346 @@ +// +// ASTextAttribute.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Enum Define + +/// The attribute type +typedef NS_OPTIONS(NSInteger, ASTextAttributeType) { + ASTextAttributeTypeNone = 0, + ASTextAttributeTypeUIKit = 1 << 0, ///< UIKit attributes, such as UILabel/UITextField/drawInRect. + ASTextAttributeTypeCoreText = 1 << 1, ///< CoreText attributes, used by CoreText. + ASTextAttributeTypeASText = 1 << 2, ///< ASText attributes, used by ASText. +}; + +/// Get the attribute type from an attribute name. +AS_EXTERN ASTextAttributeType ASTextAttributeGetType(NSString *attributeName); + +/** + Line style in ASText (similar to NSUnderlineStyle). + */ +typedef NS_OPTIONS (NSInteger, ASTextLineStyle) { + // basic style (bitmask:0xFF) + ASTextLineStyleNone = 0x00, ///< ( ) Do not draw a line (Default). + ASTextLineStyleSingle = 0x01, ///< (──────) Draw a single line. + ASTextLineStyleThick = 0x02, ///< (━━━━━━━) Draw a thick line. + ASTextLineStyleDouble = 0x09, ///< (══════) Draw a double line. + + // style pattern (bitmask:0xF00) + ASTextLineStylePatternSolid = 0x000, ///< (────────) Draw a solid line (Default). + ASTextLineStylePatternDot = 0x100, ///< (‑ ‑ ‑ ‑ ‑ ‑) Draw a line of dots. + ASTextLineStylePatternDash = 0x200, ///< (— — — —) Draw a line of dashes. + ASTextLineStylePatternDashDot = 0x300, ///< (— ‑ — ‑ — ‑) Draw a line of alternating dashes and dots. + ASTextLineStylePatternDashDotDot = 0x400, ///< (— ‑ ‑ — ‑ ‑) Draw a line of alternating dashes and two dots. + ASTextLineStylePatternCircleDot = 0x900, ///< (••••••••••••) Draw a line of small circle dots. +}; + +/** + Text vertical alignment. + */ +typedef NS_ENUM(NSInteger, ASTextVerticalAlignment) { + ASTextVerticalAlignmentTop = 0, ///< Top alignment. + ASTextVerticalAlignmentCenter = 1, ///< Center alignment. + ASTextVerticalAlignmentBottom = 2, ///< Bottom alignment. +}; + +/** + The direction define in ASText. + */ +typedef NS_OPTIONS(NSUInteger, ASTextDirection) { + ASTextDirectionNone = 0, + ASTextDirectionTop = 1 << 0, + ASTextDirectionRight = 1 << 1, + ASTextDirectionBottom = 1 << 2, + ASTextDirectionLeft = 1 << 3, +}; + +/** + The trunction type, tells the truncation engine which type of truncation is being requested. + */ +typedef NS_ENUM (NSUInteger, ASTextTruncationType) { + /// No truncate. + ASTextTruncationTypeNone = 0, + + /// Truncate at the beginning of the line, leaving the end portion visible. + ASTextTruncationTypeStart = 1, + + /// Truncate at the end of the line, leaving the start portion visible. + ASTextTruncationTypeEnd = 2, + + /// Truncate in the middle of the line, leaving both the start and the end portions visible. + ASTextTruncationTypeMiddle = 3, +}; + + + +#pragma mark - Attribute Name Defined in ASText + +/// The value of this attribute is a `ASTextBackedString` object. +/// Use this attribute to store the original plain text if it is replaced by something else (such as attachment). +UIKIT_EXTERN NSString *const ASTextBackedStringAttributeName; + +/// The value of this attribute is a `ASTextBinding` object. +/// Use this attribute to bind a range of text together, as if it was a single charactor. +UIKIT_EXTERN NSString *const ASTextBindingAttributeName; + +/// The value of this attribute is a `ASTextShadow` object. +/// Use this attribute to add shadow to a range of text. +/// Shadow will be drawn below text glyphs. Use ASTextShadow.subShadow to add multi-shadow. +UIKIT_EXTERN NSString *const ASTextShadowAttributeName; + +/// The value of this attribute is a `ASTextShadow` object. +/// Use this attribute to add inner shadow to a range of text. +/// Inner shadow will be drawn above text glyphs. Use ASTextShadow.subShadow to add multi-shadow. +UIKIT_EXTERN NSString *const ASTextInnerShadowAttributeName; + +/// The value of this attribute is a `ASTextDecoration` object. +/// Use this attribute to add underline to a range of text. +/// The underline will be drawn below text glyphs. +UIKIT_EXTERN NSString *const ASTextUnderlineAttributeName; + +/// The value of this attribute is a `ASTextDecoration` object. +/// Use this attribute to add strikethrough (delete line) to a range of text. +/// The strikethrough will be drawn above text glyphs. +UIKIT_EXTERN NSString *const ASTextStrikethroughAttributeName; + +/// The value of this attribute is a `ASTextBorder` object. +/// Use this attribute to add cover border or cover color to a range of text. +/// The border will be drawn above the text glyphs. +UIKIT_EXTERN NSString *const ASTextBorderAttributeName; + +/// The value of this attribute is a `ASTextBorder` object. +/// Use this attribute to add background border or background color to a range of text. +/// The border will be drawn below the text glyphs. +UIKIT_EXTERN NSString *const ASTextBackgroundBorderAttributeName; + +/// The value of this attribute is a `ASTextBorder` object. +/// Use this attribute to add a code block border to one or more line of text. +/// The border will be drawn below the text glyphs. +UIKIT_EXTERN NSString *const ASTextBlockBorderAttributeName; + +/// The value of this attribute is a `ASTextAttachment` object. +/// Use this attribute to add attachment to text. +/// It should be used in conjunction with a CTRunDelegate. +UIKIT_EXTERN NSString *const ASTextAttachmentAttributeName; + +/// The value of this attribute is a `ASTextHighlight` object. +/// Use this attribute to add a touchable highlight state to a range of text. +UIKIT_EXTERN NSString *const ASTextHighlightAttributeName; + +/// The value of this attribute is a `NSValue` object stores CGAffineTransform. +/// Use this attribute to add transform to each glyph in a range of text. +UIKIT_EXTERN NSString *const ASTextGlyphTransformAttributeName; + + + +#pragma mark - String Token Define + +UIKIT_EXTERN NSString *const ASTextAttachmentToken; ///< Object replacement character (U+FFFC), used for text attachment. +UIKIT_EXTERN NSString *const ASTextTruncationToken; ///< Horizontal ellipsis (U+2026), used for text truncation "…". + + + +#pragma mark - Attribute Value Define + +/** + The tap/long press action callback defined in ASText. + + @param containerView The text container view (such as ASLabel/ASTextView). + @param text The whole text. + @param range The text range in `text` (if no range, the range.location is NSNotFound). + @param rect The text frame in `containerView` (if no data, the rect is CGRectNull). + */ +typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect); + + +/** + ASTextBackedString objects are used by the NSAttributedString class cluster + as the values for text backed string attributes (stored in the attributed + string under the key named ASTextBackedStringAttributeName). + + It may used for copy/paste plain text from attributed string. + Example: If :) is replace by a custom emoji (such as😊), the backed string can be set to @":)". + */ +@interface ASTextBackedString : NSObject ++ (instancetype)stringWithString:(nullable NSString *)string NS_RETURNS_RETAINED; +@property (nullable, nonatomic, copy) NSString *string; ///< backed string +@end + + +/** + ASTextBinding objects are used by the NSAttributedString class cluster + as the values for shadow attributes (stored in the attributed string under + the key named ASTextBindingAttributeName). + + Add this to a range of text will make the specified characters 'binding together'. + ASTextView will treat the range of text as a single character during text + selection and edit. + */ +@interface ASTextBinding : NSObject ++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm NS_RETURNS_RETAINED; +@property (nonatomic) BOOL deleteConfirm; ///< confirm the range when delete in ASTextView +@end + + +/** + ASTextShadow objects are used by the NSAttributedString class cluster + as the values for shadow attributes (stored in the attributed string under + the key named ASTextShadowAttributeName or ASTextInnerShadowAttributeName). + + It's similar to `NSShadow`, but offers more options. + */ +@interface ASTextShadow : NSObject ++ (instancetype)shadowWithColor:(nullable UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius NS_RETURNS_RETAINED; + +@property (nullable, nonatomic) UIColor *color; ///< shadow color +@property (nonatomic) CGSize offset; ///< shadow offset +@property (nonatomic) CGFloat radius; ///< shadow blur radius +@property (nonatomic) CGBlendMode blendMode; ///< shadow blend mode +@property (nullable, nonatomic) ASTextShadow *subShadow; ///< a sub shadow which will be added above the parent shadow + ++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow NS_RETURNS_RETAINED; ///< convert NSShadow to ASTextShadow +- (NSShadow *)nsShadow; ///< convert ASTextShadow to NSShadow +@end + + +/** + ASTextDecorationLine objects are used by the NSAttributedString class cluster + as the values for decoration line attributes (stored in the attributed string under + the key named ASTextUnderlineAttributeName or ASTextStrikethroughAttributeName). + + When it's used as underline, the line is drawn below text glyphs; + when it's used as strikethrough, the line is drawn above text glyphs. + */ +@interface ASTextDecoration : NSObject ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style NS_RETURNS_RETAINED; ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(nullable NSNumber *)width color:(nullable UIColor *)color NS_RETURNS_RETAINED; +@property (nonatomic) ASTextLineStyle style; ///< line style +@property (nullable, nonatomic) NSNumber *width; ///< line width (nil means automatic width) +@property (nullable, nonatomic) UIColor *color; ///< line color (nil means automatic color) +@property (nullable, nonatomic) ASTextShadow *shadow; ///< line shadow +@end + + +/** + ASTextBorder objects are used by the NSAttributedString class cluster + as the values for border attributes (stored in the attributed string under + the key named ASTextBorderAttributeName or ASTextBackgroundBorderAttributeName). + + It can be used to draw a border around a range of text, or draw a background + to a range of text. + + Example: + ╭──────╮ + │ Text │ + ╰──────╯ + */ +@interface ASTextBorder : NSObject ++ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color NS_RETURNS_RETAINED; ++ (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius NS_RETURNS_RETAINED; +@property (nonatomic) ASTextLineStyle lineStyle; ///< border line style +@property (nonatomic) CGFloat strokeWidth; ///< border line width +@property (nullable, nonatomic) UIColor *strokeColor; ///< border line color +@property (nonatomic) CGLineJoin lineJoin; ///< border line join +@property (nonatomic) UIEdgeInsets insets; ///< border insets for text bounds +@property (nonatomic) CGFloat cornerRadius; ///< border corder radius +@property (nullable, nonatomic) ASTextShadow *shadow; ///< border shadow +@property (nullable, nonatomic) UIColor *fillColor; ///< inner fill color +@end + + +/** + ASTextAttachment objects are used by the NSAttributedString class cluster + as the values for attachment attributes (stored in the attributed string under + the key named ASTextAttachmentAttributeName). + + When display an attributed string which contains `ASTextAttachment` object, + the content will be placed in text metric. If the content is `UIImage`, + then it will be drawn to CGContext; if the content is `UIView` or `CALayer`, + then it will be added to the text container's view or layer. + */ +@interface ASTextAttachment : NSObject ++ (instancetype)attachmentWithContent:(nullable id)content NS_RETURNS_RETAINED; +@property (nullable, nonatomic) id content; ///< Supported type: UIImage, UIView, CALayer +@property (nonatomic) UIViewContentMode contentMode; ///< Content display mode. +@property (nonatomic) UIEdgeInsets contentInsets; ///< The insets when drawing content. +@property (nullable, nonatomic) NSDictionary *userInfo; ///< The user information dictionary. +@end + + +/** + ASTextHighlight objects are used by the NSAttributedString class cluster + as the values for touchable highlight attributes (stored in the attributed string + under the key named ASTextHighlightAttributeName). + + When display an attributed string in `ASLabel` or `ASTextView`, the range of + highlight text can be toucheds down by users. If a range of text is turned into + highlighted state, the `attributes` in `ASTextHighlight` will be used to modify + (set or remove) the original attributes in the range for display. + */ +@interface ASTextHighlight : NSObject + +/** + Attributes that you can apply to text in an attributed string when highlight. + Key: Same as CoreText/ASText Attribute Name. + Value: Modify attribute value when highlight (NSNull for remove attribute). + */ +@property (nullable, nonatomic, copy) NSDictionary *attributes; + +/** + Creates a highlight object with specified attributes. + + @param attributes The attributes which will replace original attributes when highlight, + If the value is NSNull, it will removed when highlight. + */ ++ (instancetype)highlightWithAttributes:(nullable NSDictionary *)attributes NS_RETURNS_RETAINED; + +/** + Convenience methods to create a default highlight with the specifeid background color. + + @param color The background border color. + */ ++ (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color NS_RETURNS_RETAINED; + +// Convenience methods below to set the `attributes`. +- (void)setFont:(nullable UIFont *)font; +- (void)setColor:(nullable UIColor *)color; +- (void)setStrokeWidth:(nullable NSNumber *)width; +- (void)setStrokeColor:(nullable UIColor *)color; +- (void)setShadow:(nullable ASTextShadow *)shadow; +- (void)setInnerShadow:(nullable ASTextShadow *)shadow; +- (void)setUnderline:(nullable ASTextDecoration *)underline; +- (void)setStrikethrough:(nullable ASTextDecoration *)strikethrough; +- (void)setBackgroundBorder:(nullable ASTextBorder *)border; +- (void)setBorder:(nullable ASTextBorder *)border; +- (void)setAttachment:(nullable ASTextAttachment *)attachment; + +/** + The user information dictionary, default is nil. + */ +@property (nullable, nonatomic, copy) NSDictionary *userInfo; + +/** + Tap action when user tap the highlight, default is nil. + If the value is nil, ASTextView or ASLabel will ask it's delegate to handle the tap action. + */ +@property (nullable, nonatomic) ASTextAction tapAction; + +/** + Long press action when user long press the highlight, default is nil. + If the value is nil, ASTextView or ASLabel will ask it's delegate to handle the long press action. + */ +@property (nullable, nonatomic) ASTextAction longPressAction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextAttribute.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextAttribute.mm new file mode 100644 index 0000000000..d1abadbe1f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextAttribute.mm @@ -0,0 +1,488 @@ +// +// ASTextAttribute.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTextAttribute.h" +#import +#import +#import + +NSString *const ASTextBackedStringAttributeName = @"ASTextBackedString"; +NSString *const ASTextBindingAttributeName = @"ASTextBinding"; +NSString *const ASTextShadowAttributeName = @"ASTextShadow"; +NSString *const ASTextInnerShadowAttributeName = @"ASTextInnerShadow"; +NSString *const ASTextUnderlineAttributeName = @"ASTextUnderline"; +NSString *const ASTextStrikethroughAttributeName = @"ASTextStrikethrough"; +NSString *const ASTextBorderAttributeName = @"ASTextBorder"; +NSString *const ASTextBackgroundBorderAttributeName = @"ASTextBackgroundBorder"; +NSString *const ASTextBlockBorderAttributeName = @"ASTextBlockBorder"; +NSString *const ASTextAttachmentAttributeName = @"ASTextAttachment"; +NSString *const ASTextHighlightAttributeName = @"ASTextHighlight"; +NSString *const ASTextGlyphTransformAttributeName = @"ASTextGlyphTransform"; + +NSString *const ASTextAttachmentToken = @"\uFFFC"; +NSString *const ASTextTruncationToken = @"\u2026"; + + +ASTextAttributeType ASTextAttributeGetType(NSString *name){ + if (name.length == 0) return ASTextAttributeTypeNone; + + static NSMutableDictionary *dic; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dic = [NSMutableDictionary new]; + NSNumber *All = @(ASTextAttributeTypeUIKit | ASTextAttributeTypeCoreText | ASTextAttributeTypeASText); + NSNumber *CoreText_ASText = @(ASTextAttributeTypeCoreText | ASTextAttributeTypeASText); + NSNumber *UIKit_ASText = @(ASTextAttributeTypeUIKit | ASTextAttributeTypeASText); + NSNumber *UIKit_CoreText = @(ASTextAttributeTypeUIKit | ASTextAttributeTypeCoreText); + NSNumber *UIKit = @(ASTextAttributeTypeUIKit); + NSNumber *CoreText = @(ASTextAttributeTypeCoreText); + NSNumber *ASText = @(ASTextAttributeTypeASText); + + dic[NSFontAttributeName] = All; + dic[NSKernAttributeName] = All; + dic[NSForegroundColorAttributeName] = UIKit; + dic[(id)kCTForegroundColorAttributeName] = CoreText; + dic[(id)kCTForegroundColorFromContextAttributeName] = CoreText; + dic[NSBackgroundColorAttributeName] = UIKit; + dic[NSStrokeWidthAttributeName] = All; + dic[NSStrokeColorAttributeName] = UIKit; + dic[(id)kCTStrokeColorAttributeName] = CoreText_ASText; + dic[NSShadowAttributeName] = UIKit_ASText; + dic[NSStrikethroughStyleAttributeName] = UIKit; + dic[NSUnderlineStyleAttributeName] = UIKit_CoreText; + dic[(id)kCTUnderlineColorAttributeName] = CoreText; + dic[NSLigatureAttributeName] = All; + dic[(id)kCTSuperscriptAttributeName] = UIKit; //it's a CoreText attrubite, but only supported by UIKit... + dic[NSVerticalGlyphFormAttributeName] = All; + dic[(id)kCTGlyphInfoAttributeName] = CoreText_ASText; +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + dic[(id)kCTCharacterShapeAttributeName] = CoreText_ASText; +#pragma clang diagnostic pop +#endif + dic[(id)kCTRunDelegateAttributeName] = CoreText_ASText; + dic[(id)kCTBaselineClassAttributeName] = CoreText_ASText; + dic[(id)kCTBaselineInfoAttributeName] = CoreText_ASText; + dic[(id)kCTBaselineReferenceInfoAttributeName] = CoreText_ASText; + dic[(id)kCTWritingDirectionAttributeName] = CoreText_ASText; + dic[NSParagraphStyleAttributeName] = All; + + dic[NSStrikethroughColorAttributeName] = UIKit; + dic[NSUnderlineColorAttributeName] = UIKit; + dic[NSTextEffectAttributeName] = UIKit; + dic[NSObliquenessAttributeName] = UIKit; + dic[NSExpansionAttributeName] = UIKit; + dic[(id)kCTLanguageAttributeName] = CoreText_ASText; + dic[NSBaselineOffsetAttributeName] = UIKit; + dic[NSWritingDirectionAttributeName] = All; + dic[NSAttachmentAttributeName] = UIKit; + dic[NSLinkAttributeName] = UIKit; + dic[(id)kCTRubyAnnotationAttributeName] = CoreText; + + dic[ASTextBackedStringAttributeName] = ASText; + dic[ASTextBindingAttributeName] = ASText; + dic[ASTextShadowAttributeName] = ASText; + dic[ASTextInnerShadowAttributeName] = ASText; + dic[ASTextUnderlineAttributeName] = ASText; + dic[ASTextStrikethroughAttributeName] = ASText; + dic[ASTextBorderAttributeName] = ASText; + dic[ASTextBackgroundBorderAttributeName] = ASText; + dic[ASTextBlockBorderAttributeName] = ASText; + dic[ASTextAttachmentAttributeName] = ASText; + dic[ASTextHighlightAttributeName] = ASText; + dic[ASTextGlyphTransformAttributeName] = ASText; + }); + NSNumber *num = dic[name]; + if (num) return num.integerValue; + return ASTextAttributeTypeNone; +} + + +@implementation ASTextBackedString + ++ (instancetype)stringWithString:(NSString *)string NS_RETURNS_RETAINED { + ASTextBackedString *one = [self new]; + one.string = string; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.string forKey:@"string"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _string = [aDecoder decodeObjectForKey:@"string"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.string = self.string; + return one; +} + +@end + + +@implementation ASTextBinding + ++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm NS_RETURNS_RETAINED { + ASTextBinding *one = [self new]; + one.deleteConfirm = deleteConfirm; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(self.deleteConfirm) forKey:@"deleteConfirm"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _deleteConfirm = ((NSNumber *)[aDecoder decodeObjectForKey:@"deleteConfirm"]).boolValue; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.deleteConfirm = self.deleteConfirm; + return one; +} + +@end + + +@implementation ASTextShadow + ++ (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius NS_RETURNS_RETAINED { + ASTextShadow *one = [self new]; + one.color = color; + one.offset = offset; + one.radius = radius; + return one; +} + ++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow NS_RETURNS_RETAINED { + if (!nsShadow) return nil; + ASTextShadow *shadow = [self new]; + shadow.offset = nsShadow.shadowOffset; + shadow.radius = nsShadow.shadowBlurRadius; + id color = nsShadow.shadowColor; + if (color) { + if (CGColorGetTypeID() == CFGetTypeID((__bridge CFTypeRef)(color))) { + color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)]; + } + if ([color isKindOfClass:[UIColor class]]) { + shadow.color = color; + } + } + return shadow; +} + +- (NSShadow *)nsShadow { + NSShadow *shadow = [NSShadow new]; + shadow.shadowOffset = self.offset; + shadow.shadowBlurRadius = self.radius; + shadow.shadowColor = self.color; + return shadow; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.color forKey:@"color"]; + [aCoder encodeObject:@(self.radius) forKey:@"radius"]; + [aCoder encodeObject:[NSValue valueWithCGSize:self.offset] forKey:@"offset"]; + [aCoder encodeObject:self.subShadow forKey:@"subShadow"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _color = [aDecoder decodeObjectForKey:@"color"]; + _radius = ((NSNumber *)[aDecoder decodeObjectForKey:@"radius"]).floatValue; + _offset = ((NSValue *)[aDecoder decodeObjectForKey:@"offset"]).CGSizeValue; + _subShadow = [aDecoder decodeObjectForKey:@"subShadow"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.color = self.color; + one.radius = self.radius; + one.offset = self.offset; + one.subShadow = self.subShadow.copy; + return one; +} + +@end + + +@implementation ASTextDecoration + +- (instancetype)init { + self = [super init]; + _style = ASTextLineStyleSingle; + return self; +} + ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style NS_RETURNS_RETAINED { + ASTextDecoration *one = [self new]; + one.style = style; + return one; +} ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(NSNumber *)width color:(UIColor *)color NS_RETURNS_RETAINED { + ASTextDecoration *one = [self new]; + one.style = style; + one.width = width; + one.color = color; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(self.style) forKey:@"style"]; + [aCoder encodeObject:self.width forKey:@"width"]; + [aCoder encodeObject:self.color forKey:@"color"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + self.style = ((NSNumber *)[aDecoder decodeObjectForKey:@"style"]).unsignedIntegerValue; + self.width = [aDecoder decodeObjectForKey:@"width"]; + self.color = [aDecoder decodeObjectForKey:@"color"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.style = self.style; + one.width = self.width; + one.color = self.color; + return one; +} + +@end + + +@implementation ASTextBorder + ++ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(UIColor *)color NS_RETURNS_RETAINED { + ASTextBorder *one = [self new]; + one.lineStyle = lineStyle; + one.strokeWidth = width; + one.strokeColor = color; + return one; +} + ++ (instancetype)borderWithFillColor:(UIColor *)color cornerRadius:(CGFloat)cornerRadius NS_RETURNS_RETAINED { + ASTextBorder *one = [self new]; + one.fillColor = color; + one.cornerRadius = cornerRadius; + one.insets = UIEdgeInsetsMake(-2, 0, 0, -2); + return one; +} + +- (instancetype)init { + self = [super init]; + self.lineStyle = ASTextLineStyleSingle; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(self.lineStyle) forKey:@"lineStyle"]; + [aCoder encodeObject:@(self.strokeWidth) forKey:@"strokeWidth"]; + [aCoder encodeObject:self.strokeColor forKey:@"strokeColor"]; + [aCoder encodeObject:@(self.lineJoin) forKey:@"lineJoin"]; + [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:self.insets] forKey:@"insets"]; + [aCoder encodeObject:@(self.cornerRadius) forKey:@"cornerRadius"]; + [aCoder encodeObject:self.shadow forKey:@"shadow"]; + [aCoder encodeObject:self.fillColor forKey:@"fillColor"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _lineStyle = ((NSNumber *)[aDecoder decodeObjectForKey:@"lineStyle"]).unsignedIntegerValue; + _strokeWidth = ((NSNumber *)[aDecoder decodeObjectForKey:@"strokeWidth"]).doubleValue; + _strokeColor = [aDecoder decodeObjectForKey:@"strokeColor"]; + _lineJoin = (CGLineJoin)((NSNumber *)[aDecoder decodeObjectForKey:@"join"]).unsignedIntegerValue; + _insets = ((NSValue *)[aDecoder decodeObjectForKey:@"insets"]).UIEdgeInsetsValue; + _cornerRadius = ((NSNumber *)[aDecoder decodeObjectForKey:@"cornerRadius"]).doubleValue; + _shadow = [aDecoder decodeObjectForKey:@"shadow"]; + _fillColor = [aDecoder decodeObjectForKey:@"fillColor"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.lineStyle = self.lineStyle; + one.strokeWidth = self.strokeWidth; + one.strokeColor = self.strokeColor; + one.lineJoin = self.lineJoin; + one.insets = self.insets; + one.cornerRadius = self.cornerRadius; + one.shadow = self.shadow.copy; + one.fillColor = self.fillColor; + return one; +} + +@end + + +@implementation ASTextAttachment + ++ (instancetype)attachmentWithContent:(id)content NS_RETURNS_RETAINED { + ASTextAttachment *one = [self new]; + one.content = content; + return one; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.content forKey:@"content"]; + [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:self.contentInsets] forKey:@"contentInsets"]; + [aCoder encodeObject:self.userInfo forKey:@"userInfo"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _content = [aDecoder decodeObjectForKey:@"content"]; + _contentInsets = ((NSValue *)[aDecoder decodeObjectForKey:@"contentInsets"]).UIEdgeInsetsValue; + _userInfo = [aDecoder decodeObjectForKey:@"userInfo"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + if ([self.content respondsToSelector:@selector(copy)]) { + one.content = [self.content copy]; + } else { + one.content = self.content; + } + one.contentInsets = self.contentInsets; + one.userInfo = self.userInfo.copy; + return one; +} + +@end + + +@implementation ASTextHighlight + ++ (instancetype)highlightWithAttributes:(NSDictionary *)attributes NS_RETURNS_RETAINED { + ASTextHighlight *one = [self new]; + one.attributes = attributes; + return one; +} + ++ (instancetype)highlightWithBackgroundColor:(UIColor *)color NS_RETURNS_RETAINED { + ASTextBorder *highlightBorder = [ASTextBorder new]; + highlightBorder.insets = UIEdgeInsetsMake(-2, -1, -2, -1); + highlightBorder.cornerRadius = 3; + highlightBorder.fillColor = color; + + ASTextHighlight *one = [self new]; + [one setBackgroundBorder:highlightBorder]; + return one; +} + +- (void)setAttributes:(NSDictionary *)attributes { + _attributes = attributes.mutableCopy; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.attributes = self.attributes.mutableCopy; + return one; +} + +- (void)_makeMutableAttributes { + if (!_attributes) { + _attributes = [NSMutableDictionary new]; + } else if (![_attributes isKindOfClass:[NSMutableDictionary class]]) { + _attributes = _attributes.mutableCopy; + } +} + +- (void)setFont:(UIFont *)font { + [self _makeMutableAttributes]; + if (font == (id)[NSNull null] || font == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTFontAttributeName] = [NSNull null]; + } else { + CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL); + if (ctFont) { + ((NSMutableDictionary *)_attributes)[(id)kCTFontAttributeName] = (__bridge id)(ctFont); + CFRelease(ctFont); + } + } +} + +- (void)setColor:(UIColor *)color { + [self _makeMutableAttributes]; + if (color == (id)[NSNull null] || color == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTForegroundColorAttributeName] = [NSNull null]; + ((NSMutableDictionary *)_attributes)[NSForegroundColorAttributeName] = [NSNull null]; + } else { + ((NSMutableDictionary *)_attributes)[(id)kCTForegroundColorAttributeName] = (__bridge id)(color.CGColor); + ((NSMutableDictionary *)_attributes)[NSForegroundColorAttributeName] = color; + } +} + +- (void)setStrokeWidth:(NSNumber *)width { + [self _makeMutableAttributes]; + if (width == (id)[NSNull null] || width == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeWidthAttributeName] = [NSNull null]; + } else { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeWidthAttributeName] = width; + } +} + +- (void)setStrokeColor:(UIColor *)color { + [self _makeMutableAttributes]; + if (color == (id)[NSNull null] || color == nil) { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeColorAttributeName] = [NSNull null]; + ((NSMutableDictionary *)_attributes)[NSStrokeColorAttributeName] = [NSNull null]; + } else { + ((NSMutableDictionary *)_attributes)[(id)kCTStrokeColorAttributeName] = (__bridge id)(color.CGColor); + ((NSMutableDictionary *)_attributes)[NSStrokeColorAttributeName] = color; + } +} + +- (void)setTextAttribute:(NSString *)attribute value:(id)value { + [self _makeMutableAttributes]; + if (value == nil) value = [NSNull null]; + ((NSMutableDictionary *)_attributes)[attribute] = value; +} + +- (void)setShadow:(ASTextShadow *)shadow { + [self setTextAttribute:ASTextShadowAttributeName value:shadow]; +} + +- (void)setInnerShadow:(ASTextShadow *)shadow { + [self setTextAttribute:ASTextInnerShadowAttributeName value:shadow]; +} + +- (void)setUnderline:(ASTextDecoration *)underline { + [self setTextAttribute:ASTextUnderlineAttributeName value:underline]; +} + +- (void)setStrikethrough:(ASTextDecoration *)strikethrough { + [self setTextAttribute:ASTextStrikethroughAttributeName value:strikethrough]; +} + +- (void)setBackgroundBorder:(ASTextBorder *)border { + [self setTextAttribute:ASTextBackgroundBorderAttributeName value:border]; +} + +- (void)setBorder:(ASTextBorder *)border { + [self setTextAttribute:ASTextBorderAttributeName value:border]; +} + +- (void)setAttachment:(ASTextAttachment *)attachment { + [self setTextAttribute:ASTextAttachmentAttributeName value:attachment]; +} + +@end + diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextRunDelegate.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextRunDelegate.h new file mode 100644 index 0000000000..3d3bf11c2c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextRunDelegate.h @@ -0,0 +1,65 @@ +// +// ASTextRunDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Wrapper for CTRunDelegateRef. + + Example: + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.ascent = 20; + delegate.descent = 4; + delegate.width = 20; + CTRunDelegateRef ctRunDelegate = delegate.CTRunDelegate; + if (ctRunDelegate) { + /// add to attributed string + CFRelease(ctRunDelegate); + } + + */ +@interface ASTextRunDelegate : NSObject + +/** + Creates and returns the CTRunDelegate. + + @discussion You need call CFRelease() after used. + The CTRunDelegateRef has a strong reference to this ASTextRunDelegate object. + In CoreText, use CTRunDelegateGetRefCon() to get this ASTextRunDelegate object. + + @return The CTRunDelegate object. + */ +- (nullable CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED; + +/** + Additional information about the the run delegate. + */ +@property (nullable, nonatomic) NSDictionary *userInfo; + +/** + The typographic ascent of glyphs in the run. + */ +@property (nonatomic) CGFloat ascent; + +/** + The typographic descent of glyphs in the run. + */ +@property (nonatomic) CGFloat descent; + +/** + The typographic width of glyphs in the run. + */ +@property (nonatomic) CGFloat width; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextRunDelegate.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextRunDelegate.mm new file mode 100644 index 0000000000..1c179b1fea --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/String/ASTextRunDelegate.mm @@ -0,0 +1,68 @@ +// +// ASTextRunDelegate.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +static void DeallocCallback(void *ref) { + ASTextRunDelegate *self = (__bridge_transfer ASTextRunDelegate *)(ref); + self = nil; // release +} + +static CGFloat GetAscentCallback(void *ref) { + ASTextRunDelegate *self = (__bridge ASTextRunDelegate *)(ref); + return self.ascent; +} + +static CGFloat GetDecentCallback(void *ref) { + ASTextRunDelegate *self = (__bridge ASTextRunDelegate *)(ref); + return self.descent; +} + +static CGFloat GetWidthCallback(void *ref) { + ASTextRunDelegate *self = (__bridge ASTextRunDelegate *)(ref); + return self.width; +} + +@implementation ASTextRunDelegate + +- (CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED { + CTRunDelegateCallbacks callbacks; + callbacks.version = kCTRunDelegateCurrentVersion; + callbacks.dealloc = DeallocCallback; + callbacks.getAscent = GetAscentCallback; + callbacks.getDescent = GetDecentCallback; + callbacks.getWidth = GetWidthCallback; + return CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy)); +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:@(_ascent) forKey:@"ascent"]; + [aCoder encodeObject:@(_descent) forKey:@"descent"]; + [aCoder encodeObject:@(_width) forKey:@"width"]; + [aCoder encodeObject:_userInfo forKey:@"userInfo"]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + _ascent = ((NSNumber *)[aDecoder decodeObjectForKey:@"ascent"]).floatValue; + _descent = ((NSNumber *)[aDecoder decodeObjectForKey:@"descent"]).floatValue; + _width = ((NSNumber *)[aDecoder decodeObjectForKey:@"width"]).floatValue; + _userInfo = [aDecoder decodeObjectForKey:@"userInfo"]; + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + __typeof__(self) one = [self.class new]; + one.ascent = self.ascent; + one.descent = self.descent; + one.width = self.width; + one.userInfo = self.userInfo; + return one; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/ASTextUtilities.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/ASTextUtilities.h new file mode 100644 index 0000000000..f21a931ba8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/ASTextUtilities.h @@ -0,0 +1,316 @@ +// +// ASTextUtilities.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import + + +#ifndef ASTEXT_CLAMP // return the clamped value +#define ASTEXT_CLAMP(_x_, _low_, _high_) (((_x_) > (_high_)) ? (_high_) : (((_x_) < (_low_)) ? (_low_) : (_x_))) +#endif + +#ifndef ASTEXT_SWAP // swap two value +#define ASTEXT_SWAP(_a_, _b_) do { __typeof__(_a_) _tmp_ = (_a_); (_a_) = (_b_); (_b_) = _tmp_; } while (0) +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** + Whether the character is 'line break char': + U+000D (\\r or CR) + U+2028 (Unicode line separator) + U+000A (\\n or LF) + U+2029 (Unicode paragraph separator) + + @param c A character + @return YES or NO. + */ +static inline BOOL ASTextIsLinebreakChar(unichar c) { + switch (c) { + case 0x000D: + case 0x2028: + case 0x000A: + case 0x2029: + return YES; + default: + return NO; + } +} + +/** + Whether the string is a 'line break': + U+000D (\\r or CR) + U+2028 (Unicode line separator) + U+000A (\\n or LF) + U+2029 (Unicode paragraph separator) + \\r\\n, in that order (also known as CRLF) + + @param str A string + @return YES or NO. + */ +static inline BOOL ASTextIsLinebreakString(NSString * _Nullable str) { + if (str.length > 2 || str.length == 0) return NO; + if (str.length == 1) { + unichar c = [str characterAtIndex:0]; + return ASTextIsLinebreakChar(c); + } else { + return ([str characterAtIndex:0] == '\r') && ([str characterAtIndex:1] == '\n'); + } +} + +/** + If the string has a 'line break' suffix, return the 'line break' length. + + @param str A string. + @return The length of the tail line break: 0, 1 or 2. + */ +static inline NSUInteger ASTextLinebreakTailLength(NSString * _Nullable str) { + if (str.length >= 2) { + unichar c2 = [str characterAtIndex:str.length - 1]; + if (ASTextIsLinebreakChar(c2)) { + unichar c1 = [str characterAtIndex:str.length - 2]; + if (c1 == '\r' && c2 == '\n') return 2; + else return 1; + } else { + return 0; + } + } else if (str.length == 1) { + return ASTextIsLinebreakChar([str characterAtIndex:0]) ? 1 : 0; + } else { + return 0; + } +} + +/** + Whether the font contains color bitmap glyphs. + + @discussion Only `AppleColorEmoji` contains color bitmap glyphs in iOS system fonts. + @param font A font. + @return YES: the font contains color bitmap glyphs, NO: the font has no color bitmap glyph. + */ +static inline BOOL ASTextCTFontContainsColorBitmapGlyphs(CTFontRef font) { + return (CTFontGetSymbolicTraits(font) & kCTFontTraitColorGlyphs) != 0; +} + +/** + Get the `AppleColorEmoji` font's ascent with a specified font size. + It may used to create custom emoji. + + @param fontSize The specified font size. + @return The font ascent. + */ +static inline CGFloat ASTextEmojiGetAscentWithFontSize(CGFloat fontSize) { + if (fontSize < 16) { + return 1.25 * fontSize; + } else if (16 <= fontSize && fontSize <= 24) { + return 0.5 * fontSize + 12; + } else { + return fontSize; + } +} + +/** + Get the `AppleColorEmoji` font's descent with a specified font size. + It may used to create custom emoji. + + @param fontSize The specified font size. + @return The font descent. + */ +static inline CGFloat ASTextEmojiGetDescentWithFontSize(CGFloat fontSize) { + if (fontSize < 16) { + return 0.390625 * fontSize; + } else if (16 <= fontSize && fontSize <= 24) { + return 0.15625 * fontSize + 3.75; + } else { + return 0.3125 * fontSize; + } + return 0; +} + +/** + Get the `AppleColorEmoji` font's glyph bounding rect with a specified font size. + It may used to create custom emoji. + + @param fontSize The specified font size. + @return The font glyph bounding rect. + */ +static inline CGRect ASTextEmojiGetGlyphBoundingRectWithFontSize(CGFloat fontSize) { + CGRect rect; + rect.origin.x = 0.75; + rect.size.width = rect.size.height = ASTextEmojiGetAscentWithFontSize(fontSize); + if (fontSize < 16) { + rect.origin.y = -0.2525 * fontSize; + } else if (16 <= fontSize && fontSize <= 24) { + rect.origin.y = 0.1225 * fontSize -6; + } else { + rect.origin.y = -0.1275 * fontSize; + } + return rect; +} + + +/** + Get the character set which should rotate in vertical form. + @return The shared character set. + */ +NSCharacterSet *ASTextVerticalFormRotateCharacterSet(void); + +/** + Get the character set which should rotate and move in vertical form. + @return The shared character set. + */ +NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet(void); + + +/// Get the transform rotation. +/// @return the rotation in radians [-PI,PI] ([-180°,180°]) +static inline CGFloat ASTextCGAffineTransformGetRotation(CGAffineTransform transform) { + return atan2(transform.b, transform.a); +} + +/// Negates/inverts a UIEdgeInsets. +static inline UIEdgeInsets ASTextUIEdgeInsetsInvert(UIEdgeInsets insets) { + return UIEdgeInsetsMake(-insets.top, -insets.left, -insets.bottom, -insets.right); +} + +/** + Returns a rectangle to fit `rect` with specified content mode. + + @param rect The constraint rect + @param size The content size + @param mode The content mode + @return A rectangle for the given content mode. + @discussion UIViewContentModeRedraw is same as UIViewContentModeScaleToFill. + */ +CGRect ASTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode); + +/// Returns the center for the rectangle. +static inline CGPoint ASTextCGRectGetCenter(CGRect rect) { + return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); +} + +/// Returns the area of the rectangle. +static inline CGFloat ASTextCGRectGetArea(CGRect rect) { + if (CGRectIsNull(rect)) return 0; + rect = CGRectStandardize(rect); + return rect.size.width * rect.size.height; +} + +/// Returns the minmium distance between a point to a rectangle. +static inline CGFloat ASTextCGPointGetDistanceToRect(CGPoint p, CGRect r) { + r = CGRectStandardize(r); + if (CGRectContainsPoint(r, p)) return 0; + CGFloat distV, distH; + if (CGRectGetMinY(r) <= p.y && p.y <= CGRectGetMaxY(r)) { + distV = 0; + } else { + distV = p.y < CGRectGetMinY(r) ? CGRectGetMinY(r) - p.y : p.y - CGRectGetMaxY(r); + } + if (CGRectGetMinX(r) <= p.x && p.x <= CGRectGetMaxX(r)) { + distH = 0; + } else { + distH = p.x < CGRectGetMinX(r) ? CGRectGetMinX(r) - p.x : p.x - CGRectGetMaxX(r); + } + return MAX(distV, distH); +} + +/// Convert point to pixel. +static inline CGFloat ASTextCGFloatToPixel(CGFloat value) { + return value * ASScreenScale(); +} + +/// Convert pixel to point. +static inline CGFloat ASTextCGFloatFromPixel(CGFloat value) { + return value / ASScreenScale(); +} + +/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned) +static inline CGFloat ASTextCGFloatPixelHalf(CGFloat value) { + CGFloat scale = ASScreenScale(); + return (floor(value * scale) + 0.5) / scale; +} + +/// floor point value for pixel-aligned +static inline CGPoint ASTextCGPointPixelFloor(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake(floor(point.x * scale) / scale, + floor(point.y * scale) / scale); +} + +/// round point value for pixel-aligned +static inline CGPoint ASTextCGPointPixelRound(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake(round(point.x * scale) / scale, + round(point.y * scale) / scale); +} + +/// ceil point value for pixel-aligned +static inline CGPoint ASTextCGPointPixelCeil(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake(ceil(point.x * scale) / scale, + ceil(point.y * scale) / scale); +} + +/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned) +static inline CGPoint ASTextCGPointPixelHalf(CGPoint point) { + CGFloat scale = ASScreenScale(); + return CGPointMake((floor(point.x * scale) + 0.5) / scale, + (floor(point.y * scale) + 0.5) / scale); +} + +/// round point value for pixel-aligned +static inline CGRect ASTextCGRectPixelRound(CGRect rect) { + CGPoint origin = ASTextCGPointPixelRound(rect.origin); + CGPoint corner = ASTextCGPointPixelRound(CGPointMake(rect.origin.x + rect.size.width, + rect.origin.y + rect.size.height)); + return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y); +} + +/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned) +static inline CGRect ASTextCGRectPixelHalf(CGRect rect) { + CGPoint origin = ASTextCGPointPixelHalf(rect.origin); + CGPoint corner = ASTextCGPointPixelHalf(CGPointMake(rect.origin.x + rect.size.width, + rect.origin.y + rect.size.height)); + return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y); +} + + +static inline UIFont * _Nullable ASTextFontWithBold(UIFont *font) { + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize]; +} + +static inline UIFont * _Nullable ASTextFontWithItalic(UIFont *font) { + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic] size:font.pointSize]; +} + +static inline UIFont * _Nullable ASTextFontWithBoldItalic(UIFont *font) { + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic] size:font.pointSize]; +} + + + +/** + Convert CFRange to NSRange + @param range CFRange @return NSRange + */ +static inline NSRange ASTextNSRangeFromCFRange(CFRange range) { + return NSMakeRange(range.location, range.length); +} + +/** + Convert NSRange to CFRange + @param range NSRange @return CFRange + */ +static inline CFRange ASTextCFRangeFromNSRange(NSRange range) { + return CFRangeMake(range.location, range.length); +} + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/ASTextUtilities.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/ASTextUtilities.mm new file mode 100644 index 0000000000..8d0137718b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/ASTextUtilities.mm @@ -0,0 +1,143 @@ +// +// ASTextUtilities.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTextUtilities.h" +#import + +NSCharacterSet *ASTextVerticalFormRotateCharacterSet() { + static NSMutableCharacterSet *set; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + set = [NSMutableCharacterSet new]; + [set addCharactersInRange:NSMakeRange(0x1100, 256)]; // Hangul Jamo + [set addCharactersInRange:NSMakeRange(0x2460, 160)]; // Enclosed Alphanumerics + [set addCharactersInRange:NSMakeRange(0x2600, 256)]; // Miscellaneous Symbols + [set addCharactersInRange:NSMakeRange(0x2700, 192)]; // Dingbats + [set addCharactersInRange:NSMakeRange(0x2E80, 128)]; // CJK Radicals Supplement + [set addCharactersInRange:NSMakeRange(0x2F00, 224)]; // Kangxi Radicals + [set addCharactersInRange:NSMakeRange(0x2FF0, 16)]; // Ideographic Description Characters + [set addCharactersInRange:NSMakeRange(0x3000, 64)]; // CJK Symbols and Punctuation + [set removeCharactersInRange:NSMakeRange(0x3008, 10)]; + [set removeCharactersInRange:NSMakeRange(0x3014, 12)]; + [set addCharactersInRange:NSMakeRange(0x3040, 96)]; // Hiragana + [set addCharactersInRange:NSMakeRange(0x30A0, 96)]; // Katakana + [set addCharactersInRange:NSMakeRange(0x3100, 48)]; // Bopomofo + [set addCharactersInRange:NSMakeRange(0x3130, 96)]; // Hangul Compatibility Jamo + [set addCharactersInRange:NSMakeRange(0x3190, 16)]; // Kanbun + [set addCharactersInRange:NSMakeRange(0x31A0, 32)]; // Bopomofo Extended + [set addCharactersInRange:NSMakeRange(0x31C0, 48)]; // CJK Strokes + [set addCharactersInRange:NSMakeRange(0x31F0, 16)]; // Katakana Phonetic Extensions + [set addCharactersInRange:NSMakeRange(0x3200, 256)]; // Enclosed CJK Letters and Months + [set addCharactersInRange:NSMakeRange(0x3300, 256)]; // CJK Compatibility + [set addCharactersInRange:NSMakeRange(0x3400, 2582)]; // CJK Unified Ideographs Extension A + [set addCharactersInRange:NSMakeRange(0x4E00, 20941)]; // CJK Unified Ideographs + [set addCharactersInRange:NSMakeRange(0xAC00, 11172)]; // Hangul Syllables + [set addCharactersInRange:NSMakeRange(0xD7B0, 80)]; // Hangul Jamo Extended-B + [set addCharactersInString:@""]; // U+F8FF (Private Use Area) + [set addCharactersInRange:NSMakeRange(0xF900, 512)]; // CJK Compatibility Ideographs + [set addCharactersInRange:NSMakeRange(0xFE10, 16)]; // Vertical Forms + [set addCharactersInRange:NSMakeRange(0xFF00, 240)]; // Halfwidth and Fullwidth Forms + [set addCharactersInRange:NSMakeRange(0x1F200, 256)]; // Enclosed Ideographic Supplement + [set addCharactersInRange:NSMakeRange(0x1F300, 768)]; // Enclosed Ideographic Supplement + [set addCharactersInRange:NSMakeRange(0x1F600, 80)]; // Emoticons (Emoji) + [set addCharactersInRange:NSMakeRange(0x1F680, 128)]; // Transport and Map Symbols + + // See http://unicode-table.com/ for more information. + }); + return set; +} + +NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet() { + static NSMutableCharacterSet *set; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + set = [NSMutableCharacterSet new]; + [set addCharactersInString:@",。、."]; + }); + return set; +} + +CGRect ASTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode) { + rect = CGRectStandardize(rect); + size.width = size.width < 0 ? -size.width : size.width; + size.height = size.height < 0 ? -size.height : size.height; + CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); + switch (mode) { + case UIViewContentModeScaleAspectFit: + case UIViewContentModeScaleAspectFill: { + if (rect.size.width < 0.01 || rect.size.height < 0.01 || + size.width < 0.01 || size.height < 0.01) { + rect.origin = center; + rect.size = CGSizeZero; + } else { + CGFloat scale; + if (mode == UIViewContentModeScaleAspectFit) { + if (size.width / size.height < rect.size.width / rect.size.height) { + scale = rect.size.height / size.height; + } else { + scale = rect.size.width / size.width; + } + } else { + if (size.width / size.height < rect.size.width / rect.size.height) { + scale = rect.size.width / size.width; + } else { + scale = rect.size.height / size.height; + } + } + size.width *= scale; + size.height *= scale; + rect.size = size; + rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); + } + } break; + case UIViewContentModeCenter: { + rect.size = size; + rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); + } break; + case UIViewContentModeTop: { + rect.origin.x = center.x - size.width * 0.5; + rect.size = size; + } break; + case UIViewContentModeBottom: { + rect.origin.x = center.x - size.width * 0.5; + rect.origin.y += rect.size.height - size.height; + rect.size = size; + } break; + case UIViewContentModeLeft: { + rect.origin.y = center.y - size.height * 0.5; + rect.size = size; + } break; + case UIViewContentModeRight: { + rect.origin.y = center.y - size.height * 0.5; + rect.origin.x += rect.size.width - size.width; + rect.size = size; + } break; + case UIViewContentModeTopLeft: { + rect.size = size; + } break; + case UIViewContentModeTopRight: { + rect.origin.x += rect.size.width - size.width; + rect.size = size; + } break; + case UIViewContentModeBottomLeft: { + rect.origin.y += rect.size.height - size.height; + rect.size = size; + } break; + case UIViewContentModeBottomRight: { + rect.origin.x += rect.size.width - size.width; + rect.origin.y += rect.size.height - size.height; + rect.size = size; + } break; + case UIViewContentModeScaleToFill: + case UIViewContentModeRedraw: + default: { + rect = rect; + } + } + return rect; +} diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h new file mode 100644 index 0000000000..ef44fb4f35 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h @@ -0,0 +1,1375 @@ +// +// NSAttributedString+ASText.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Get pre-defined attributes from attributed string. + All properties defined in UIKit, CoreText and ASText are included. + */ +@interface NSAttributedString (ASText) + +#pragma mark - Retrieving character attribute information +///============================================================================= +/// @name Retrieving character attribute information +///============================================================================= + +/** + Returns the attributes at first charactor. + */ +@property (nullable, nonatomic, copy, readonly) NSDictionary *as_attributes; + +/** + Returns the attributes for the character at a given index. + + @discussion Raises an `NSRangeException` if index lies beyond the end of the + receiver's characters. + + @param index The index for which to return attributes. + This value must lie within the bounds of the receiver. + + @return The attributes for the character at index. + */ +- (nullable NSDictionary *)as_attributesAtIndex:(NSUInteger)index; + +/** + Returns the value for an attribute with a given name of the character at a given index. + + @discussion Raises an `NSRangeException` if index lies beyond the end of the + receiver's characters. + + @param attributeName The name of an attribute. + @param index The index for which to return attributes. + This value must not exceed the bounds of the receiver. + + @return The value for the attribute named `attributeName` of the character at + index `index`, or nil if there is no such attribute. + */ +- (nullable id)as_attribute:(NSString *)attributeName atIndex:(NSUInteger)index; + + +#pragma mark - Get character attribute as property +///============================================================================= +/// @name Get character attribute as property +///============================================================================= + +/** + The font of the text. (read-only) + + @discussion Default is Helvetica (Neue) 12. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) UIFont *as_font; +- (nullable UIFont *)as_fontAtIndex:(NSUInteger)index; + +/** + A kerning adjustment. (read-only) + + @discussion Default is standard kerning. The kerning attribute indicate how many + points the following character should be shifted from its default offset as + defined by the current character's font in points; a positive kern indicates a + shift farther along and a negative kern indicates a shift closer to the current + character. If this attribute is not present, standard kerning will be used. + If this attribute is set to 0.0, no kerning will be done at all. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) NSNumber *as_kern; +- (nullable NSNumber *)as_kernAtIndex:(NSUInteger)index; + +/** + The foreground color. (read-only) + + @discussion Default is Black. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) UIColor *as_color; +- (nullable UIColor *)as_colorAtIndex:(NSUInteger)index; + +/** + The background color. (read-only) + + @discussion Default is nil (or no background). + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nullable, nonatomic, readonly) UIColor *as_backgroundColor; +- (nullable UIColor *)as_backgroundColorAtIndex:(NSUInteger)index; + +/** + The stroke width. (read-only) + + @discussion Default value is 0.0 (no stroke). This attribute, interpreted as + a percentage of font point size, controls the text drawing mode: positive + values effect drawing with stroke only; negative values are for stroke and fill. + A typical value for outlined text is 3.0. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nullable, nonatomic, readonly) NSNumber *as_strokeWidth; +- (nullable NSNumber *)as_strokeWidthAtIndex:(NSUInteger)index; + +/** + The stroke color. (read-only) + + @discussion Default value is nil (same as foreground color). + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nullable, nonatomic, readonly) UIColor *as_strokeColor; +- (nullable UIColor *)as_strokeColorAtIndex:(NSUInteger)index; + +/** + The text shadow. (read-only) + + @discussion Default value is nil (no shadow). + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) NSShadow *as_shadow; +- (nullable NSShadow *)as_shadowAtIndex:(NSUInteger)index; + +/** + The strikethrough style. (read-only) + + @discussion Default value is NSUnderlineStyleNone (no strikethrough). + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic, readonly) NSUnderlineStyle as_strikethroughStyle; +- (NSUnderlineStyle)as_strikethroughStyleAtIndex:(NSUInteger)index; + +/** + The strikethrough color. (read-only) + + @discussion Default value is nil (same as foreground color). + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, readonly) UIColor *as_strikethroughColor; +- (nullable UIColor *)as_strikethroughColorAtIndex:(NSUInteger)index; + +/** + The underline style. (read-only) + + @discussion Default value is NSUnderlineStyleNone (no underline). + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nonatomic, readonly) NSUnderlineStyle as_underlineStyle; +- (NSUnderlineStyle)as_underlineStyleAtIndex:(NSUInteger)index; + +/** + The underline color. (read-only) + + @discussion Default value is nil (same as foreground color). + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:7.0 + */ +@property (nullable, nonatomic, readonly) UIColor *as_underlineColor; +- (nullable UIColor *)as_underlineColorAtIndex:(NSUInteger)index; + +/** + Ligature formation control. (read-only) + + @discussion Default is int value 1. The ligature attribute determines what kinds + of ligatures should be used when displaying the string. A value of 0 indicates + that only ligatures essential for proper rendering of text should be used, + 1 indicates that standard ligatures should be used, and 2 indicates that all + available ligatures should be used. Which ligatures are standard depends on the + script and possibly the font. + @discussion Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) NSNumber *as_ligature; +- (nullable NSNumber *)as_ligatureAtIndex:(NSUInteger)index; + +/** + The text effect. (read-only) + + @discussion Default is nil (no effect). The only currently supported value + is NSTextEffectLetterpressStyle. + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, readonly) NSString *as_textEffect; +- (nullable NSString *)as_textEffectAtIndex:(NSUInteger)index; + +/** + The skew to be applied to glyphs. (read-only) + + @discussion Default is 0 (no skew). + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, readonly) NSNumber *as_obliqueness; +- (nullable NSNumber *)as_obliquenessAtIndex:(NSUInteger)index; + +/** + The log of the expansion factor to be applied to glyphs. (read-only) + + @discussion Default is 0 (no expansion). + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, readonly) NSNumber *as_expansion; +- (nullable NSNumber *)as_expansionAtIndex:(NSUInteger)index; + +/** + The character's offset from the baseline, in points. (read-only) + + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic, readonly) NSNumber *as_baselineOffset; +- (nullable NSNumber *)as_baselineOffsetAtIndex:(NSUInteger)index; + +/** + Glyph orientation control. (read-only) + + @discussion Default is NO. A value of NO indicates that horizontal glyph forms + are to be used, YES indicates that vertical glyph forms are to be used. + @discussion Get this property returns the first character's attribute. + @since CoreText:4.3 ASText:6.0 + */ +@property (nonatomic, readonly) BOOL as_verticalGlyphForm; +- (BOOL)as_verticalGlyphFormAtIndex:(NSUInteger)index; + +/** + Specifies text language. (read-only) + + @discussion Value must be a NSString containing a locale identifier. Default is + unset. When this attribute is set to a valid identifier, it will be used to select + localized glyphs (if supported by the font) and locale-specific line breaking rules. + @discussion Get this property returns the first character's attribute. + @since CoreText:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, readonly) NSString *as_language; +- (nullable NSString *)as_languageAtIndex:(NSUInteger)index; + +/** + Specifies a bidirectional override or embedding. (read-only) + + @discussion See alse NSWritingDirection and NSWritingDirectionAttributeName. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:7.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) NSArray *as_writingDirection; +- (nullable NSArray *)as_writingDirectionAtIndex:(NSUInteger)index; + +/** + An NSParagraphStyle object which is used to specify things like + line alignment, tab rulers, writing direction, etc. (read-only) + + @discussion Default is nil ([NSParagraphStyle defaultParagraphStyle]). + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic, readonly) NSParagraphStyle *as_paragraphStyle; +- (nullable NSParagraphStyle *)as_paragraphStyleAtIndex:(NSUInteger)index; + +#pragma mark - Get paragraph attribute as property +///============================================================================= +/// @name Get paragraph attribute as property +///============================================================================= + +/** + The text alignment (A wrapper for NSParagraphStyle). (read-only) + + @discussion Natural text alignment is realized as left or right alignment + depending on the line sweep direction of the first script contained in the paragraph. + @discussion Default is NSTextAlignmentNatural. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) NSTextAlignment as_alignment; +- (NSTextAlignment)as_alignmentAtIndex:(NSUInteger)index; + +/** + The mode that should be used to break lines (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is NSLineBreakByWordWrapping. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) NSLineBreakMode as_lineBreakMode; +- (NSLineBreakMode)as_lineBreakModeAtIndex:(NSUInteger)index; + +/** + The distance in points between the bottom of one line fragment and the top of the next. + (A wrapper for NSParagraphStyle) (read-only) + + @discussion This value is always nonnegative. This value is included in the line + fragment heights in the layout manager. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_lineSpacing; +- (CGFloat)as_lineSpacingAtIndex:(NSUInteger)index; + +/** + The space after the end of the paragraph (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the space (measured in points) added at the + end of the paragraph to separate it from the following paragraph. This value must + be nonnegative. The space between paragraphs is determined by adding the previous + paragraph's paragraphSpacing and the current paragraph's paragraphSpacingBefore. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_paragraphSpacing; +- (CGFloat)as_paragraphSpacingAtIndex:(NSUInteger)index; + +/** + The distance between the paragraph's top and the beginning of its text content. + (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the space (measured in points) between the + paragraph's top and the beginning of its text content. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_paragraphSpacingBefore; +- (CGFloat)as_paragraphSpacingBeforeAtIndex:(NSUInteger)index; + +/** + The indentation of the first line (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of the paragraph's first line. This value + is always nonnegative. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_firstLineHeadIndent; +- (CGFloat)as_firstLineHeadIndentAtIndex:(NSUInteger)index; + +/** + The indentation of the receiver's lines other than the first. (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of lines other than the first. This value is + always nonnegative. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_headIndent; +- (CGFloat)as_headIndentAtIndex:(NSUInteger)index; + +/** + The trailing indentation (A wrapper for NSParagraphStyle). (read-only) + + @discussion If positive, this value is the distance from the leading margin + (for example, the left margin in left-to-right text). If 0 or negative, it's the + distance from the trailing margin. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_tailIndent; +- (CGFloat)as_tailIndentAtIndex:(NSUInteger)index; + +/** + The receiver's minimum height (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the minimum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value must be nonnegative. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_minimumLineHeight; +- (CGFloat)as_minimumLineHeightAtIndex:(NSUInteger)index; + +/** + The receiver's maximum line height (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the maximum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value is always nonnegative. Glyphs and graphics exceeding this height will + overlap neighboring lines; however, a maximum height of 0 implies no line height limit. + Although this limit applies to the line itself, line spacing adds extra space between adjacent lines. + @discussion Default is 0 (no limit). + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_maximumLineHeight; +- (CGFloat)as_maximumLineHeightAtIndex:(NSUInteger)index; + +/** + The line height multiple (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is 0 (no multiple). + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) CGFloat as_lineHeightMultiple; +- (CGFloat)as_lineHeightMultipleAtIndex:(NSUInteger)index; + +/** + The base writing direction (A wrapper for NSParagraphStyle). (read-only) + + @discussion If you specify NSWritingDirectionNaturalDirection, the receiver resolves + the writing direction to either NSWritingDirectionLeftToRight or NSWritingDirectionRightToLeft, + depending on the direction for the user's `language` preference setting. + @discussion Default is NSWritingDirectionNatural. + @discussion Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic, readonly) NSWritingDirection as_baseWritingDirection; +- (NSWritingDirection)as_baseWritingDirectionAtIndex:(NSUInteger)index; + +/** + The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle). (read-only) + + @discussion Valid values lie between 0.0 and 1.0 inclusive. Hyphenation is attempted + when the ratio of the text width (as broken without hyphenation) to the width of the + line fragment is less than the hyphenation factor. When the paragraph's hyphenation + factor is 0.0, the layout manager's hyphenation factor is used instead. When both + are 0.0, hyphenation is disabled. + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic, readonly) float as_hyphenationFactor; +- (float)as_hyphenationFactorAtIndex:(NSUInteger)index; + +/** + The document-wide default tab interval (A wrapper for NSParagraphStyle). (read-only) + + @discussion This property represents the default tab interval in points. Tabs after the + last specified in tabStops are placed at integer multiples of this distance (if positive). + @discussion Default is 0. + @discussion Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nonatomic, readonly) CGFloat as_defaultTabInterval; +- (CGFloat)as_defaultTabIntervalAtIndex:(NSUInteger)index; + +/** + An array of NSTextTab objects representing the receiver's tab stops. + (A wrapper for NSParagraphStyle). (read-only) + + @discussion The NSTextTab objects, sorted by location, define the tab stops for + the paragraph style. + @discussion Default is 12 TabStops with 28.0 tab interval. + @discussion Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, copy, readonly) NSArray *as_tabStops; +- (nullable NSArray *)as_tabStopsAtIndex:(NSUInteger)index; + +#pragma mark - Get ASText attribute as property +///============================================================================= +/// @name Get ASText attribute as property +///============================================================================= + +/** + The text shadow. (read-only) + + @discussion Default value is nil (no shadow). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, readonly) ASTextShadow *as_textShadow; +- (nullable ASTextShadow *)as_textShadowAtIndex:(NSUInteger)index; + +/** + The text inner shadow. (read-only) + + @discussion Default value is nil (no shadow). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, readonly) ASTextShadow *as_textInnerShadow; +- (nullable ASTextShadow *)as_textInnerShadowAtIndex:(NSUInteger)index; + +/** + The text underline. (read-only) + + @discussion Default value is nil (no underline). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, readonly) ASTextDecoration *as_textUnderline; +- (nullable ASTextDecoration *)as_textUnderlineAtIndex:(NSUInteger)index; + +/** + The text strikethrough. (read-only) + + @discussion Default value is nil (no strikethrough). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, readonly) ASTextDecoration *as_textStrikethrough; +- (nullable ASTextDecoration *)as_textStrikethroughAtIndex:(NSUInteger)index; + +/** + The text border. (read-only) + + @discussion Default value is nil (no border). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, readonly) ASTextBorder *as_textBorder; +- (nullable ASTextBorder *)as_textBorderAtIndex:(NSUInteger)index; + +/** + The text background border. (read-only) + + @discussion Default value is nil (no background border). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic, readonly) ASTextBorder *as_textBackgroundBorder; +- (nullable ASTextBorder *)as_textBackgroundBorderAtIndex:(NSUInteger)index; + +/** + The glyph transform. (read-only) + + @discussion Default value is CGAffineTransformIdentity (no transform). + @discussion Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nonatomic, readonly) CGAffineTransform as_textGlyphTransform; +- (CGAffineTransform)as_textGlyphTransformAtIndex:(NSUInteger)index; + + +#pragma mark - Query for ASText +///============================================================================= +/// @name Query for ASText +///============================================================================= + +/** + Returns the plain text from a range. + If there's `ASTextBackedStringAttributeName` attribute, the backed string will + replace the attributed string range. + + @param range A range in receiver. + @return The plain text. + */ +- (nullable NSString *)as_plainTextForRange:(NSRange)range; + + +#pragma mark - Create attachment string for ASText +///============================================================================= +/// @name Create attachment string for ASText +///============================================================================= + +/** + Creates and returns an attachment. + + @param content The attachment (UIImage/UIView/CALayer). + @param contentMode The attachment's content mode. + @param width The attachment's container width in layout. + @param ascent The attachment's container ascent in layout. + @param descent The attachment's container descent in layout. + + @return An attributed string, or nil if an error occurs. + @since ASText:6.0 + */ ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(nullable id)content + contentMode:(UIViewContentMode)contentMode + width:(CGFloat)width + ascent:(CGFloat)ascent + descent:(CGFloat)descent; + +/** + Creates and returns an attachment. + + + Example: ContentMode:bottom Alignment:Top. + + The text The attachment holder + ↓ ↓ + ─────────┌──────────────────────┐─────── + / \ │ │ / ___| + / _ \ │ │| | + / ___ \ │ │| |___ ←── The text line + /_/ \_\│ ██████████████ │ \____| + ─────────│ ██████████████ │─────── + │ ██████████████ │ + │ ██████████████ ←───────────────── The attachment content + │ ██████████████ │ + └──────────────────────┘ + + @param content The attachment (UIImage/UIView/CALayer). + @param contentMode The attachment's content mode in attachment holder + @param attachmentSize The attachment holder's size in text layout. + @param font The attachment will align to this font. + @param alignment The attachment holder's alignment to text line. + + @return An attributed string, or nil if an error occurs. + @since ASText:6.0 + */ ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(nullable id)content + contentMode:(UIViewContentMode)contentMode + attachmentSize:(CGSize)attachmentSize + alignToFont:(UIFont *)font + alignment:(ASTextVerticalAlignment)alignment; + +/** + Creates and returns an attahment from a fourquare image as if it was an emoji. + + @param image A fourquare image. + @param fontSize The font size. + + @return An attributed string, or nil if an error occurs. + @since ASText:6.0 + */ ++ (nullable NSMutableAttributedString *)as_attachmentStringWithEmojiImage:(UIImage *)image + fontSize:(CGFloat)fontSize; + +#pragma mark - Utility +///============================================================================= +/// @name Utility +///============================================================================= + +/** + Returns NSMakeRange(0, self.length). + */ +- (NSRange)as_rangeOfAll; + +/** + If YES, it share the same attribute in entire text range. + */ +- (BOOL)as_isSharedAttributesInAllRange; + +/** + If YES, it can be drawn with the [drawWithRect:options:context:] method or displayed with UIKit. + If NO, it should be drawn with CoreText or ASText. + + @discussion If the method returns NO, it means that there's at least one attribute + which is not supported by UIKit (such as CTParagraphStyleRef). If display this string + in UIKit, it may lose some attribute, or even crash the app. + */ +- (BOOL)as_canDrawWithUIKit; + +@end + + + + +/** + Set pre-defined attributes to attributed string. + All properties defined in UIKit, CoreText and ASText are included. + */ +@interface NSMutableAttributedString (ASText) + +#pragma mark - Set character attribute +///============================================================================= +/// @name Set character attribute +///============================================================================= + +/** + Sets the attributes to the entire text string. + + @discussion The old attributes will be removed. + + @param attributes A dictionary containing the attributes to set, or nil to remove all attributes. + */ +- (void)as_setAttributes:(nullable NSDictionary *)attributes; +- (void)setAs_attributes:(nullable NSDictionary *)attributes; + +/** + Sets an attribute with the given name and value to the entire text string. + + @param name A string specifying the attribute name. + @param value The attribute value associated with name. Pass `nil` or `NSNull` to + remove the attribute. + */ +- (void)as_setAttribute:(NSString *)name value:(nullable id)value; + +/** + Sets an attribute with the given name and value to the characters in the specified range. + + @param name A string specifying the attribute name. + @param value The attribute value associated with name. Pass `nil` or `NSNull` to + remove the attribute. + @param range The range of characters to which the specified attribute/value pair applies. + */ +- (void)as_setAttribute:(NSString *)name value:(nullable id)value range:(NSRange)range; + +/** + Removes all attributes in the specified range. + + @param range The range of characters. + */ +- (void)as_removeAttributesInRange:(NSRange)range; + + +#pragma mark - Set character attribute as property +///============================================================================= +/// @name Set character attribute as property +///============================================================================= + +/** + The font of the text. + + @discussion Default is Helvetica (Neue) 12. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) UIFont *as_font; +- (void)as_setFont:(nullable UIFont *)font range:(NSRange)range; + +/** + A kerning adjustment. + + @discussion Default is standard kerning. The kerning attribute indicate how many + points the following character should be shifted from its default offset as + defined by the current character's font in points; a positive kern indicates a + shift farther along and a negative kern indicates a shift closer to the current + character. If this attribute is not present, standard kerning will be used. + If this attribute is set to 0.0, no kerning will be done at all. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) NSNumber *as_kern; +- (void)as_setKern:(nullable NSNumber *)kern range:(NSRange)range; + +/** + The foreground color. + + @discussion Default is Black. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) UIColor *as_color; +- (void)as_setColor:(nullable UIColor *)color range:(NSRange)range; + +/** + The background color. + + @discussion Default is nil (or no background). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nullable, nonatomic) UIColor *as_backgroundColor; +- (void)as_setBackgroundColor:(nullable UIColor *)backgroundColor range:(NSRange)range; + +/** + The stroke width. + + @discussion Default value is 0.0 (no stroke). This attribute, interpreted as + a percentage of font point size, controls the text drawing mode: positive + values effect drawing with stroke only; negative values are for stroke and fill. + A typical value for outlined text is 3.0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) NSNumber *as_strokeWidth; +- (void)as_setStrokeWidth:(nullable NSNumber *)strokeWidth range:(NSRange)range; + +/** + The stroke color. + + @discussion Default value is nil (same as foreground color). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) UIColor *as_strokeColor; +- (void)as_setStrokeColor:(nullable UIColor *)strokeColor range:(NSRange)range; + +/** + The text shadow. + + @discussion Default value is nil (no shadow). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) NSShadow *as_shadow; +- (void)as_setShadow:(nullable NSShadow *)shadow range:(NSRange)range; + +/** + The strikethrough style. + + @discussion Default value is NSUnderlineStyleNone (no strikethrough). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic) NSUnderlineStyle as_strikethroughStyle; +- (void)as_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range; + +/** + The strikethrough color. + + @discussion Default value is nil (same as foreground color). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic) UIColor *as_strikethroughColor; +- (void)as_setStrikethroughColor:(nullable UIColor *)strikethroughColor range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The underline style. + + @discussion Default value is NSUnderlineStyleNone (no underline). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 + */ +@property (nonatomic) NSUnderlineStyle as_underlineStyle; +- (void)as_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range; + +/** + The underline color. + + @discussion Default value is nil (same as foreground color). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:7.0 + */ +@property (nullable, nonatomic) UIColor *as_underlineColor; +- (void)as_setUnderlineColor:(nullable UIColor *)underlineColor range:(NSRange)range; + +/** + Ligature formation control. + + @discussion Default is int value 1. The ligature attribute determines what kinds + of ligatures should be used when displaying the string. A value of 0 indicates + that only ligatures essential for proper rendering of text should be used, + 1 indicates that standard ligatures should be used, and 2 indicates that all + available ligatures should be used. Which ligatures are standard depends on the + script and possibly the font. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:3.2 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) NSNumber *as_ligature; +- (void)as_setLigature:(nullable NSNumber *)ligature range:(NSRange)range; + +/** + The text effect. + + @discussion Default is nil (no effect). The only currently supported value + is NSTextEffectLetterpressStyle. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic) NSString *as_textEffect; +- (void)as_setTextEffect:(nullable NSString *)textEffect range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The skew to be applied to glyphs. + + @discussion Default is 0 (no skew). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic) NSNumber *as_obliqueness; +- (void)as_setObliqueness:(nullable NSNumber *)obliqueness range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The log of the expansion factor to be applied to glyphs. + + @discussion Default is 0 (no expansion). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic) NSNumber *as_expansion; +- (void)as_setExpansion:(nullable NSNumber *)expansion range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + The character's offset from the baseline, in points. + + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:7.0 + */ +@property (nullable, nonatomic) NSNumber *as_baselineOffset; +- (void)as_setBaselineOffset:(nullable NSNumber *)baselineOffset range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + Glyph orientation control. + + @discussion Default is NO. A value of NO indicates that horizontal glyph forms + are to be used, YES indicates that vertical glyph forms are to be used. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:4.3 ASText:6.0 + */ +@property (nonatomic) BOOL as_verticalGlyphForm; +- (void)as_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range; + +/** + Specifies text language. + + @discussion Value must be a NSString containing a locale identifier. Default is + unset. When this attribute is set to a valid identifier, it will be used to select + localized glyphs (if supported by the font) and locale-specific line breaking rules. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:7.0 ASText:7.0 + */ +@property (nullable, nonatomic) NSString *as_language; +- (void)as_setLanguage:(nullable NSString *)language range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + Specifies a bidirectional override or embedding. + + @discussion See alse NSWritingDirection and NSWritingDirectionAttributeName. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:7.0 ASText:6.0 + */ +@property (nullable, nonatomic) NSArray *as_writingDirection; +- (void)as_setWritingDirection:(nullable NSArray *)writingDirection range:(NSRange)range; + +/** + An NSParagraphStyle object which is used to specify things like + line alignment, tab rulers, writing direction, etc. + + @discussion Default is nil ([NSParagraphStyle defaultParagraphStyle]). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nullable, nonatomic) NSParagraphStyle *as_paragraphStyle; +- (void)as_setParagraphStyle:(nullable NSParagraphStyle *)paragraphStyle range:(NSRange)range; + + +#pragma mark - Set paragraph attribute as property +///============================================================================= +/// @name Set paragraph attribute as property +///============================================================================= + +/** + The text alignment (A wrapper for NSParagraphStyle). + + @discussion Natural text alignment is realized as left or right alignment + depending on the line sweep direction of the first script contained in the paragraph. + @discussion Default is NSTextAlignmentNatural. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) NSTextAlignment as_alignment; +- (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range; + +/** + The mode that should be used to break lines (A wrapper for NSParagraphStyle). + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is NSLineBreakByWordWrapping. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) NSLineBreakMode as_lineBreakMode; +- (void)as_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range; + +/** + The distance in points between the bottom of one line fragment and the top of the next. + (A wrapper for NSParagraphStyle) + + @discussion This value is always nonnegative. This value is included in the line + fragment heights in the layout manager. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_lineSpacing; +- (void)as_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range; + +/** + The space after the end of the paragraph (A wrapper for NSParagraphStyle). + + @discussion This property contains the space (measured in points) added at the + end of the paragraph to separate it from the following paragraph. This value must + be nonnegative. The space between paragraphs is determined by adding the previous + paragraph's paragraphSpacing and the current paragraph's paragraphSpacingBefore. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_paragraphSpacing; +- (void)as_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range; + +/** + The distance between the paragraph's top and the beginning of its text content. + (A wrapper for NSParagraphStyle). + + @discussion This property contains the space (measured in points) between the + paragraph's top and the beginning of its text content. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_paragraphSpacingBefore; +- (void)as_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range; + +/** + The indentation of the first line (A wrapper for NSParagraphStyle). + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of the paragraph's first line. This value + is always nonnegative. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_firstLineHeadIndent; +- (void)as_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range; + +/** + The indentation of the receiver's lines other than the first. (A wrapper for NSParagraphStyle). + + @discussion This property contains the distance (in points) from the leading margin + of a text container to the beginning of lines other than the first. This value is + always nonnegative. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_headIndent; +- (void)as_setHeadIndent:(CGFloat)headIndent range:(NSRange)range; + +/** + The trailing indentation (A wrapper for NSParagraphStyle). + + @discussion If positive, this value is the distance from the leading margin + (for example, the left margin in left-to-right text). If 0 or negative, it's the + distance from the trailing margin. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_tailIndent; +- (void)as_setTailIndent:(CGFloat)tailIndent range:(NSRange)range; + +/** + The receiver's minimum height (A wrapper for NSParagraphStyle). + + @discussion This property contains the minimum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value must be nonnegative. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_minimumLineHeight; +- (void)as_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range; + +/** + The receiver's maximum line height (A wrapper for NSParagraphStyle). + + @discussion This property contains the maximum height in points that any line in + the receiver will occupy, regardless of the font size or size of any attached graphic. + This value is always nonnegative. Glyphs and graphics exceeding this height will + overlap neighboring lines; however, a maximum height of 0 implies no line height limit. + Although this limit applies to the line itself, line spacing adds extra space between adjacent lines. + @discussion Default is 0 (no limit). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_maximumLineHeight; +- (void)as_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range; + +/** + The line height multiple (A wrapper for NSParagraphStyle). + + @discussion This property contains the line break mode to be used laying out the paragraph's text. + @discussion Default is 0 (no multiple). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) CGFloat as_lineHeightMultiple; +- (void)as_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range; + +/** + The base writing direction (A wrapper for NSParagraphStyle). + + @discussion If you specify NSWritingDirectionNaturalDirection, the receiver resolves + the writing direction to either NSWritingDirectionLeftToRight or NSWritingDirectionRightToLeft, + depending on the direction for the user's `language` preference setting. + @discussion Default is NSWritingDirectionNatural. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:6.0 UIKit:6.0 ASText:6.0 + */ +@property (nonatomic) NSWritingDirection as_baseWritingDirection; +- (void)as_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range; + +/** + The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle). + + @discussion Valid values lie between 0.0 and 1.0 inclusive. Hyphenation is attempted + when the ratio of the text width (as broken without hyphenation) to the width of the + line fragment is less than the hyphenation factor. When the paragraph's hyphenation + factor is 0.0, the layout manager's hyphenation factor is used instead. When both + are 0.0, hyphenation is disabled. + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since UIKit:6.0 + */ +@property (nonatomic) float as_hyphenationFactor; +- (void)as_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range; + +/** + The document-wide default tab interval (A wrapper for NSParagraphStyle). + + @discussion This property represents the default tab interval in points. Tabs after the + last specified in tabStops are placed at integer multiples of this distance (if positive). + @discussion Default is 0. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nonatomic) CGFloat as_defaultTabInterval; +- (void)as_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +/** + An array of NSTextTab objects representing the receiver's tab stops. + (A wrapper for NSParagraphStyle). + + @discussion The NSTextTab objects, sorted by location, define the tab stops for + the paragraph style. + @discussion Default is 12 TabStops with 28.0 tab interval. + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since CoreText:7.0 UIKit:7.0 ASText:7.0 + */ +@property (nullable, nonatomic, copy) NSArray *as_tabStops; +- (void)as_setTabStops:(nullable NSArray *)tabStops range:(NSRange)range NS_AVAILABLE_IOS(7_0); + +#pragma mark - Set ASText attribute as property +///============================================================================= +/// @name Set ASText attribute as property +///============================================================================= + +/** + The text shadow. + + @discussion Default value is nil (no shadow). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic) ASTextShadow *as_textShadow; +- (void)as_setTextShadow:(nullable ASTextShadow *)textShadow range:(NSRange)range; + +/** + The text inner shadow. + + @discussion Default value is nil (no shadow). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic) ASTextShadow *as_textInnerShadow; +- (void)as_setTextInnerShadow:(nullable ASTextShadow *)textInnerShadow range:(NSRange)range; + +/** + The text underline. + + @discussion Default value is nil (no underline). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic) ASTextDecoration *as_textUnderline; +- (void)as_setTextUnderline:(nullable ASTextDecoration *)textUnderline range:(NSRange)range; + +/** + The text strikethrough. + + @discussion Default value is nil (no strikethrough). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic) ASTextDecoration *as_textStrikethrough; +- (void)as_setTextStrikethrough:(nullable ASTextDecoration *)textStrikethrough range:(NSRange)range; + +/** + The text border. + + @discussion Default value is nil (no border). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic) ASTextBorder *as_textBorder; +- (void)as_setTextBorder:(nullable ASTextBorder *)textBorder range:(NSRange)range; + +/** + The text background border. + + @discussion Default value is nil (no background border). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nullable, nonatomic) ASTextBorder *as_textBackgroundBorder; +- (void)as_setTextBackgroundBorder:(nullable ASTextBorder *)textBackgroundBorder range:(NSRange)range; + +/** + The glyph transform. + + @discussion Default value is CGAffineTransformIdentity (no transform). + @discussion Set this property applies to the entire text string. + Get this property returns the first character's attribute. + @since ASText:6.0 + */ +@property (nonatomic) CGAffineTransform as_textGlyphTransform; +- (void)as_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range; + + +#pragma mark - Set discontinuous attribute for range +///============================================================================= +/// @name Set discontinuous attribute for range +///============================================================================= + +- (void)as_setSuperscript:(nullable NSNumber *)superscript range:(NSRange)range; +- (void)as_setGlyphInfo:(nullable CTGlyphInfoRef)glyphInfo range:(NSRange)range; +- (void)as_setCharacterShape:(nullable NSNumber *)characterShape range:(NSRange)range __TVOS_PROHIBITED; +- (void)as_setRunDelegate:(nullable CTRunDelegateRef)runDelegate range:(NSRange)range; +- (void)as_setBaselineClass:(nullable CFStringRef)baselineClass range:(NSRange)range; +- (void)as_setBaselineInfo:(nullable CFDictionaryRef)baselineInfo range:(NSRange)range; +- (void)as_setBaselineReferenceInfo:(nullable CFDictionaryRef)referenceInfo range:(NSRange)range; +- (void)as_setRubyAnnotation:(nullable CTRubyAnnotationRef)ruby range:(NSRange)range NS_AVAILABLE_IOS(8_0); +- (void)as_setAttachment:(nullable NSTextAttachment *)attachment range:(NSRange)range NS_AVAILABLE_IOS(7_0); +- (void)as_setLink:(nullable id)link range:(NSRange)range NS_AVAILABLE_IOS(7_0); +- (void)as_setTextBackedString:(nullable ASTextBackedString *)textBackedString range:(NSRange)range; +- (void)as_setTextBinding:(nullable ASTextBinding *)textBinding range:(NSRange)range; +- (void)as_setTextAttachment:(nullable ASTextAttachment *)textAttachment range:(NSRange)range; +- (void)as_setTextHighlight:(nullable ASTextHighlight *)textHighlight range:(NSRange)range; +- (void)as_setTextBlockBorder:(nullable ASTextBorder *)textBlockBorder range:(NSRange)range; + + +#pragma mark - Convenience methods for text highlight +///============================================================================= +/// @name Convenience methods for text highlight +///============================================================================= + +/** + Convenience method to set text highlight + + @param range text range + @param color text color (pass nil to ignore) + @param backgroundColor text background color when highlight + @param userInfo user information dictionary (pass nil to ignore) + @param tapAction tap action when user tap the highlight (pass nil to ignore) + @param longPressAction long press action when user long press the highlight (pass nil to ignore) + */ +- (void)as_setTextHighlightRange:(NSRange)range + color:(nullable UIColor *)color + backgroundColor:(nullable UIColor *)backgroundColor + userInfo:(nullable NSDictionary *)userInfo + tapAction:(nullable ASTextAction)tapAction + longPressAction:(nullable ASTextAction)longPressAction; + +/** + Convenience method to set text highlight + + @param range text range + @param color text color (pass nil to ignore) + @param backgroundColor text background color when highlight + @param tapAction tap action when user tap the highlight (pass nil to ignore) + */ +- (void)as_setTextHighlightRange:(NSRange)range + color:(nullable UIColor *)color + backgroundColor:(nullable UIColor *)backgroundColor + tapAction:(nullable ASTextAction)tapAction; + +/** + Convenience method to set text highlight + + @param range text range + @param color text color (pass nil to ignore) + @param backgroundColor text background color when highlight + @param userInfo tap action when user tap the highlight (pass nil to ignore) + */ +- (void)as_setTextHighlightRange:(NSRange)range + color:(nullable UIColor *)color + backgroundColor:(nullable UIColor *)backgroundColor + userInfo:(nullable NSDictionary *)userInfo; + +#pragma mark - Utilities +///============================================================================= +/// @name Utilities +///============================================================================= + +/** + Inserts into the receiver the characters of a given string at a given location. + The new string inherit the attributes of the first replaced character from location. + + @param string The string to insert into the receiver, must not be nil. + @param location The location at which string is inserted. The location must not + exceed the bounds of the receiver. + @throw Raises an NSRangeException if the location out of bounds. + */ +- (void)as_insertString:(NSString *)string atIndex:(NSUInteger)location; + +/** + Adds to the end of the receiver the characters of a given string. + The new string inherit the attributes of the receiver's tail. + + @param string The string to append to the receiver, must not be nil. + */ +- (void)as_appendString:(NSString *)string; + +/** + Removes all discontinuous attributes in a specified range. + See `allDiscontinuousAttributeKeys`. + + @param range A text range. + */ +- (void)as_removeDiscontinuousAttributesInRange:(NSRange)range; + +/** + Returns all discontinuous attribute keys, such as RunDelegate/Attachment/Ruby. + + @discussion These attributes can only set to a specified range of text, and + should not extend to other range when editing text. + */ ++ (NSArray *)as_allDiscontinuousAttributeKeys; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.mm new file mode 100644 index 0000000000..39f628151c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.mm @@ -0,0 +1,1208 @@ +// +// NSAttributedString+ASText.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import + + +// Dummy class for category +@interface NSAttributedString_ASText : NSObject @end +@implementation NSAttributedString_ASText @end + + +@implementation NSAttributedString (ASText) + +- (NSDictionary *)as_attributesAtIndex:(NSUInteger)index { + if (index > self.length || self.length == 0) return nil; + if (self.length > 0 && index == self.length) index--; + return [self attributesAtIndex:index effectiveRange:NULL]; +} + +- (id)as_attribute:(NSString *)attributeName atIndex:(NSUInteger)index { + if (!attributeName) return nil; + if (index > self.length || self.length == 0) return nil; + if (self.length > 0 && index == self.length) index--; + return [self attribute:attributeName atIndex:index effectiveRange:NULL]; +} + +- (NSDictionary *)as_attributes { + return [self as_attributesAtIndex:0]; +} + +- (UIFont *)as_font { + return [self as_fontAtIndex:0]; +} + +- (UIFont *)as_fontAtIndex:(NSUInteger)index { + return [self as_attribute:NSFontAttributeName atIndex:index]; +} + +- (NSNumber *)as_kern { + return [self as_kernAtIndex:0]; +} + +- (NSNumber *)as_kernAtIndex:(NSUInteger)index { + return [self as_attribute:NSKernAttributeName atIndex:index]; +} + +- (UIColor *)as_color { + return [self as_colorAtIndex:0]; +} + +- (UIColor *)as_colorAtIndex:(NSUInteger)index { + UIColor *color = [self as_attribute:NSForegroundColorAttributeName atIndex:index]; + if (!color) { + CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTForegroundColorAttributeName atIndex:index]); + color = [UIColor colorWithCGColor:ref]; + } + if (color && ![color isKindOfClass:[UIColor class]]) { + if (CFGetTypeID((__bridge CFTypeRef)(color)) == CGColorGetTypeID()) { + color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)]; + } else { + color = nil; + } + } + return color; +} + +- (UIColor *)as_backgroundColor { + return [self as_backgroundColorAtIndex:0]; +} + +- (UIColor *)as_backgroundColorAtIndex:(NSUInteger)index { + return [self as_attribute:NSBackgroundColorAttributeName atIndex:index]; +} + +- (NSNumber *)as_strokeWidth { + return [self as_strokeWidthAtIndex:0]; +} + +- (NSNumber *)as_strokeWidthAtIndex:(NSUInteger)index { + return [self as_attribute:NSStrokeWidthAttributeName atIndex:index]; +} + +- (UIColor *)as_strokeColor { + return [self as_strokeColorAtIndex:0]; +} + +- (UIColor *)as_strokeColorAtIndex:(NSUInteger)index { + UIColor *color = [self as_attribute:NSStrokeColorAttributeName atIndex:index]; + if (!color) { + CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTStrokeColorAttributeName atIndex:index]); + color = [UIColor colorWithCGColor:ref]; + } + return color; +} + +- (NSShadow *)as_shadow { + return [self as_shadowAtIndex:0]; +} + +- (NSShadow *)as_shadowAtIndex:(NSUInteger)index { + return [self as_attribute:NSShadowAttributeName atIndex:index]; +} + +- (NSUnderlineStyle)as_strikethroughStyle { + return [self as_strikethroughStyleAtIndex:0]; +} + +- (NSUnderlineStyle)as_strikethroughStyleAtIndex:(NSUInteger)index { + NSNumber *style = [self as_attribute:NSStrikethroughStyleAttributeName atIndex:index]; + return (NSUnderlineStyle)style.integerValue; +} + +- (UIColor *)as_strikethroughColor { + return [self as_strikethroughColorAtIndex:0]; +} + +- (UIColor *)as_strikethroughColorAtIndex:(NSUInteger)index { + return [self as_attribute:NSStrikethroughColorAttributeName atIndex:index]; +} + +- (NSUnderlineStyle)as_underlineStyle { + return [self as_underlineStyleAtIndex:0]; +} + +- (NSUnderlineStyle)as_underlineStyleAtIndex:(NSUInteger)index { + NSNumber *style = [self as_attribute:NSUnderlineStyleAttributeName atIndex:index]; + return (NSUnderlineStyle)style.integerValue; +} + +- (UIColor *)as_underlineColor { + return [self as_underlineColorAtIndex:0]; +} + +- (UIColor *)as_underlineColorAtIndex:(NSUInteger)index { + UIColor *color = [self as_attribute:NSUnderlineColorAttributeName atIndex:index]; + if (!color) { + CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTUnderlineColorAttributeName atIndex:index]); + color = [UIColor colorWithCGColor:ref]; + } + return color; +} + +- (NSNumber *)as_ligature { + return [self as_ligatureAtIndex:0]; +} + +- (NSNumber *)as_ligatureAtIndex:(NSUInteger)index { + return [self as_attribute:NSLigatureAttributeName atIndex:index]; +} + +- (NSString *)as_textEffect { + return [self as_textEffectAtIndex:0]; +} + +- (NSString *)as_textEffectAtIndex:(NSUInteger)index { + return [self as_attribute:NSTextEffectAttributeName atIndex:index]; +} + +- (NSNumber *)as_obliqueness { + return [self as_obliquenessAtIndex:0]; +} + +- (NSNumber *)as_obliquenessAtIndex:(NSUInteger)index { + return [self as_attribute:NSObliquenessAttributeName atIndex:index]; +} + +- (NSNumber *)as_expansion { + return [self as_expansionAtIndex:0]; +} + +- (NSNumber *)as_expansionAtIndex:(NSUInteger)index { + return [self as_attribute:NSExpansionAttributeName atIndex:index]; +} + +- (NSNumber *)as_baselineOffset { + return [self as_baselineOffsetAtIndex:0]; +} + +- (NSNumber *)as_baselineOffsetAtIndex:(NSUInteger)index { + return [self as_attribute:NSBaselineOffsetAttributeName atIndex:index]; +} + +- (BOOL)as_verticalGlyphForm { + return [self as_verticalGlyphFormAtIndex:0]; +} + +- (BOOL)as_verticalGlyphFormAtIndex:(NSUInteger)index { + NSNumber *num = [self as_attribute:NSVerticalGlyphFormAttributeName atIndex:index]; + return num.boolValue; +} + +- (NSString *)as_language { + return [self as_languageAtIndex:0]; +} + +- (NSString *)as_languageAtIndex:(NSUInteger)index { + return [self as_attribute:(id)kCTLanguageAttributeName atIndex:index]; +} + +- (NSArray *)as_writingDirection { + return [self as_writingDirectionAtIndex:0]; +} + +- (NSArray *)as_writingDirectionAtIndex:(NSUInteger)index { + return [self as_attribute:(id)kCTWritingDirectionAttributeName atIndex:index]; +} + +- (NSParagraphStyle *)as_paragraphStyle { + return [self as_paragraphStyleAtIndex:0]; +} + +- (NSParagraphStyle *)as_paragraphStyleAtIndex:(NSUInteger)index { + /* + NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef. + + CoreText can use both NSParagraphStyle and CTParagraphStyleRef, + but UILabel/UITextView can only use NSParagraphStyle. + + We use NSParagraphStyle in both CoreText and UIKit. + */ + NSParagraphStyle *style = [self as_attribute:NSParagraphStyleAttributeName atIndex:index]; + if (style) { + if (CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) { \ + style = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(style)]; + } + } + return style; +} + +#define ParagraphAttribute(_attr_) \ +NSParagraphStyle *style = self.as_paragraphStyle; \ +if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \ +return style. _attr_; + +#define ParagraphAttributeAtIndex(_attr_) \ +NSParagraphStyle *style = [self as_paragraphStyleAtIndex:index]; \ +if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \ +return style. _attr_; + +- (NSTextAlignment)as_alignment { + ParagraphAttribute(alignment); +} + +- (NSLineBreakMode)as_lineBreakMode { + ParagraphAttribute(lineBreakMode); +} + +- (CGFloat)as_lineSpacing { + ParagraphAttribute(lineSpacing); +} + +- (CGFloat)as_paragraphSpacing { + ParagraphAttribute(paragraphSpacing); +} + +- (CGFloat)as_paragraphSpacingBefore { + ParagraphAttribute(paragraphSpacingBefore); +} + +- (CGFloat)as_firstLineHeadIndent { + ParagraphAttribute(firstLineHeadIndent); +} + +- (CGFloat)as_headIndent { + ParagraphAttribute(headIndent); +} + +- (CGFloat)as_tailIndent { + ParagraphAttribute(tailIndent); +} + +- (CGFloat)as_minimumLineHeight { + ParagraphAttribute(minimumLineHeight); +} + +- (CGFloat)as_maximumLineHeight { + ParagraphAttribute(maximumLineHeight); +} + +- (CGFloat)as_lineHeightMultiple { + ParagraphAttribute(lineHeightMultiple); +} + +- (NSWritingDirection)as_baseWritingDirection { + ParagraphAttribute(baseWritingDirection); +} + +- (float)as_hyphenationFactor { + ParagraphAttribute(hyphenationFactor); +} + +- (CGFloat)as_defaultTabInterval { + ParagraphAttribute(defaultTabInterval); +} + +- (NSArray *)as_tabStops { + ParagraphAttribute(tabStops); +} + +- (NSTextAlignment)as_alignmentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(alignment); +} + +- (NSLineBreakMode)as_lineBreakModeAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(lineBreakMode); +} + +- (CGFloat)as_lineSpacingAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(lineSpacing); +} + +- (CGFloat)as_paragraphSpacingAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(paragraphSpacing); +} + +- (CGFloat)as_paragraphSpacingBeforeAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(paragraphSpacingBefore); +} + +- (CGFloat)as_firstLineHeadIndentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(firstLineHeadIndent); +} + +- (CGFloat)as_headIndentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(headIndent); +} + +- (CGFloat)as_tailIndentAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(tailIndent); +} + +- (CGFloat)as_minimumLineHeightAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(minimumLineHeight); +} + +- (CGFloat)as_maximumLineHeightAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(maximumLineHeight); +} + +- (CGFloat)as_lineHeightMultipleAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(lineHeightMultiple); +} + +- (NSWritingDirection)as_baseWritingDirectionAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(baseWritingDirection); +} + +- (float)as_hyphenationFactorAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(hyphenationFactor); +} + +- (CGFloat)as_defaultTabIntervalAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(defaultTabInterval); +} + +- (NSArray *)as_tabStopsAtIndex:(NSUInteger)index { + ParagraphAttributeAtIndex(tabStops); +} + +#undef ParagraphAttribute +#undef ParagraphAttributeAtIndex + +- (ASTextShadow *)as_textShadow { + return [self as_textShadowAtIndex:0]; +} + +- (ASTextShadow *)as_textShadowAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextShadowAttributeName atIndex:index]; +} + +- (ASTextShadow *)as_textInnerShadow { + return [self as_textInnerShadowAtIndex:0]; +} + +- (ASTextShadow *)as_textInnerShadowAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextInnerShadowAttributeName atIndex:index]; +} + +- (ASTextDecoration *)as_textUnderline { + return [self as_textUnderlineAtIndex:0]; +} + +- (ASTextDecoration *)as_textUnderlineAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextUnderlineAttributeName atIndex:index]; +} + +- (ASTextDecoration *)as_textStrikethrough { + return [self as_textStrikethroughAtIndex:0]; +} + +- (ASTextDecoration *)as_textStrikethroughAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextStrikethroughAttributeName atIndex:index]; +} + +- (ASTextBorder *)as_textBorder { + return [self as_textBorderAtIndex:0]; +} + +- (ASTextBorder *)as_textBorderAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextBorderAttributeName atIndex:index]; +} + +- (ASTextBorder *)as_textBackgroundBorder { + return [self as_textBackgroundBorderAtIndex:0]; +} + +- (ASTextBorder *)as_textBackgroundBorderAtIndex:(NSUInteger)index { + return [self as_attribute:ASTextBackedStringAttributeName atIndex:index]; +} + +- (CGAffineTransform)as_textGlyphTransform { + return [self as_textGlyphTransformAtIndex:0]; +} + +- (CGAffineTransform)as_textGlyphTransformAtIndex:(NSUInteger)index { + NSValue *value = [self as_attribute:ASTextGlyphTransformAttributeName atIndex:index]; + if (!value) return CGAffineTransformIdentity; + return [value CGAffineTransformValue]; +} + +- (NSString *)as_plainTextForRange:(NSRange)range { + if (range.location == NSNotFound ||range.length == NSNotFound) return nil; + NSMutableString *result = [NSMutableString string]; + if (range.length == 0) return result; + NSString *string = self.string; + [self enumerateAttribute:ASTextBackedStringAttributeName inRange:range options:kNilOptions usingBlock:^(id value, NSRange range, BOOL *stop) { + ASTextBackedString *backed = value; + if (backed && backed.string) { + [result appendString:backed.string]; + } else { + [result appendString:[string substringWithRange:range]]; + } + }]; + return result; +} + ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content + contentMode:(UIViewContentMode)contentMode + width:(CGFloat)width + ascent:(CGFloat)ascent + descent:(CGFloat)descent { + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; + + ASTextAttachment *attach = [ASTextAttachment new]; + attach.content = content; + attach.contentMode = contentMode; + [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.width = width; + delegate.ascent = ascent; + delegate.descent = descent; + CTRunDelegateRef delegateRef = delegate.CTRunDelegate; + [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)]; + if (delegate) CFRelease(delegateRef); + + return atr; +} + ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content + contentMode:(UIViewContentMode)contentMode + attachmentSize:(CGSize)attachmentSize + alignToFont:(UIFont *)font + alignment:(ASTextVerticalAlignment)alignment { + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; + + ASTextAttachment *attach = [ASTextAttachment new]; + attach.content = content; + attach.contentMode = contentMode; + [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.width = attachmentSize.width; + switch (alignment) { + case ASTextVerticalAlignmentTop: { + delegate.ascent = font.ascender; + delegate.descent = attachmentSize.height - font.ascender; + if (delegate.descent < 0) { + delegate.descent = 0; + delegate.ascent = attachmentSize.height; + } + } break; + case ASTextVerticalAlignmentCenter: { + CGFloat fontHeight = font.ascender - font.descender; + CGFloat yOffset = font.ascender - fontHeight * 0.5; + delegate.ascent = attachmentSize.height * 0.5 + yOffset; + delegate.descent = attachmentSize.height - delegate.ascent; + if (delegate.descent < 0) { + delegate.descent = 0; + delegate.ascent = attachmentSize.height; + } + } break; + case ASTextVerticalAlignmentBottom: { + delegate.ascent = attachmentSize.height + font.descender; + delegate.descent = -font.descender; + if (delegate.ascent < 0) { + delegate.ascent = 0; + delegate.descent = attachmentSize.height; + } + } break; + default: { + delegate.ascent = attachmentSize.height; + delegate.descent = 0; + } break; + } + + CTRunDelegateRef delegateRef = delegate.CTRunDelegate; + [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)]; + if (delegate) CFRelease(delegateRef); + + return atr; +} + ++ (NSMutableAttributedString *)as_attachmentStringWithEmojiImage:(UIImage *)image + fontSize:(CGFloat)fontSize { + if (!image || fontSize <= 0) return nil; + + BOOL hasAnim = NO; + if (image.images.count > 1) { + hasAnim = YES; + } else if (NSProtocolFromString(@"ASAnimatedImage") && + [image conformsToProtocol:NSProtocolFromString(@"ASAnimatedImage")]) { + NSNumber *frameCount = [image valueForKey:@"animatedImageFrameCount"]; + if (frameCount.intValue > 1) hasAnim = YES; + } + + CGFloat ascent = ASTextEmojiGetAscentWithFontSize(fontSize); + CGFloat descent = ASTextEmojiGetDescentWithFontSize(fontSize); + CGRect bounding = ASTextEmojiGetGlyphBoundingRectWithFontSize(fontSize); + + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; + delegate.ascent = ascent; + delegate.descent = descent; + delegate.width = bounding.size.width + 2 * bounding.origin.x; + + ASTextAttachment *attachment = [ASTextAttachment new]; + attachment.contentMode = UIViewContentModeScaleAspectFit; + attachment.contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), bounding.origin.x, descent + bounding.origin.y, bounding.origin.x); + if (hasAnim) { + Class imageClass = NSClassFromString(@"ASAnimatedImageView"); + if (!imageClass) imageClass = [UIImageView class]; + UIImageView *view = (id)[imageClass new]; + view.frame = bounding; + view.image = image; + view.contentMode = UIViewContentModeScaleAspectFit; + attachment.content = view; + } else { + attachment.content = image; + } + + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; + [atr as_setTextAttachment:attachment range:NSMakeRange(0, atr.length)]; + CTRunDelegateRef ctDelegate = delegate.CTRunDelegate; + [atr as_setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)]; + if (ctDelegate) CFRelease(ctDelegate); + + return atr; +} + +- (NSRange)as_rangeOfAll { + return NSMakeRange(0, self.length); +} + +- (BOOL)as_isSharedAttributesInAllRange { + __block BOOL shared = YES; + __block NSDictionary *firstAttrs = nil; + [self enumerateAttributesInRange:self.as_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { + if (range.location == 0) { + firstAttrs = attrs; + } else { + if (firstAttrs.count != attrs.count) { + shared = NO; + *stop = YES; + } else if (firstAttrs) { + if (![firstAttrs isEqualToDictionary:attrs]) { + shared = NO; + *stop = YES; + } + } + } + }]; + return shared; +} + +- (BOOL)as_canDrawWithUIKit { + static NSMutableSet *failSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + failSet = [NSMutableSet new]; + [failSet addObject:(id)kCTGlyphInfoAttributeName]; +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [failSet addObject:(id)kCTCharacterShapeAttributeName]; +#pragma clang diagnostic pop +#endif + [failSet addObject:(id)kCTLanguageAttributeName]; + [failSet addObject:(id)kCTRunDelegateAttributeName]; + [failSet addObject:(id)kCTBaselineClassAttributeName]; + [failSet addObject:(id)kCTBaselineInfoAttributeName]; + [failSet addObject:(id)kCTBaselineReferenceInfoAttributeName]; + [failSet addObject:(id)kCTRubyAnnotationAttributeName]; + [failSet addObject:ASTextShadowAttributeName]; + [failSet addObject:ASTextInnerShadowAttributeName]; + [failSet addObject:ASTextUnderlineAttributeName]; + [failSet addObject:ASTextStrikethroughAttributeName]; + [failSet addObject:ASTextBorderAttributeName]; + [failSet addObject:ASTextBackgroundBorderAttributeName]; + [failSet addObject:ASTextBlockBorderAttributeName]; + [failSet addObject:ASTextAttachmentAttributeName]; + [failSet addObject:ASTextHighlightAttributeName]; + [failSet addObject:ASTextGlyphTransformAttributeName]; + }); + +#define Fail { result = NO; *stop = YES; return; } + __block BOOL result = YES; + [self enumerateAttributesInRange:self.as_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { + if (attrs.count == 0) return; + for (NSString *str in attrs.allKeys) { + if ([failSet containsObject:str]) Fail; + } + if (attrs[(id)kCTForegroundColorAttributeName] && !attrs[NSForegroundColorAttributeName]) Fail; + if (attrs[(id)kCTStrokeColorAttributeName] && !attrs[NSStrokeColorAttributeName]) Fail; + if (attrs[(id)kCTUnderlineColorAttributeName]) { + if (!attrs[NSUnderlineColorAttributeName]) Fail; + } + NSParagraphStyle *style = attrs[NSParagraphStyleAttributeName]; + if (style && CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) Fail; + }]; + return result; +#undef Fail +} + +@end + +@implementation NSMutableAttributedString (ASText) + +- (void)as_setAttributes:(NSDictionary *)attributes { + [self setAs_attributes:attributes]; +} + +- (void)setAs_attributes:(NSDictionary *)attributes { + if (attributes == (id)[NSNull null]) attributes = nil; + [self setAttributes:@{} range:NSMakeRange(0, self.length)]; + [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [self as_setAttribute:key value:obj]; + }]; +} + +- (void)as_setAttribute:(NSString *)name value:(id)value { + [self as_setAttribute:name value:value range:NSMakeRange(0, self.length)]; +} + +- (void)as_setAttribute:(NSString *)name value:(id)value range:(NSRange)range { + if (!name || [NSNull isEqual:name]) return; + if (value && ![NSNull isEqual:value]) [self addAttribute:name value:value range:range]; + else [self removeAttribute:name range:range]; +} + +- (void)as_removeAttributesInRange:(NSRange)range { + [self setAttributes:nil range:range]; +} + +#pragma mark - Property Setter + +- (void)setAs_font:(UIFont *)font { + /* + In iOS7 and later, UIFont is toll-free bridged to CTFontRef, + although Apple does not mention it in documentation. + + In iOS6, UIFont is a wrapper for CTFontRef, so CoreText can alse use UIfont, + but UILabel/UITextView cannot use CTFontRef. + + We use UIFont for both CoreText and UIKit. + */ + [self as_setFont:font range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_kern:(NSNumber *)kern { + [self as_setKern:kern range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_color:(UIColor *)color { + [self as_setColor:color range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_backgroundColor:(UIColor *)backgroundColor { + [self as_setBackgroundColor:backgroundColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strokeWidth:(NSNumber *)strokeWidth { + [self as_setStrokeWidth:strokeWidth range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strokeColor:(UIColor *)strokeColor { + [self as_setStrokeColor:strokeColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_shadow:(NSShadow *)shadow { + [self as_setShadow:shadow range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strikethroughStyle:(NSUnderlineStyle)strikethroughStyle { + [self as_setStrikethroughStyle:strikethroughStyle range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_strikethroughColor:(UIColor *)strikethroughColor { + [self as_setStrikethroughColor:strikethroughColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_underlineStyle:(NSUnderlineStyle)underlineStyle { + [self as_setUnderlineStyle:underlineStyle range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_underlineColor:(UIColor *)underlineColor { + [self as_setUnderlineColor:underlineColor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_ligature:(NSNumber *)ligature { + [self as_setLigature:ligature range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textEffect:(NSString *)textEffect { + [self as_setTextEffect:textEffect range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_obliqueness:(NSNumber *)obliqueness { + [self as_setObliqueness:obliqueness range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_expansion:(NSNumber *)expansion { + [self as_setExpansion:expansion range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_baselineOffset:(NSNumber *)baselineOffset { + [self as_setBaselineOffset:baselineOffset range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_verticalGlyphForm:(BOOL)verticalGlyphForm { + [self as_setVerticalGlyphForm:verticalGlyphForm range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_language:(NSString *)language { + [self as_setLanguage:language range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_writingDirection:(NSArray *)writingDirection { + [self as_setWritingDirection:writingDirection range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_paragraphStyle:(NSParagraphStyle *)paragraphStyle { + /* + NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef. + + CoreText can use both NSParagraphStyle and CTParagraphStyleRef, + but UILabel/UITextView can only use NSParagraphStyle. + + We use NSParagraphStyle in both CoreText and UIKit. + */ + [self as_setParagraphStyle:paragraphStyle range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_alignment:(NSTextAlignment)alignment { + [self as_setAlignment:alignment range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_baseWritingDirection:(NSWritingDirection)baseWritingDirection { + [self as_setBaseWritingDirection:baseWritingDirection range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_lineSpacing:(CGFloat)lineSpacing { + [self as_setLineSpacing:lineSpacing range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_paragraphSpacing:(CGFloat)paragraphSpacing { + [self as_setParagraphSpacing:paragraphSpacing range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_paragraphSpacingBefore:(CGFloat)paragraphSpacingBefore { + [self as_setParagraphSpacing:paragraphSpacingBefore range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_firstLineHeadIndent:(CGFloat)firstLineHeadIndent { + [self as_setFirstLineHeadIndent:firstLineHeadIndent range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_headIndent:(CGFloat)headIndent { + [self as_setHeadIndent:headIndent range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_tailIndent:(CGFloat)tailIndent { + [self as_setTailIndent:tailIndent range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_lineBreakMode:(NSLineBreakMode)lineBreakMode { + [self as_setLineBreakMode:lineBreakMode range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_minimumLineHeight:(CGFloat)minimumLineHeight { + [self as_setMinimumLineHeight:minimumLineHeight range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_maximumLineHeight:(CGFloat)maximumLineHeight { + [self as_setMaximumLineHeight:maximumLineHeight range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_lineHeightMultiple:(CGFloat)lineHeightMultiple { + [self as_setLineHeightMultiple:lineHeightMultiple range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_hyphenationFactor:(float)hyphenationFactor { + [self as_setHyphenationFactor:hyphenationFactor range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_defaultTabInterval:(CGFloat)defaultTabInterval { + [self as_setDefaultTabInterval:defaultTabInterval range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_tabStops:(NSArray *)tabStops { + [self as_setTabStops:tabStops range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textShadow:(ASTextShadow *)textShadow { + [self as_setTextShadow:textShadow range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textInnerShadow:(ASTextShadow *)textInnerShadow { + [self as_setTextInnerShadow:textInnerShadow range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textUnderline:(ASTextDecoration *)textUnderline { + [self as_setTextUnderline:textUnderline range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textStrikethrough:(ASTextDecoration *)textStrikethrough { + [self as_setTextStrikethrough:textStrikethrough range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textBorder:(ASTextBorder *)textBorder { + [self as_setTextBorder:textBorder range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textBackgroundBorder:(ASTextBorder *)textBackgroundBorder { + [self as_setTextBackgroundBorder:textBackgroundBorder range:NSMakeRange(0, self.length)]; +} + +- (void)setAs_textGlyphTransform:(CGAffineTransform)textGlyphTransform { + [self as_setTextGlyphTransform:textGlyphTransform range:NSMakeRange(0, self.length)]; +} + +#pragma mark - Range Setter + +- (void)as_setFont:(UIFont *)font range:(NSRange)range { + [self as_setAttribute:NSFontAttributeName value:font range:range]; +} + +- (void)as_setKern:(NSNumber *)kern range:(NSRange)range { + [self as_setAttribute:NSKernAttributeName value:kern range:range]; +} + +- (void)as_setColor:(UIColor *)color range:(NSRange)range { + [self as_setAttribute:(id)kCTForegroundColorAttributeName value:(id)color.CGColor range:range]; + [self as_setAttribute:NSForegroundColorAttributeName value:color range:range]; +} + +- (void)as_setBackgroundColor:(UIColor *)backgroundColor range:(NSRange)range { + [self as_setAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range]; +} + +- (void)as_setStrokeWidth:(NSNumber *)strokeWidth range:(NSRange)range { + [self as_setAttribute:NSStrokeWidthAttributeName value:strokeWidth range:range]; +} + +- (void)as_setStrokeColor:(UIColor *)strokeColor range:(NSRange)range { + [self as_setAttribute:(id)kCTStrokeColorAttributeName value:(id)strokeColor.CGColor range:range]; + [self as_setAttribute:NSStrokeColorAttributeName value:strokeColor range:range]; +} + +- (void)as_setShadow:(NSShadow *)shadow range:(NSRange)range { + [self as_setAttribute:NSShadowAttributeName value:shadow range:range]; +} + +- (void)as_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range { + NSNumber *style = strikethroughStyle == 0 ? nil : @(strikethroughStyle); + [self as_setAttribute:NSStrikethroughStyleAttributeName value:style range:range]; +} + +- (void)as_setStrikethroughColor:(UIColor *)strikethroughColor range:(NSRange)range { + [self as_setAttribute:NSStrikethroughColorAttributeName value:strikethroughColor range:range]; +} + +- (void)as_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range { + NSNumber *style = underlineStyle == 0 ? nil : @(underlineStyle); + [self as_setAttribute:NSUnderlineStyleAttributeName value:style range:range]; +} + +- (void)as_setUnderlineColor:(UIColor *)underlineColor range:(NSRange)range { + [self as_setAttribute:(id)kCTUnderlineColorAttributeName value:(id)underlineColor.CGColor range:range]; + [self as_setAttribute:NSUnderlineColorAttributeName value:underlineColor range:range]; +} + +- (void)as_setLigature:(NSNumber *)ligature range:(NSRange)range { + [self as_setAttribute:NSLigatureAttributeName value:ligature range:range]; +} + +- (void)as_setTextEffect:(NSString *)textEffect range:(NSRange)range { + [self as_setAttribute:NSTextEffectAttributeName value:textEffect range:range]; +} + +- (void)as_setObliqueness:(NSNumber *)obliqueness range:(NSRange)range { + [self as_setAttribute:NSObliquenessAttributeName value:obliqueness range:range]; +} + +- (void)as_setExpansion:(NSNumber *)expansion range:(NSRange)range { + [self as_setAttribute:NSExpansionAttributeName value:expansion range:range]; +} + +- (void)as_setBaselineOffset:(NSNumber *)baselineOffset range:(NSRange)range { + [self as_setAttribute:NSBaselineOffsetAttributeName value:baselineOffset range:range]; +} + +- (void)as_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range { + NSNumber *v = verticalGlyphForm ? @(YES) : nil; + [self as_setAttribute:NSVerticalGlyphFormAttributeName value:v range:range]; +} + +- (void)as_setLanguage:(NSString *)language range:(NSRange)range { + [self as_setAttribute:(id)kCTLanguageAttributeName value:language range:range]; +} + +- (void)as_setWritingDirection:(NSArray *)writingDirection range:(NSRange)range { + [self as_setAttribute:(id)kCTWritingDirectionAttributeName value:writingDirection range:range]; +} + +- (void)as_setParagraphStyle:(NSParagraphStyle *)paragraphStyle range:(NSRange)range { + /* + NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef. + + CoreText can use both NSParagraphStyle and CTParagraphStyleRef, + but UILabel/UITextView can only use NSParagraphStyle. + + We use NSParagraphStyle in both CoreText and UIKit. + */ + [self as_setAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; +} + +#define ParagraphStyleSet(_attr_) \ +[self enumerateAttribute:NSParagraphStyleAttributeName \ +inRange:range \ +options:kNilOptions \ +usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \ +NSMutableParagraphStyle *style = nil; \ +if (value) { \ +if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \ +value = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \ +} \ +if (value. _attr_ == _attr_) return; \ +if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \ +style = (id)value; \ +} else { \ +style = value.mutableCopy; \ +} \ +} else { \ +if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \ +style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \ +} \ +style. _attr_ = _attr_; \ +[self as_setParagraphStyle:style range:subRange]; \ +}]; + +- (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range { + ParagraphStyleSet(alignment); +} + +- (void)as_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range { + ParagraphStyleSet(baseWritingDirection); +} + +- (void)as_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range { + ParagraphStyleSet(lineSpacing); +} + +- (void)as_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range { + ParagraphStyleSet(paragraphSpacing); +} + +- (void)as_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range { + ParagraphStyleSet(paragraphSpacingBefore); +} + +- (void)as_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range { + ParagraphStyleSet(firstLineHeadIndent); +} + +- (void)as_setHeadIndent:(CGFloat)headIndent range:(NSRange)range { + ParagraphStyleSet(headIndent); +} + +- (void)as_setTailIndent:(CGFloat)tailIndent range:(NSRange)range { + ParagraphStyleSet(tailIndent); +} + +- (void)as_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range { + ParagraphStyleSet(lineBreakMode); +} + +- (void)as_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range { + ParagraphStyleSet(minimumLineHeight); +} + +- (void)as_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range { + ParagraphStyleSet(maximumLineHeight); +} + +- (void)as_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range { + ParagraphStyleSet(lineHeightMultiple); +} + +- (void)as_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range { + ParagraphStyleSet(hyphenationFactor); +} + +- (void)as_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range { + ParagraphStyleSet(defaultTabInterval); +} + +- (void)as_setTabStops:(NSArray *)tabStops range:(NSRange)range { + ParagraphStyleSet(tabStops); +} + +#undef ParagraphStyleSet + +- (void)as_setSuperscript:(NSNumber *)superscript range:(NSRange)range { + if ([superscript isEqualToNumber:@(0)]) { + superscript = nil; + } + [self as_setAttribute:(id)kCTSuperscriptAttributeName value:superscript range:range]; +} + +- (void)as_setGlyphInfo:(CTGlyphInfoRef)glyphInfo range:(NSRange)range { + [self as_setAttribute:(id)kCTGlyphInfoAttributeName value:(__bridge id)glyphInfo range:range]; +} + +- (void)as_setCharacterShape:(NSNumber *)characterShape range:(NSRange)range { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self as_setAttribute:(id)kCTCharacterShapeAttributeName value:characterShape range:range]; +#pragma clang diagnostic pop +} + +- (void)as_setRunDelegate:(CTRunDelegateRef)runDelegate range:(NSRange)range { + [self as_setAttribute:(id)kCTRunDelegateAttributeName value:(__bridge id)runDelegate range:range]; +} + +- (void)as_setBaselineClass:(CFStringRef)baselineClass range:(NSRange)range { + [self as_setAttribute:(id)kCTBaselineClassAttributeName value:(__bridge id)baselineClass range:range]; +} + +- (void)as_setBaselineInfo:(CFDictionaryRef)baselineInfo range:(NSRange)range { + [self as_setAttribute:(id)kCTBaselineInfoAttributeName value:(__bridge id)baselineInfo range:range]; +} + +- (void)as_setBaselineReferenceInfo:(CFDictionaryRef)referenceInfo range:(NSRange)range { + [self as_setAttribute:(id)kCTBaselineReferenceInfoAttributeName value:(__bridge id)referenceInfo range:range]; +} + +- (void)as_setRubyAnnotation:(CTRubyAnnotationRef)ruby range:(NSRange)range { + [self as_setAttribute:(id)kCTRubyAnnotationAttributeName value:(__bridge id)ruby range:range]; +} + +- (void)as_setAttachment:(NSTextAttachment *)attachment range:(NSRange)range { + [self as_setAttribute:NSAttachmentAttributeName value:attachment range:range]; +} + +- (void)as_setLink:(id)link range:(NSRange)range { + [self as_setAttribute:NSLinkAttributeName value:link range:range]; +} + +- (void)as_setTextBackedString:(ASTextBackedString *)textBackedString range:(NSRange)range { + [self as_setAttribute:ASTextBackedStringAttributeName value:textBackedString range:range]; +} + +- (void)as_setTextBinding:(ASTextBinding *)textBinding range:(NSRange)range { + [self as_setAttribute:ASTextBindingAttributeName value:textBinding range:range]; +} + +- (void)as_setTextShadow:(ASTextShadow *)textShadow range:(NSRange)range { + [self as_setAttribute:ASTextShadowAttributeName value:textShadow range:range]; +} + +- (void)as_setTextInnerShadow:(ASTextShadow *)textInnerShadow range:(NSRange)range { + [self as_setAttribute:ASTextInnerShadowAttributeName value:textInnerShadow range:range]; +} + +- (void)as_setTextUnderline:(ASTextDecoration *)textUnderline range:(NSRange)range { + [self as_setAttribute:ASTextUnderlineAttributeName value:textUnderline range:range]; +} + +- (void)as_setTextStrikethrough:(ASTextDecoration *)textStrikethrough range:(NSRange)range { + [self as_setAttribute:ASTextStrikethroughAttributeName value:textStrikethrough range:range]; +} + +- (void)as_setTextBorder:(ASTextBorder *)textBorder range:(NSRange)range { + [self as_setAttribute:ASTextBorderAttributeName value:textBorder range:range]; +} + +- (void)as_setTextBackgroundBorder:(ASTextBorder *)textBackgroundBorder range:(NSRange)range { + [self as_setAttribute:ASTextBackgroundBorderAttributeName value:textBackgroundBorder range:range]; +} + +- (void)as_setTextAttachment:(ASTextAttachment *)textAttachment range:(NSRange)range { + [self as_setAttribute:ASTextAttachmentAttributeName value:textAttachment range:range]; +} + +- (void)as_setTextHighlight:(ASTextHighlight *)textHighlight range:(NSRange)range { + [self as_setAttribute:ASTextHighlightAttributeName value:textHighlight range:range]; +} + +- (void)as_setTextBlockBorder:(ASTextBorder *)textBlockBorder range:(NSRange)range { + [self as_setAttribute:ASTextBlockBorderAttributeName value:textBlockBorder range:range]; +} + +- (void)as_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range { + NSValue *value = CGAffineTransformIsIdentity(textGlyphTransform) ? nil : [NSValue valueWithCGAffineTransform:textGlyphTransform]; + [self as_setAttribute:ASTextGlyphTransformAttributeName value:value range:range]; +} + +- (void)as_setTextHighlightRange:(NSRange)range + color:(UIColor *)color + backgroundColor:(UIColor *)backgroundColor + userInfo:(NSDictionary *)userInfo + tapAction:(ASTextAction)tapAction + longPressAction:(ASTextAction)longPressAction { + ASTextHighlight *highlight = [ASTextHighlight highlightWithBackgroundColor:backgroundColor]; + highlight.userInfo = userInfo; + highlight.tapAction = tapAction; + highlight.longPressAction = longPressAction; + if (color) [self as_setColor:color range:range]; + [self as_setTextHighlight:highlight range:range]; +} + +- (void)as_setTextHighlightRange:(NSRange)range + color:(UIColor *)color + backgroundColor:(UIColor *)backgroundColor + tapAction:(ASTextAction)tapAction { + [self as_setTextHighlightRange:range + color:color + backgroundColor:backgroundColor + userInfo:nil + tapAction:tapAction + longPressAction:nil]; +} + +- (void)as_setTextHighlightRange:(NSRange)range + color:(UIColor *)color + backgroundColor:(UIColor *)backgroundColor + userInfo:(NSDictionary *)userInfo { + [self as_setTextHighlightRange:range + color:color + backgroundColor:backgroundColor + userInfo:userInfo + tapAction:nil + longPressAction:nil]; +} + +- (void)as_insertString:(NSString *)string atIndex:(NSUInteger)location { + [self replaceCharactersInRange:NSMakeRange(location, 0) withString:string]; + [self as_removeDiscontinuousAttributesInRange:NSMakeRange(location, string.length)]; +} + +- (void)as_appendString:(NSString *)string { + NSUInteger length = self.length; + [self replaceCharactersInRange:NSMakeRange(length, 0) withString:string]; + [self as_removeDiscontinuousAttributesInRange:NSMakeRange(length, string.length)]; +} + +- (void)as_removeDiscontinuousAttributesInRange:(NSRange)range { + NSArray *keys = [NSMutableAttributedString as_allDiscontinuousAttributeKeys]; + for (NSString *key in keys) { + [self removeAttribute:key range:range]; + } +} + ++ (NSArray *)as_allDiscontinuousAttributeKeys { + static NSArray *keys; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keys = @[(id)kCTSuperscriptAttributeName, + (id)kCTRunDelegateAttributeName, + ASTextBackedStringAttributeName, + ASTextBindingAttributeName, + ASTextAttachmentAttributeName, + (id)kCTRubyAnnotationAttributeName, + NSAttachmentAttributeName]; + }); + return keys; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h new file mode 100644 index 0000000000..a7c0aec5ff --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.h @@ -0,0 +1,34 @@ +// +// NSParagraphStyle+ASText.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Provides extensions for `NSParagraphStyle` to work with CoreText. + */ +@interface NSParagraphStyle (ASText) + +/** + Creates a new NSParagraphStyle object from the CoreText Style. + + @param CTStyle CoreText Paragraph Style. + + @return a new NSParagraphStyle + */ ++ (nullable NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle; + +/** + Creates and returns a CoreText Paragraph Style. (need call CFRelease() after used) + */ +- (nullable CTParagraphStyleRef)as_CTStyle CF_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.mm b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.mm new file mode 100644 index 0000000000..bc19fd234c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.mm @@ -0,0 +1,219 @@ +// +// NSParagraphStyle+ASText.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +// Dummy class for category +@interface NSParagraphStyle_ASText : NSObject @end +@implementation NSParagraphStyle_ASText @end + + +@implementation NSParagraphStyle (ASText) + ++ (NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { + if (CTStyle == NULL) return nil; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CGFloat lineSpacing; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing)) { + style.lineSpacing = lineSpacing; + } +#pragma clang diagnostic pop +#endif + + CGFloat paragraphSpacing; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacing)) { + style.paragraphSpacing = paragraphSpacing; + } + + CTTextAlignment alignment; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &alignment)) { + style.alignment = NSTextAlignmentFromCTTextAlignment(alignment); + } + + CGFloat firstLineHeadIndent; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineHeadIndent)) { + style.firstLineHeadIndent = firstLineHeadIndent; + } + + CGFloat headIndent; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &headIndent)) { + style.headIndent = headIndent; + } + + CGFloat tailIndent; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &tailIndent)) { + style.tailIndent = tailIndent; + } + + CTLineBreakMode lineBreakMode; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode)) { + style.lineBreakMode = (NSLineBreakMode)lineBreakMode; + } + + CGFloat minimumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minimumLineHeight)) { + style.minimumLineHeight = minimumLineHeight; + } + + CGFloat maximumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maximumLineHeight)) { + style.maximumLineHeight = maximumLineHeight; + } + + CTWritingDirection baseWritingDirection; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) { + style.baseWritingDirection = (NSWritingDirection)baseWritingDirection; + } + + CGFloat lineHeightMultiple; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple)) { + style.lineHeightMultiple = lineHeightMultiple; + } + + CGFloat paragraphSpacingBefore; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), ¶graphSpacingBefore)) { + style.paragraphSpacingBefore = paragraphSpacingBefore; + } + + CFArrayRef tabStops; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops)) { + NSMutableArray *tabs = [NSMutableArray new]; + [((__bridge NSArray *)(tabStops))enumerateObjectsUsingBlock : ^(id obj, NSUInteger idx, BOOL *stop) { + CTTextTabRef ctTab = (__bridge CTTextTabRef)obj; + + NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentFromCTTextAlignment(CTTextTabGetAlignment(ctTab)) location:CTTextTabGetLocation(ctTab) options:(__bridge id)CTTextTabGetOptions(ctTab)]; + [tabs addObject:tab]; + }]; + if (tabs.count) { + style.tabStops = tabs; + } + } + + CGFloat defaultTabInterval; + if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(CGFloat), &defaultTabInterval)) { + style.defaultTabInterval = defaultTabInterval; + } + + return style; +} + +- (CTParagraphStyleRef)as_CTStyle CF_RETURNS_RETAINED { + CTParagraphStyleSetting set[kCTParagraphStyleSpecifierCount] = { }; + int count = 0; + +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CGFloat lineSpacing = self.lineSpacing; + set[count].spec = kCTParagraphStyleSpecifierLineSpacing; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &lineSpacing; + count++; +#pragma clang diagnostic pop +#endif + + CGFloat paragraphSpacing = self.paragraphSpacing; + set[count].spec = kCTParagraphStyleSpecifierParagraphSpacing; + set[count].valueSize = sizeof(CGFloat); + set[count].value = ¶graphSpacing; + count++; + + CTTextAlignment alignment = NSTextAlignmentToCTTextAlignment(self.alignment); + set[count].spec = kCTParagraphStyleSpecifierAlignment; + set[count].valueSize = sizeof(CTTextAlignment); + set[count].value = &alignment; + count++; + + CGFloat firstLineHeadIndent = self.firstLineHeadIndent; + set[count].spec = kCTParagraphStyleSpecifierFirstLineHeadIndent; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &firstLineHeadIndent; + count++; + + CGFloat headIndent = self.headIndent; + set[count].spec = kCTParagraphStyleSpecifierHeadIndent; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &headIndent; + count++; + + CGFloat tailIndent = self.tailIndent; + set[count].spec = kCTParagraphStyleSpecifierTailIndent; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &tailIndent; + count++; + + CTLineBreakMode paraLineBreak = (CTLineBreakMode)self.lineBreakMode; + set[count].spec = kCTParagraphStyleSpecifierLineBreakMode; + set[count].valueSize = sizeof(CTLineBreakMode); + set[count].value = ¶LineBreak; + count++; + + CGFloat minimumLineHeight = self.minimumLineHeight; + set[count].spec = kCTParagraphStyleSpecifierMinimumLineHeight; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &minimumLineHeight; + count++; + + CGFloat maximumLineHeight = self.maximumLineHeight; + set[count].spec = kCTParagraphStyleSpecifierMaximumLineHeight; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &maximumLineHeight; + count++; + + CTWritingDirection paraWritingDirection = (CTWritingDirection)self.baseWritingDirection; + set[count].spec = kCTParagraphStyleSpecifierBaseWritingDirection; + set[count].valueSize = sizeof(CTWritingDirection); + set[count].value = ¶WritingDirection; + count++; + + CGFloat lineHeightMultiple = self.lineHeightMultiple; + set[count].spec = kCTParagraphStyleSpecifierLineHeightMultiple; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &lineHeightMultiple; + count++; + + CGFloat paragraphSpacingBefore = self.paragraphSpacingBefore; + set[count].spec = kCTParagraphStyleSpecifierParagraphSpacingBefore; + set[count].valueSize = sizeof(CGFloat); + set[count].value = ¶graphSpacingBefore; + count++; + + NSMutableArray *tabs = [NSMutableArray array]; + NSInteger numTabs = self.tabStops.count; + if (numTabs) { + [self.tabStops enumerateObjectsUsingBlock: ^(NSTextTab *tab, NSUInteger idx, BOOL *stop) { + CTTextTabRef ctTab = CTTextTabCreate(NSTextAlignmentToCTTextAlignment(tab.alignment), tab.location, (__bridge CFDictionaryRef)tab.options); + [tabs addObject:(__bridge id)ctTab]; + CFRelease(ctTab); + }]; + + CFArrayRef tabStops = (__bridge CFArrayRef)(tabs); + set[count].spec = kCTParagraphStyleSpecifierTabStops; + set[count].valueSize = sizeof(CFArrayRef); + set[count].value = &tabStops; + count++; + } + + CGFloat defaultTabInterval = self.defaultTabInterval; + set[count].spec = kCTParagraphStyleSpecifierDefaultTabInterval; + set[count].valueSize = sizeof(CGFloat); + set[count].value = &defaultTabInterval; + count++; + + CTParagraphStyleRef style = CTParagraphStyleCreate(set, count); + return style; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutInfo.h b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutInfo.h new file mode 100644 index 0000000000..6d7c91e5e5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutInfo.h @@ -0,0 +1,26 @@ +// +// _ASCollectionGalleryLayoutInfo.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface _ASCollectionGalleryLayoutInfo : NSObject + +// Read-only properties +@property (nonatomic, readonly) CGSize itemSize; +@property (nonatomic, readonly) CGFloat minimumLineSpacing; +@property (nonatomic, readonly) CGFloat minimumInteritemSpacing; +@property (nonatomic, readonly) UIEdgeInsets sectionInset; + +- (instancetype)initWithItemSize:(CGSize)itemSize + minimumLineSpacing:(CGFloat)minimumLineSpacing + minimumInteritemSpacing:(CGFloat)minimumInteritemSpacing + sectionInset:(UIEdgeInsets)sectionInset NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutInfo.mm b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutInfo.mm new file mode 100644 index 0000000000..fec0b52872 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutInfo.mm @@ -0,0 +1,68 @@ +// +// _ASCollectionGalleryLayoutInfo.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation _ASCollectionGalleryLayoutInfo + +- (instancetype)initWithItemSize:(CGSize)itemSize + minimumLineSpacing:(CGFloat)minimumLineSpacing + minimumInteritemSpacing:(CGFloat)minimumInteritemSpacing + sectionInset:(UIEdgeInsets)sectionInset +{ + self = [super init]; + if (self) { + _itemSize = itemSize; + _minimumLineSpacing = minimumLineSpacing; + _minimumInteritemSpacing = minimumInteritemSpacing; + _sectionInset = sectionInset; + } + return self; +} + +- (BOOL)isEqualToInfo:(_ASCollectionGalleryLayoutInfo *)info +{ + if (info == nil) { + return NO; + } + + return CGSizeEqualToSize(_itemSize, info.itemSize) + && _minimumLineSpacing == info.minimumLineSpacing + && _minimumInteritemSpacing == info.minimumInteritemSpacing + && UIEdgeInsetsEqualToEdgeInsets(_sectionInset, info.sectionInset); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[_ASCollectionGalleryLayoutInfo class]]) { + return NO; + } + return [self isEqualToInfo:other]; +} + +- (NSUInteger)hash +{ + struct { + CGSize itemSize; + CGFloat minimumLineSpacing; + CGFloat minimumInteritemSpacing; + UIEdgeInsets sectionInset; + } data = { + _itemSize, + _minimumLineSpacing, + _minimumInteritemSpacing, + _sectionInset, + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutItem.h b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutItem.h new file mode 100644 index 0000000000..21df3bcdc0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutItem.h @@ -0,0 +1,38 @@ +// +// _ASCollectionGalleryLayoutItem.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import + +@class ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A dummy item that represents a collection element to participate in the collection layout calculation process + * without triggering measurement on the actual node of the collection element. + * + * This item always has a fixed size that is the item size passed to it. + */ +AS_SUBCLASSING_RESTRICTED +@interface _ASGalleryLayoutItem : NSObject + +@property (nonatomic, readonly) CGSize itemSize; +@property (nonatomic, weak, readonly) ASCollectionElement *collectionElement; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement; +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutItem.mm b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutItem.mm new file mode 100644 index 0000000000..582d7983cc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -0,0 +1,85 @@ +// +// _ASCollectionGalleryLayoutItem.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import + +#import +#import +#import +#import +#import + +@implementation _ASGalleryLayoutItem { + std::atomic _primitiveTraitCollection; +} + +@synthesize style; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement +{ + self = [super init]; + if (self) { + ASDisplayNodeAssert(! CGSizeEqualToSize(CGSizeZero, itemSize), @"Item size should not be zero"); + ASDisplayNodeAssertNotNil(collectionElement, @"Collection element should not be nil"); + _itemSize = itemSize; + _collectionElement = collectionElement; + } + return self; +} + +ASLayoutElementStyleExtensibilityForwarding +ASPrimitiveTraitCollectionDefaults + +- (ASTraitCollection *)asyncTraitCollection +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeLayoutSpec; +} + +- (NSArray> *)sublayoutElements +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (BOOL)implementsLayoutMethod +{ + return YES; +} + +ASLayoutElementLayoutCalculationDefaults + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + ASDisplayNodeAssert(CGSizeEqualToSize(_itemSize, ASSizeRangeClamp(constrainedSize, _itemSize)), + @"Item size %@ can't fit within the bounds of constrained size %@", NSStringFromCGSize(_itemSize), NSStringFromASSizeRange(constrainedSize)); + return [ASLayout layoutWithLayoutElement:self size:_itemSize]; +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + return [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASCoreAnimationExtras.h b/submodules/AsyncDisplayKit/Source/Private/_ASCoreAnimationExtras.h new file mode 100644 index 0000000000..5937f3012c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASCoreAnimationExtras.h @@ -0,0 +1,84 @@ +// +// _ASCoreAnimationExtras.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +// This protocol defines the core properties that ASDisplayNode and CALayer share, for managing contents. +@protocol ASResizableContents +@required +@property id contents; +@property CGRect contentsRect; +@property CGRect contentsCenter; +@property CGFloat contentsScale; +@property CGFloat rasterizationScale; +@property NSString *contentsGravity; +@end + +@interface CALayer (ASResizableContents) +@end +@interface ASDisplayNode (ASResizableContents) +@end + +/** + This function can operate on either an ASDisplayNode (including un-loaded) or CALayer directly. More info about resizing mode: https://github.com/TextureGroup/Texture/issues/439 + + @param obj ASDisplayNode, CALayer or object that conforms to `ASResizableContents` protocol + @param image Image you would like to resize + */ +AS_EXTERN void ASDisplayNodeSetResizableContents(id obj, UIImage *image); + +/** + Turns a value of UIViewContentMode to a string for debugging or serialization + @param contentMode Any of the UIViewContentMode constants + @return A human-readable representation of the constant, or the integer value of the constant if not recognized. + */ +AS_EXTERN NSString *ASDisplayNodeNSStringFromUIContentMode(UIViewContentMode contentMode); + +/** + Turns a string representing a contentMode into a contentMode + @param string Any of the strings in UIContentModeDescriptionLUT + @return Any of the UIViewContentMode constants, or an int if the string is a number. If the string is not recognized, UIViewContentModeScaleToFill is returned. + */ +AS_EXTERN UIViewContentMode ASDisplayNodeUIContentModeFromNSString(NSString *string); + +/** + Maps a value of UIViewContentMode to a corresponding contentsGravity + It is worth noting that UIKit and CA have inverse definitions of "top" and "bottom" on iOS, so the corresponding contentsGravity for UIViewContentModeTopLeft is kCAContentsGravityBottomLeft + @param contentMode A content mode except for UIViewContentModeRedraw, which has no corresponding contentsGravity (it corresponds to needsDisplayOnBoundsChange = YES) + @return An NSString constant from the documentation, eg kCAGravityCenter... or nil if there is no corresponding contentsGravity. Will assert if contentMode is unknown. + */ +AS_EXTERN NSString *const ASDisplayNodeCAContentsGravityFromUIContentMode(UIViewContentMode contentMode); + +/** + Maps a value of contentsGravity to a corresponding UIViewContentMode + It is worth noting that UIKit and CA have inverse definitions of "top" and "bottom" on iOS, so the corresponding contentMode for kCAContentsGravityBottomLeft is UIViewContentModeTopLeft + @param contentsGravity A contents gravity + @return A UIViewContentMode constant from UIView.h, eg UIViewContentModeCenter..., or UIViewContentModeScaleToFill if contentsGravity is not one of the CA constants. Will assert if the contentsGravity is unknown. + */ +AS_EXTERN UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *const contentsGravity); + +/** + Use this to create a stretchable appropriate to approximate a filled rectangle, but with antialiasing on the edges when not pixel-aligned. It's best to keep the layer this image is added to with contentsScale equal to the scale of the final transform to screen space so it is able to antialias appropriately even when you shrink or grow the layer. + @param color the fill color to use in the center of the image + @param innerSize Unfortunately, 4 seems to be the smallest inner size that works if you're applying this stretchable to a larger box, whereas it does not display correctly for larger boxes. Thus some adjustment is necessary for the size of box you're displaying. If you're showing a 1px horizontal line, pass 1 height and at least 4 width. 2px vertical line: 2px wide, 4px high. Passing an innerSize greater that you desire is wasteful + */ +AS_EXTERN UIImage *ASDisplayNodeStretchableBoxContentsWithColor(UIColor *color, CGSize innerSize); + +/** + Checks whether a layer has ongoing animations + @param layer A layer to check if animations are ongoing + @return YES if the layer has ongoing animations, otherwise NO + */ +AS_EXTERN BOOL ASDisplayNodeLayerHasAnimations(CALayer *layer); + +// This function is a less generalized version of ASDisplayNodeSetResizableContents. +AS_EXTERN void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UIImage *image) ASDISPLAYNODE_DEPRECATED; diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASCoreAnimationExtras.mm b/submodules/AsyncDisplayKit/Source/Private/_ASCoreAnimationExtras.mm new file mode 100644 index 0000000000..b55bd6442f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASCoreAnimationExtras.mm @@ -0,0 +1,187 @@ +// +// _ASCoreAnimationExtras.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UIImage *image) +{ + ASDisplayNodeSetResizableContents(layer, image); +} + +void ASDisplayNodeSetResizableContents(id obj, UIImage *image) +{ + // FIXME (https://github.com/TextureGroup/Texture/issues/1046): This method does not currently handle UIImageResizingModeTile, which is the default. + // See also https://developer.apple.com/documentation/uikit/uiimage/1624157-resizingmode?language=objc + // I'm not sure of a way to use CALayer directly to perform such tiling on the GPU, though the stretch is handled by the GPU, + // and CALayer.h documents the fact that contentsCenter is used to stretch the pixels. + + if (image) { + ASDisplayNodeCAssert(image.resizingMode == UIImageResizingModeStretch || UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero), + @"Image insets must be all-zero or resizingMode has to be UIImageResizingModeStretch. XCode assets default value is UIImageResizingModeTile which is not supported by Texture because of GPU-accelerated CALayer features."); + + // Image may not actually be stretchable in one or both dimensions; this is handled + obj.contents = (id)[image CGImage]; + obj.contentsScale = [image scale]; + obj.rasterizationScale = [image scale]; + CGSize imageSize = [image size]; + + UIEdgeInsets insets = [image capInsets]; + + // These are lifted from what UIImageView does by experimentation. Without these exact values, the stretching is slightly off. + const CGFloat halfPixelFudge = 0.49f; + const CGFloat otherPixelFudge = 0.02f; + // Convert to unit coordinates for the contentsCenter property. + CGRect contentsCenter = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); + if (insets.left > 0 || insets.right > 0) { + contentsCenter.origin.x = ((insets.left + halfPixelFudge) / imageSize.width); + contentsCenter.size.width = (imageSize.width - (insets.left + insets.right + 1.f) + otherPixelFudge) / imageSize.width; + } + if (insets.top > 0 || insets.bottom > 0) { + contentsCenter.origin.y = ((insets.top + halfPixelFudge) / imageSize.height); + contentsCenter.size.height = (imageSize.height - (insets.top + insets.bottom + 1.f) + otherPixelFudge) / imageSize.height; + } + obj.contentsGravity = kCAGravityResize; + obj.contentsCenter = contentsCenter; + + } else { + obj.contents = nil; + } +} + + +struct _UIContentModeStringLUTEntry { + UIViewContentMode contentMode; + NSString *const string; +}; + +static const _UIContentModeStringLUTEntry *UIContentModeCAGravityLUT(size_t *count) +{ + // Initialize this in a function (instead of at file level) to avoid + // startup initialization time. + static const _UIContentModeStringLUTEntry sUIContentModeCAGravityLUT[] = { + {UIViewContentModeScaleToFill, kCAGravityResize}, + {UIViewContentModeScaleAspectFit, kCAGravityResizeAspect}, + {UIViewContentModeScaleAspectFill, kCAGravityResizeAspectFill}, + {UIViewContentModeCenter, kCAGravityCenter}, + {UIViewContentModeTop, kCAGravityBottom}, + {UIViewContentModeBottom, kCAGravityTop}, + {UIViewContentModeLeft, kCAGravityLeft}, + {UIViewContentModeRight, kCAGravityRight}, + {UIViewContentModeTopLeft, kCAGravityBottomLeft}, + {UIViewContentModeTopRight, kCAGravityBottomRight}, + {UIViewContentModeBottomLeft, kCAGravityTopLeft}, + {UIViewContentModeBottomRight, kCAGravityTopRight}, + }; + *count = sizeof(sUIContentModeCAGravityLUT) / sizeof(sUIContentModeCAGravityLUT[0]); + return sUIContentModeCAGravityLUT; +} + +static const _UIContentModeStringLUTEntry *UIContentModeDescriptionLUT(size_t *count) +{ + // Initialize this in a function (instead of at file level) to avoid + // startup initialization time. + static const _UIContentModeStringLUTEntry sUIContentModeDescriptionLUT[] = { + {UIViewContentModeScaleToFill, @"scaleToFill"}, + {UIViewContentModeScaleAspectFit, @"aspectFit"}, + {UIViewContentModeScaleAspectFill, @"aspectFill"}, + {UIViewContentModeRedraw, @"redraw"}, + {UIViewContentModeCenter, @"center"}, + {UIViewContentModeTop, @"top"}, + {UIViewContentModeBottom, @"bottom"}, + {UIViewContentModeLeft, @"left"}, + {UIViewContentModeRight, @"right"}, + {UIViewContentModeTopLeft, @"topLeft"}, + {UIViewContentModeTopRight, @"topRight"}, + {UIViewContentModeBottomLeft, @"bottomLeft"}, + {UIViewContentModeBottomRight, @"bottomRight"}, + }; + *count = sizeof(sUIContentModeDescriptionLUT) / sizeof(sUIContentModeDescriptionLUT[0]); + return sUIContentModeDescriptionLUT; +} + +NSString *ASDisplayNodeNSStringFromUIContentMode(UIViewContentMode contentMode) +{ + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeDescriptionLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (lut[i].contentMode == contentMode) { + return lut[i].string; + } + } + return [NSString stringWithFormat:@"%d", (int)contentMode]; +} + +UIViewContentMode ASDisplayNodeUIContentModeFromNSString(NSString *string) +{ + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeDescriptionLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (ASObjectIsEqual(lut[i].string, string)) { + return lut[i].contentMode; + } + } + return UIViewContentModeScaleToFill; +} + +NSString *const ASDisplayNodeCAContentsGravityFromUIContentMode(UIViewContentMode contentMode) +{ + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeCAGravityLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (lut[i].contentMode == contentMode) { + return lut[i].string; + } + } + ASDisplayNodeCAssert(contentMode == UIViewContentModeRedraw, @"Encountered an unknown contentMode %ld. Is this a new version of iOS?", (long)contentMode); + // Redraw is ok to return nil. + return nil; +} + +#define ContentModeCacheSize 10 +UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *const contentsGravity) +{ + static int currentCacheIndex = 0; + static NSMutableArray *cachedStrings = [NSMutableArray arrayWithCapacity:ContentModeCacheSize]; + static UIViewContentMode cachedModes[ContentModeCacheSize] = {}; + + NSInteger foundCacheIndex = [cachedStrings indexOfObjectIdenticalTo:contentsGravity]; + if (foundCacheIndex != NSNotFound && foundCacheIndex < ContentModeCacheSize) { + return cachedModes[foundCacheIndex]; + } + + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeCAGravityLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (ASObjectIsEqual(lut[i].string, contentsGravity)) { + UIViewContentMode foundContentMode = lut[i].contentMode; + + if (currentCacheIndex < ContentModeCacheSize) { + // Cache the input value. This is almost always a different pointer than in our LUT and will frequently + // be the same value for an overwhelming majority of inputs. + [cachedStrings addObject:contentsGravity]; + cachedModes[currentCacheIndex] = foundContentMode; + currentCacheIndex++; + } + + return foundContentMode; + } + } + + ASDisplayNodeCAssert(contentsGravity, @"Encountered an unknown contentsGravity \"%@\". Is this a new version of iOS?", contentsGravity); + ASDisplayNodeCAssert(!contentsGravity, @"You passed nil to ASDisplayNodeUIContentModeFromCAContentsGravity. We're falling back to resize, but this is probably a bug."); + // If asserts disabled, fall back to this + return UIViewContentModeScaleToFill; +} + +BOOL ASDisplayNodeLayerHasAnimations(CALayer *layer) +{ + return (layer.animationKeys.count != 0); +} diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASHierarchyChangeSet.h b/submodules/AsyncDisplayKit/Source/Private/_ASHierarchyChangeSet.h new file mode 100644 index 0000000000..800be37517 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASHierarchyChangeSet.h @@ -0,0 +1,218 @@ +// +// _ASHierarchyChangeSet.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NSUInteger ASDataControllerAnimationOptions; + +typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { + /** + * A reload change, as submitted by the user. When a change set is + * completed, these changes are decomposed into delete-insert pairs + * and combined with the original deletes and inserts of the change. + */ + _ASHierarchyChangeTypeReload, + + /** + * A change that was either an original delete, or the first + * part of a decomposed reload. + */ + _ASHierarchyChangeTypeDelete, + + /** + * A change that was submitted by the user as a delete. + */ + _ASHierarchyChangeTypeOriginalDelete, + + /** + * A change that was either an original insert, or the second + * part of a decomposed reload. + */ + _ASHierarchyChangeTypeInsert, + + /** + * A change that was submitted by the user as an insert. + */ + _ASHierarchyChangeTypeOriginalInsert +}; + +/** + * Returns YES if the given change type is either .Insert or .Delete, NO otherwise. + * Other change types – .Reload, .OriginalInsert, .OriginalDelete – are + * intermediary types used while building the change set. All changes will + * be reduced to either .Insert or .Delete when the change is marked completed. + */ +BOOL ASHierarchyChangeTypeIsFinal(_ASHierarchyChangeType changeType); + +NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); + +@interface _ASHierarchySectionChange : NSObject + +// FIXME: Generalize this to `changeMetadata` dict? +@property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; + +@property (nonatomic, readonly) NSIndexSet *indexSet; + +@property (nonatomic, readonly) _ASHierarchyChangeType changeType; + +/** + * If this is a .OriginalInsert or .OriginalDelete change, this returns a copied change + * with type .Insert or .Delete. Calling this on changes of other types is an error. + */ +- (_ASHierarchySectionChange *)changeByFinalizingType; + +@end + +@interface _ASHierarchyItemChange : NSObject + +@property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; + +/// Index paths are sorted descending for changeType .Delete, ascending otherwise +@property (nonatomic, readonly) NSArray *indexPaths; + +@property (nonatomic, readonly) _ASHierarchyChangeType changeType; + ++ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes; + +/** + * If this is a .OriginalInsert or .OriginalDelete change, this returns a copied change + * with type .Insert or .Delete. Calling this on changes of other types is an error. + */ +- (_ASHierarchyItemChange *)changeByFinalizingType; + +@end + +@interface _ASHierarchyChangeSet : NSObject + +/// @precondition The change set must be completed. +@property (nonatomic, readonly) NSIndexSet *deletedSections; + +/// @precondition The change set must be completed. +@property (nonatomic, readonly) NSIndexSet *insertedSections; + +@property (nonatomic, readonly) BOOL completed; + +/// Whether or not changes should be animated. +// TODO: if any update in this chagne set is non-animated, the whole update should be non-animated. +@property (nonatomic) BOOL animated; + +@property (nonatomic, readonly) BOOL includesReloadData; + +/// Indicates whether the change set is empty, that is it includes neither reload data nor per item or section changes. +@property (nonatomic, readonly) BOOL isEmpty; + +/// The count of new ASCellNodes that can undergo async layout calculation. May be zero if all UIKit passthrough cells. +@property (nonatomic, assign) NSUInteger countForAsyncLayout; + +/// The top-level activity for this update. +@property (nonatomic, OS_ACTIVITY_NULLABLE) os_activity_t rootActivity; + +/// The activity for submitting this update i.e. between -beginUpdates and -endUpdates. +@property (nonatomic, OS_ACTIVITY_NULLABLE) os_activity_t submitActivity; + +- (instancetype)initWithOldData:(std::vector)oldItemCounts NS_DESIGNATED_INITIALIZER; + +/** + * Append the given completion handler to the combined @c completionHandler. + * + * @discussion Since batch updates can be nested, we have to support multiple + * completion handlers per update. + * + * @precondition The change set must not be completed. + */ +- (void)addCompletionHandler:(nullable void(^)(BOOL finished))completion; + +/** + * Execute the combined completion handler. + * + * @warning The completion block is discarded after reading because it may have captured + * significant resources that we would like to reclaim as soon as possible. + */ +- (void)executeCompletionHandlerWithFinished:(BOOL)finished; + +/** + * Get the section index after the update for the given section before the update. + * + * @precondition The change set must be completed. + * @return The new section index, or NSNotFound if the given section was deleted. + */ +- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection; + +/** + * A table that maps old section indexes to new section indexes. + */ +@property (nonatomic, readonly) ASIntegerMap *sectionMapping; + +/** + * A table that maps new section indexes to old section indexes. + */ +@property (nonatomic, readonly) ASIntegerMap *reverseSectionMapping; + +/** + * A table that provides the item mapping for the old section. If the section was deleted + * or is out of bounds, returns the empty table. + */ +- (ASIntegerMap *)itemMappingInSection:(NSInteger)oldSection; + +/** + * A table that provides the reverse item mapping for the new section. If the section was inserted + * or is out of bounds, returns the empty table. + */ +- (ASIntegerMap *)reverseItemMappingInSection:(NSInteger)newSection; + +/** + * Get the old item index path for the given new index path. + * + * @precondition The change set must be completed. + * @return The old index path, or nil if the given item was inserted. + */ +- (nullable NSIndexPath *)oldIndexPathForNewIndexPath:(NSIndexPath *)indexPath; + +/** + * Get the new item index path for the given old index path. + * + * @precondition The change set must be completed. + * @return The new index path, or nil if the given item was deleted. + */ +- (nullable NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath; + +/// Call this once the change set has been constructed to prevent future modifications to the changeset. Calling this more than once is a programmer error. +/// NOTE: Calling this method will cause the changeset to convert all reloads into delete/insert pairs. +- (void)markCompletedWithNewItemCounts:(std::vector)newItemCounts; + +- (nullable NSArray <_ASHierarchySectionChange *> *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; + +- (nullable NSArray <_ASHierarchyItemChange *> *)itemChangesOfType:(_ASHierarchyChangeType)changeType; + +/// Returns all item indexes affected by changes of the given type in the given section. +- (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section; + +- (void)reloadData; +- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection animationOptions:(ASDataControllerAnimationOptions)options; +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath animationOptions:(ASDataControllerAnimationOptions)options; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASHierarchyChangeSet.mm b/submodules/AsyncDisplayKit/Source/Private/_ASHierarchyChangeSet.mm new file mode 100644 index 0000000000..77d4d1af3f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASHierarchyChangeSet.mm @@ -0,0 +1,1016 @@ +// +// _ASHierarchyChangeSet.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef MINIMAL_ASDK + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// If assertions are enabled and they haven't forced us to suppress the exception, +// then throw, otherwise log. +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + #define ASFailUpdateValidation(...)\ + _Pragma("clang diagnostic push")\ + _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")\ + if ([ASDisplayNode suppressesInvalidCollectionUpdateExceptions]) {\ + NSLog(__VA_ARGS__);\ + } else {\ + NSLog(__VA_ARGS__);\ + [NSException raise:ASCollectionInvalidUpdateException format:__VA_ARGS__];\ + }\ + _Pragma("clang diagnostic pop") +#else + #define ASFailUpdateValidation(...) NSLog(__VA_ARGS__); +#endif + +BOOL ASHierarchyChangeTypeIsFinal(_ASHierarchyChangeType changeType) { + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + case _ASHierarchyChangeTypeDelete: + return YES; + default: + return NO; + } +} + +NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) +{ + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return @"Insert"; + case _ASHierarchyChangeTypeOriginalInsert: + return @"OriginalInsert"; + case _ASHierarchyChangeTypeDelete: + return @"Delete"; + case _ASHierarchyChangeTypeOriginalDelete: + return @"OriginalDelete"; + case _ASHierarchyChangeTypeReload: + return @"Reload"; + default: + return @"(invalid)"; + } +} + +@interface _ASHierarchySectionChange () +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + On return `changes` is sorted according to the change type with changes coalesced by animationOptions + Assumes: `changes` all have the same changeType + */ ++ (void)sortAndCoalesceSectionChanges:(NSMutableArray<_ASHierarchySectionChange *> *)changes; + +/// Returns all the indexes from all the `indexSet`s of the given `_ASHierarchySectionChange` objects. ++ (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray *)changes; + ++ (NSString *)smallDescriptionForSectionChanges:(NSArray<_ASHierarchySectionChange *> *)changes; +@end + +@interface _ASHierarchyItemChange () +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted; + +/** + On return `changes` is sorted according to the change type with changes coalesced by animationOptions + Assumes: `changes` all have the same changeType + */ ++ (void)sortAndCoalesceItemChanges:(NSMutableArray<_ASHierarchyItemChange *> *)changes ignoringChangesInSections:(NSIndexSet *)sections; + ++ (NSString *)smallDescriptionForItemChanges:(NSArray<_ASHierarchyItemChange *> *)changes; + ++ (void)ensureItemChanges:(NSArray<_ASHierarchyItemChange *> *)changes ofSameType:(_ASHierarchyChangeType)changeType; +@end + +@interface _ASHierarchyChangeSet () + +// array index is old section index, map goes oldItem -> newItem +@property (nonatomic, readonly) NSMutableArray *itemMappings; + +// array index is new section index, map goes newItem -> oldItem +@property (nonatomic, readonly) NSMutableArray *reverseItemMappings; + +@property (nonatomic, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges; +@property (nonatomic, readonly) NSMutableArray<_ASHierarchyItemChange *> *originalInsertItemChanges; + +@property (nonatomic, readonly) NSMutableArray<_ASHierarchyItemChange *> *deleteItemChanges; +@property (nonatomic, readonly) NSMutableArray<_ASHierarchyItemChange *> *originalDeleteItemChanges; + +@property (nonatomic, readonly) NSMutableArray<_ASHierarchyItemChange *> *reloadItemChanges; + +@property (nonatomic, readonly) NSMutableArray<_ASHierarchySectionChange *> *insertSectionChanges; +@property (nonatomic, readonly) NSMutableArray<_ASHierarchySectionChange *> *originalInsertSectionChanges; + +@property (nonatomic, readonly) NSMutableArray<_ASHierarchySectionChange *> *deleteSectionChanges; +@property (nonatomic, readonly) NSMutableArray<_ASHierarchySectionChange *> *originalDeleteSectionChanges; + +@property (nonatomic, readonly) NSMutableArray<_ASHierarchySectionChange *> *reloadSectionChanges; + +@end + +@implementation _ASHierarchyChangeSet { + NSUInteger _countForAsyncLayout; + std::vector _oldItemCounts; + std::vector _newItemCounts; + void (^_completionHandler)(BOOL finished); +} +@synthesize sectionMapping = _sectionMapping; +@synthesize reverseSectionMapping = _reverseSectionMapping; +@synthesize itemMappings = _itemMappings; +@synthesize reverseItemMappings = _reverseItemMappings; +@synthesize countForAsyncLayout = _countForAsyncLayout; + +- (instancetype)init +{ + ASFailUpdateValidation(@"_ASHierarchyChangeSet: -init is not supported. Call -initWithOldData:"); + return [self initWithOldData:std::vector()]; +} + +- (instancetype)initWithOldData:(std::vector)oldItemCounts +{ + self = [super init]; + if (self) { + _oldItemCounts = oldItemCounts; + + _originalInsertItemChanges = [[NSMutableArray alloc] init]; + _insertItemChanges = [[NSMutableArray alloc] init]; + _originalDeleteItemChanges = [[NSMutableArray alloc] init]; + _deleteItemChanges = [[NSMutableArray alloc] init]; + _reloadItemChanges = [[NSMutableArray alloc] init]; + + _originalInsertSectionChanges = [[NSMutableArray alloc] init]; + _insertSectionChanges = [[NSMutableArray alloc] init]; + _originalDeleteSectionChanges = [[NSMutableArray alloc] init]; + _deleteSectionChanges = [[NSMutableArray alloc] init]; + _reloadSectionChanges = [[NSMutableArray alloc] init]; + } + return self; +} + +#pragma mark External API + +- (BOOL)isEmpty +{ + return (! _includesReloadData) && (! [self _includesPerItemOrSectionChanges]); +} + +- (void)addCompletionHandler:(void (^)(BOOL))completion +{ + [self _ensureNotCompleted]; + if (completion == nil) { + return; + } + + void (^oldCompletionHandler)(BOOL finished) = _completionHandler; + _completionHandler = ^(BOOL finished) { + if (oldCompletionHandler != nil) { + oldCompletionHandler(finished); + } + completion(finished); + }; +} + +- (void)executeCompletionHandlerWithFinished:(BOOL)finished +{ + if (_completionHandler != nil) { + _completionHandler(finished); + _completionHandler = nil; + } +} + +- (void)markCompletedWithNewItemCounts:(std::vector)newItemCounts +{ + NSAssert(!_completed, @"Attempt to mark already-completed changeset as completed."); + _completed = YES; + _newItemCounts = newItemCounts; + [self _sortAndCoalesceChangeArrays]; + [self _validateUpdate]; +} + +- (NSArray *)sectionChangesOfType:(_ASHierarchyChangeType)changeType +{ + [self _ensureCompleted]; + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return _insertSectionChanges; + case _ASHierarchyChangeTypeReload: + return _reloadSectionChanges; + case _ASHierarchyChangeTypeDelete: + return _deleteSectionChanges; + case _ASHierarchyChangeTypeOriginalDelete: + return _originalDeleteSectionChanges; + case _ASHierarchyChangeTypeOriginalInsert: + return _originalInsertSectionChanges; + default: + NSAssert(NO, @"Request for section changes with invalid type: %lu", (long)changeType); + return nil; + } +} + +- (NSArray *)itemChangesOfType:(_ASHierarchyChangeType)changeType +{ + [self _ensureCompleted]; + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return _insertItemChanges; + case _ASHierarchyChangeTypeReload: + return _reloadItemChanges; + case _ASHierarchyChangeTypeDelete: + return _deleteItemChanges; + case _ASHierarchyChangeTypeOriginalInsert: + return _originalInsertItemChanges; + case _ASHierarchyChangeTypeOriginalDelete: + return _originalDeleteItemChanges; + default: + NSAssert(NO, @"Request for item changes with invalid type: %lu", (long)changeType); + return nil; + } +} + +- (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section +{ + [self _ensureCompleted]; + const auto result = [[NSMutableIndexSet alloc] init]; + for (_ASHierarchyItemChange *change in [self itemChangesOfType:changeType]) { + [result addIndexes:[NSIndexSet as_indexSetFromIndexPaths:change.indexPaths inSection:section]]; + } + return result; +} + +- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection +{ + return [self.sectionMapping integerForKey:oldSection]; +} + +- (NSUInteger)oldSectionForNewSection:(NSUInteger)newSection +{ + return [self.reverseSectionMapping integerForKey:newSection]; +} + +- (ASIntegerMap *)sectionMapping +{ + ASDisplayNodeAssertNotNil(_deletedSections, @"Cannot call %s before `markCompleted` returns.", sel_getName(_cmd)); + ASDisplayNodeAssertNotNil(_insertedSections, @"Cannot call %s before `markCompleted` returns.", sel_getName(_cmd)); + [self _ensureCompleted]; + if (_sectionMapping == nil) { + _sectionMapping = [ASIntegerMap mapForUpdateWithOldCount:_oldItemCounts.size() deleted:_deletedSections inserted:_insertedSections]; + } + return _sectionMapping; +} + +- (ASIntegerMap *)reverseSectionMapping +{ + if (_reverseSectionMapping == nil) { + _reverseSectionMapping = [self.sectionMapping inverseMap]; + } + return _reverseSectionMapping; +} + +- (NSMutableArray *)itemMappings +{ + [self _ensureCompleted]; + + if (_itemMappings == nil) { + _itemMappings = [[NSMutableArray alloc] init]; + const auto insertMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_originalInsertItemChanges]; + const auto deleteMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_originalDeleteItemChanges]; + NSInteger oldSection = 0; + for (NSInteger oldCount : _oldItemCounts) { + NSInteger newSection = [self newSectionForOldSection:oldSection]; + ASIntegerMap *table; + if (newSection == NSNotFound) { + table = ASIntegerMap.emptyMap; + } else { + table = [ASIntegerMap mapForUpdateWithOldCount:oldCount deleted:deleteMap[@(oldSection)] inserted:insertMap[@(newSection)]]; + } + _itemMappings[oldSection] = table; + oldSection++; + } + } + return _itemMappings; +} + +- (NSMutableArray *)reverseItemMappings +{ + [self _ensureCompleted]; + + if (_reverseItemMappings == nil) { + _reverseItemMappings = [[NSMutableArray alloc] init]; + for (NSInteger newSection = 0; newSection < _newItemCounts.size(); newSection++) { + NSInteger oldSection = [self oldSectionForNewSection:newSection]; + ASIntegerMap *table; + if (oldSection == NSNotFound) { + table = ASIntegerMap.emptyMap; + } else { + table = [[self itemMappingInSection:oldSection] inverseMap]; + } + _reverseItemMappings[newSection] = table; + } + } + return _reverseItemMappings; +} + +- (ASIntegerMap *)itemMappingInSection:(NSInteger)oldSection +{ + if (self.includesReloadData || oldSection >= _oldItemCounts.size()) { + return ASIntegerMap.emptyMap; + } + return self.itemMappings[oldSection]; +} + +- (ASIntegerMap *)reverseItemMappingInSection:(NSInteger)newSection +{ + if (self.includesReloadData || newSection >= _newItemCounts.size()) { + return ASIntegerMap.emptyMap; + } + return self.reverseItemMappings[newSection]; +} + +- (NSIndexPath *)oldIndexPathForNewIndexPath:(NSIndexPath *)indexPath +{ + [self _ensureCompleted]; + NSInteger newSection = indexPath.section; + NSInteger oldSection = [self oldSectionForNewSection:newSection]; + if (oldSection == NSNotFound) { + return nil; + } + NSInteger oldItem = [[self reverseItemMappingInSection:newSection] integerForKey:indexPath.item]; + if (oldItem == NSNotFound) { + return nil; + } + return [NSIndexPath indexPathForItem:oldItem inSection:oldSection]; +} + +- (NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath +{ + [self _ensureCompleted]; + NSInteger oldSection = indexPath.section; + NSInteger newSection = [self newSectionForOldSection:oldSection]; + if (newSection == NSNotFound) { + return nil; + } + NSInteger newItem = [[self itemMappingInSection:oldSection] integerForKey:indexPath.item]; + if (newItem == NSNotFound) { + return nil; + } + return [NSIndexPath indexPathForItem:newItem inSection:newSection]; +} + +- (void)reloadData +{ + [self _ensureNotCompleted]; + NSAssert(_includesReloadData == NO, @"Attempt to reload data multiple times %@", self); + _includesReloadData = YES; +} + +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalDelete indexPaths:indexPaths animationOptions:options presorted:NO]; + [_originalDeleteItemChanges addObject:change]; +} + +- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalDelete indexSet:sections animationOptions:options]; + [_originalDeleteSectionChanges addObject:change]; +} + +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalInsert indexPaths:indexPaths animationOptions:options presorted:NO]; + [_originalInsertItemChanges addObject:change]; +} + +- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalInsert indexSet:sections animationOptions:options]; + [_originalInsertSectionChanges addObject:change]; +} + +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexPaths:indexPaths animationOptions:options presorted:NO]; + [_reloadItemChanges addObject:change]; +} + +- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexSet:sections animationOptions:options]; + [_reloadSectionChanges addObject:change]; +} + +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath animationOptions:(ASDataControllerAnimationOptions)options +{ + /** + * TODO: Proper move implementation. + */ + [self deleteItems:@[ indexPath ] animationOptions:options]; + [self insertItems:@[ newIndexPath ] animationOptions:options]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection animationOptions:(ASDataControllerAnimationOptions)options +{ + /** + * TODO: Proper move implementation. + */ + [self deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:options]; + [self insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:options]; +} + +#pragma mark Private + +- (BOOL)_ensureNotCompleted +{ + NSAssert(!_completed, @"Attempt to modify completed changeset %@", self); + return !_completed; +} + +- (BOOL)_ensureCompleted +{ + NSAssert(_completed, @"Attempt to process incomplete changeset %@", self); + return _completed; +} + +- (void)_sortAndCoalesceChangeArrays +{ + if (_includesReloadData) { + return; + } + + @autoreleasepool { + + // Split reloaded sections into [delete(oldIndex), insert(newIndex)] + + // Give these their "pre-reloads" values. Once we add in the reloads we'll re-process them. + _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_originalDeleteSectionChanges]; + _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_originalInsertSectionChanges]; + for (_ASHierarchySectionChange *originalDeleteSectionChange in _originalDeleteSectionChanges) { + [_deleteSectionChanges addObject:[originalDeleteSectionChange changeByFinalizingType]]; + } + for (_ASHierarchySectionChange *originalInsertSectionChange in _originalInsertSectionChanges) { + [_insertSectionChanges addObject:[originalInsertSectionChange changeByFinalizingType]]; + } + + for (_ASHierarchySectionChange *change in _reloadSectionChanges) { + NSIndexSet *newSections = [change.indexSet as_indexesByMapping:^(NSUInteger idx) { + NSUInteger newSec = [self newSectionForOldSection:idx]; + ASDisplayNodeAssert(newSec != NSNotFound, @"Request to reload and delete same section %tu", idx); + return newSec; + }]; + + _ASHierarchySectionChange *deleteChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:change.indexSet animationOptions:change.animationOptions]; + [_deleteSectionChanges addObject:deleteChange]; + + _ASHierarchySectionChange *insertChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:newSections animationOptions:change.animationOptions]; + [_insertSectionChanges addObject:insertChange]; + } + + [_ASHierarchySectionChange sortAndCoalesceSectionChanges:_deleteSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceSectionChanges:_insertSectionChanges]; + _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; + _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; + + // Split reloaded items into [delete(oldIndexPath), insert(newIndexPath)] + for (_ASHierarchyItemChange *originalDeleteItemChange in _originalDeleteItemChanges) { + [_deleteItemChanges addObject:[originalDeleteItemChange changeByFinalizingType]]; + } + for (_ASHierarchyItemChange *originalInsertItemChange in _originalInsertItemChanges) { + [_insertItemChanges addObject:[originalInsertItemChange changeByFinalizingType]]; + } + + [_ASHierarchyItemChange ensureItemChanges:_insertItemChanges ofSameType:_ASHierarchyChangeTypeInsert]; + [_ASHierarchyItemChange ensureItemChanges:_deleteItemChanges ofSameType:_ASHierarchyChangeTypeDelete]; + + for (_ASHierarchyItemChange *change in _reloadItemChanges) { + NSAssert(change.changeType == _ASHierarchyChangeTypeReload, @"It must be a reload change to be in here"); + + const auto newIndexPaths = ASArrayByFlatMapping(change.indexPaths, NSIndexPath *indexPath, [self newIndexPathForOldIndexPath:indexPath]); + + // All reload changes are translated into deletes and inserts + // We delete the items that needs reload together with other deleted items, at their original index + _ASHierarchyItemChange *deleteItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:change.indexPaths animationOptions:change.animationOptions presorted:NO]; + [_deleteItemChanges addObject:deleteItemChangeFromReloadChange]; + // We insert the items that needs reload together with other inserted items, at their future index + _ASHierarchyItemChange *insertItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:newIndexPaths animationOptions:change.animationOptions presorted:NO]; + [_insertItemChanges addObject:insertItemChangeFromReloadChange]; + } + + // Ignore item deletes in reloaded/deleted sections. + [_ASHierarchyItemChange sortAndCoalesceItemChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; + + // Ignore item inserts in reloaded(new)/inserted sections. + [_ASHierarchyItemChange sortAndCoalesceItemChanges:_insertItemChanges ignoringChangesInSections:_insertedSections]; + } +} + +- (void)_validateUpdate +{ + // If reloadData exists, ignore other changes + if (_includesReloadData) { + if ([self _includesPerItemOrSectionChanges]) { + NSLog(@"Warning: A reload data shouldn't be used in conjuntion with other updates."); + } + return; + } + + NSIndexSet *allReloadedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_reloadSectionChanges]; + + NSInteger newSectionCount = _newItemCounts.size(); + NSInteger oldSectionCount = _oldItemCounts.size(); + + NSInteger insertedSectionCount = _insertedSections.count; + NSInteger deletedSectionCount = _deletedSections.count; + // Assert that the new section count is correct. + if (newSectionCount != oldSectionCount + insertedSectionCount - deletedSectionCount) { + ASFailUpdateValidation(@"Invalid number of sections. The number of sections after the update (%ld) must be equal to the number of sections before the update (%ld) plus or minus the number of sections inserted or deleted (%ld inserted, %ld deleted)", (long)newSectionCount, (long)oldSectionCount, (long)insertedSectionCount, (long)deletedSectionCount); + return; + } + + // Assert that no invalid deletes/reloads happened. + NSInteger invalidSectionDelete = NSNotFound; + if (oldSectionCount == 0) { + invalidSectionDelete = _deletedSections.firstIndex; + } else { + invalidSectionDelete = [_deletedSections indexGreaterThanIndex:oldSectionCount - 1]; + } + if (invalidSectionDelete != NSNotFound) { + ASFailUpdateValidation(@"Attempt to delete section %ld but there are only %ld sections before the update.", (long)invalidSectionDelete, (long)oldSectionCount); + return; + } + + for (_ASHierarchyItemChange *change in _deleteItemChanges) { + for (NSIndexPath *indexPath in change.indexPaths) { + // Assert that item delete happened in a valid section. + NSInteger section = indexPath.section; + NSInteger item = indexPath.item; + if (section >= oldSectionCount) { + ASFailUpdateValidation(@"Attempt to delete item %ld from section %ld, but there are only %ld sections before the update.", (long)item, (long)section, (long)oldSectionCount); + return; + } + + // Assert that item delete happened to a valid item. + NSInteger oldItemCount = _oldItemCounts[section]; + if (item >= oldItemCount) { + ASFailUpdateValidation(@"Attempt to delete item %ld from section %ld, which only contains %ld items before the update.", (long)item, (long)section, (long)oldItemCount); + return; + } + } + } + + for (_ASHierarchyItemChange *change in _insertItemChanges) { + for (NSIndexPath *indexPath in change.indexPaths) { + NSInteger section = indexPath.section; + NSInteger item = indexPath.item; + // Assert that item insert happened in a valid section. + if (section >= newSectionCount) { + ASFailUpdateValidation(@"Attempt to insert item %ld into section %ld, but there are only %ld sections after the update.", (long)item, (long)section, (long)newSectionCount); + return; + } + + // Assert that item delete happened to a valid item. + NSInteger newItemCount = _newItemCounts[section]; + if (item >= newItemCount) { + ASFailUpdateValidation(@"Attempt to insert item %ld into section %ld, which only contains %ld items after the update.", (long)item, (long)section, (long)newItemCount); + return; + } + } + } + + // Assert that no sections were inserted out of bounds. + NSInteger invalidSectionInsert = NSNotFound; + if (newSectionCount == 0) { + invalidSectionInsert = _insertedSections.firstIndex; + } else { + invalidSectionInsert = [_insertedSections indexGreaterThanIndex:newSectionCount - 1]; + } + if (invalidSectionInsert != NSNotFound) { + ASFailUpdateValidation(@"Attempt to insert section %ld but there are only %ld sections after the update.", (long)invalidSectionInsert, (long)newSectionCount); + return; + } + + for (NSUInteger oldSection = 0; oldSection < oldSectionCount; oldSection++) { + NSInteger oldItemCount = _oldItemCounts[oldSection]; + // If section was reloaded, ignore. + if ([allReloadedSections containsIndex:oldSection]) { + continue; + } + + // If section was deleted, ignore. + NSUInteger newSection = [self newSectionForOldSection:oldSection]; + if (newSection == NSNotFound) { + continue; + } + + NSIndexSet *originalInsertedItems = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeOriginalInsert inSection:newSection]; + NSIndexSet *originalDeletedItems = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeOriginalDelete inSection:oldSection]; + NSIndexSet *reloadedItems = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeReload inSection:oldSection]; + + // Assert that no reloaded items were deleted. + NSInteger deletedReloadedItem = [originalDeletedItems as_intersectionWithIndexes:reloadedItems].firstIndex; + if (deletedReloadedItem != NSNotFound) { + ASFailUpdateValidation(@"Attempt to delete and reload the same item at index path %@", [NSIndexPath indexPathForItem:deletedReloadedItem inSection:oldSection]); + return; + } + + // Assert that the new item count is correct. + NSInteger newItemCount = _newItemCounts[newSection]; + NSInteger insertedItemCount = originalInsertedItems.count; + NSInteger deletedItemCount = originalDeletedItems.count; + if (newItemCount != oldItemCount + insertedItemCount - deletedItemCount) { + ASFailUpdateValidation(@"Invalid number of items in section %ld. The number of items after the update (%ld) must be equal to the number of items before the update (%ld) plus or minus the number of items inserted or deleted (%ld inserted, %ld deleted).", (long)oldSection, (long)newItemCount, (long)oldItemCount, (long)insertedItemCount, (long)deletedItemCount); + return; + } + } +} + +- (BOOL)_includesPerItemOrSectionChanges +{ + return 0 < (_originalDeleteSectionChanges.count + _originalDeleteItemChanges.count + +_originalInsertSectionChanges.count + _originalInsertItemChanges.count + + _reloadSectionChanges.count + _reloadItemChanges.count); +} + +#pragma mark - Debugging (Private) + +- (NSString *)description +{ + return ASObjectDescriptionMakeWithoutObject([self propertiesForDescription]); +} + +- (NSString *)debugDescription +{ + return ASObjectDescriptionMake(self, [self propertiesForDebugDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + if (_includesReloadData) { + [result addObject:@{ @"reloadData" : @"YES" }]; + } + if (_reloadSectionChanges.count > 0) { + [result addObject:@{ @"reloadSections" : [_ASHierarchySectionChange smallDescriptionForSectionChanges:_reloadSectionChanges] }]; + } + if (_reloadItemChanges.count > 0) { + [result addObject:@{ @"reloadItems" : [_ASHierarchyItemChange smallDescriptionForItemChanges:_reloadItemChanges] }]; + } + if (_originalDeleteSectionChanges.count > 0) { + [result addObject:@{ @"deleteSections" : [_ASHierarchySectionChange smallDescriptionForSectionChanges:_originalDeleteSectionChanges] }]; + } + if (_originalDeleteItemChanges.count > 0) { + [result addObject:@{ @"deleteItems" : [_ASHierarchyItemChange smallDescriptionForItemChanges:_originalDeleteItemChanges] }]; + } + if (_originalInsertSectionChanges.count > 0) { + [result addObject:@{ @"insertSections" : [_ASHierarchySectionChange smallDescriptionForSectionChanges:_originalInsertSectionChanges] }]; + } + if (_originalInsertItemChanges.count > 0) { + [result addObject:@{ @"insertItems" : [_ASHierarchyItemChange smallDescriptionForItemChanges:_originalInsertItemChanges] }]; + } + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + return [self propertiesForDescription]; +} + +@end + +@implementation _ASHierarchySectionChange + +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + self = [super init]; + if (self) { + ASDisplayNodeAssert(indexSet.count > 0, @"Request to create _ASHierarchySectionChange with no sections!"); + _changeType = changeType; + _indexSet = indexSet; + _animationOptions = animationOptions; + } + return self; +} + +- (_ASHierarchySectionChange *)changeByFinalizingType +{ + _ASHierarchyChangeType newType; + switch (_changeType) { + case _ASHierarchyChangeTypeOriginalInsert: + newType = _ASHierarchyChangeTypeInsert; + break; + case _ASHierarchyChangeTypeOriginalDelete: + newType = _ASHierarchyChangeTypeDelete; + break; + default: + ASFailUpdateValidation(@"Attempt to finalize section change of invalid type %@.", NSStringFromASHierarchyChangeType(_changeType)); + return self; + } + return [[_ASHierarchySectionChange alloc] initWithChangeType:newType indexSet:_indexSet animationOptions:_animationOptions]; +} + ++ (void)sortAndCoalesceSectionChanges:(NSMutableArray<_ASHierarchySectionChange *> *)changes +{ + _ASHierarchySectionChange *firstChange = changes.firstObject; + if (firstChange == nil) { + return; + } + _ASHierarchyChangeType type = [firstChange changeType]; + + ASDisplayNodeAssert(ASHierarchyChangeTypeIsFinal(type), @"Attempt to sort and coalesce section changes of intermediary type %@. Why?", NSStringFromASHierarchyChangeType(type)); + + // Lookup table [Int: AnimationOptions] + __block std::unordered_map animationOptions; + + // All changed indexes + NSMutableIndexSet *allIndexes = [NSMutableIndexSet new]; + + for (_ASHierarchySectionChange *change in changes) { + ASDataControllerAnimationOptions options = change.animationOptions; + NSIndexSet *indexes = change.indexSet; + [indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + animationOptions[i] = options; + } + }]; + [allIndexes addIndexes:indexes]; + } + + // Create new changes by grouping sorted changes by animation option + NSMutableArray *result = [[NSMutableArray alloc] init]; + + __block ASDataControllerAnimationOptions currentOptions = 0; + const auto currentIndexes = [[NSMutableIndexSet alloc] init]; + + BOOL reverse = type == _ASHierarchyChangeTypeDelete || type == _ASHierarchyChangeTypeOriginalDelete; + NSEnumerationOptions options = reverse ? NSEnumerationReverse : kNilOptions; + + [allIndexes enumerateRangesWithOptions:options usingBlock:^(NSRange range, BOOL * _Nonnull stop) { + NSInteger increment = reverse ? -1 : 1; + NSUInteger start = reverse ? NSMaxRange(range) - 1 : range.location; + NSInteger limit = reverse ? range.location - 1 : NSMaxRange(range); + for (NSInteger i = start; i != limit; i += increment) { + ASDataControllerAnimationOptions options = animationOptions[i]; + + // End the previous group if needed. + if (options != currentOptions && currentIndexes.count > 0) { + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:[currentIndexes copy] animationOptions:currentOptions]; + [result addObject:change]; + [currentIndexes removeAllIndexes]; + } + + // Start a new group if needed. + if (currentIndexes.count == 0) { + currentOptions = options; + } + + [currentIndexes addIndex:i]; + } + }]; + + // Finish up the last group. + if (currentIndexes.count > 0) { + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:[currentIndexes copy] animationOptions:currentOptions]; + [result addObject:change]; + } + + [changes setArray:result]; +} + ++ (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray<_ASHierarchySectionChange *> *)changes +{ + const auto indexes = [[NSMutableIndexSet alloc] init]; + for (_ASHierarchySectionChange *change in changes) { + [indexes addIndexes:change.indexSet]; + } + return indexes; +} + +#pragma mark - Debugging (Private) + ++ (NSString *)smallDescriptionForSectionChanges:(NSArray<_ASHierarchySectionChange *> *)changes +{ + NSMutableIndexSet *unionIndexSet = [NSMutableIndexSet indexSet]; + for (_ASHierarchySectionChange *change in changes) { + [unionIndexSet addIndexes:change.indexSet]; + } + return [unionIndexSet as_smallDescription]; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)debugDescription +{ + return ASObjectDescriptionMake(self, [self propertiesForDebugDescription]); +} + +- (NSString *)smallDescription +{ + return [self.indexSet as_smallDescription]; +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"indexes" : [self.indexSet as_smallDescription] }]; + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"anim" : @(_animationOptions) }]; + [result addObject:@{ @"type" : NSStringFromASHierarchyChangeType(_changeType) }]; + [result addObject:@{ @"indexes" : self.indexSet }]; + return result; +} + +@end + +@implementation _ASHierarchyItemChange + +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted +{ + self = [super init]; + if (self) { + ASDisplayNodeAssert(indexPaths.count > 0, @"Request to create _ASHierarchyItemChange with no items!"); + _changeType = changeType; + if (presorted) { + _indexPaths = indexPaths; + } else { + SEL sorting = changeType == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:); + _indexPaths = [indexPaths sortedArrayUsingSelector:sorting]; + } + _animationOptions = animationOptions; + } + return self; +} + +// Create a mapping out of changes indexPaths to a {@section : [indexSet]} fashion +// e.g. changes: (0 - 0), (0 - 1), (2 - 5) +// will become: {@0 : [0, 1], @2 : [5]} ++ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes +{ + NSMutableDictionary *sectionToIndexSetMap = [[NSMutableDictionary alloc] init]; + for (_ASHierarchyItemChange *change in changes) { + for (NSIndexPath *indexPath in change.indexPaths) { + NSNumber *sectionKey = @(indexPath.section); + NSMutableIndexSet *indexSet = sectionToIndexSetMap[sectionKey]; + if (indexSet) { + [indexSet addIndex:indexPath.item]; + } else { + indexSet = [NSMutableIndexSet indexSetWithIndex:indexPath.item]; + sectionToIndexSetMap[sectionKey] = indexSet; + } + } + } + return sectionToIndexSetMap; +} + ++ (void)ensureItemChanges:(NSArray<_ASHierarchyItemChange *> *)changes ofSameType:(_ASHierarchyChangeType)changeType +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + for (_ASHierarchyItemChange *change in changes) { + NSAssert(change.changeType == changeType, @"The map we created must all be of the same changeType as of now"); + } +#endif +} + +- (_ASHierarchyItemChange *)changeByFinalizingType +{ + _ASHierarchyChangeType newType; + switch (_changeType) { + case _ASHierarchyChangeTypeOriginalInsert: + newType = _ASHierarchyChangeTypeInsert; + break; + case _ASHierarchyChangeTypeOriginalDelete: + newType = _ASHierarchyChangeTypeDelete; + break; + default: + ASFailUpdateValidation(@"Attempt to finalize item change of invalid type %@.", NSStringFromASHierarchyChangeType(_changeType)); + return self; + } + return [[_ASHierarchyItemChange alloc] initWithChangeType:newType indexPaths:_indexPaths animationOptions:_animationOptions presorted:YES]; +} + ++ (void)sortAndCoalesceItemChanges:(NSMutableArray<_ASHierarchyItemChange *> *)changes ignoringChangesInSections:(NSIndexSet *)ignoredSections +{ + if (changes.count < 1) { + return; + } + + _ASHierarchyChangeType type = [changes.firstObject changeType]; + ASDisplayNodeAssert(ASHierarchyChangeTypeIsFinal(type), @"Attempt to sort and coalesce item changes of intermediary type %@. Why?", NSStringFromASHierarchyChangeType(type)); + + // Lookup table [NSIndexPath: AnimationOptions] + const auto animationOptions = [[NSMutableDictionary alloc] init]; + + // All changed index paths, sorted + const auto allIndexPaths = [[NSMutableArray alloc] init]; + + for (_ASHierarchyItemChange *change in changes) { + for (NSIndexPath *indexPath in change.indexPaths) { + if (![ignoredSections containsIndex:indexPath.section]) { + animationOptions[indexPath] = @(change.animationOptions); + [allIndexPaths addObject:indexPath]; + } + } + } + + SEL sorting = type == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:); + [allIndexPaths sortUsingSelector:sorting]; + + // Create new changes by grouping sorted changes by animation option + const auto result = [[NSMutableArray<_ASHierarchyItemChange *> alloc] init]; + + ASDataControllerAnimationOptions currentOptions = 0; + const auto currentIndexPaths = [[NSMutableArray alloc] init]; + + for (NSIndexPath *indexPath in allIndexPaths) { + ASDataControllerAnimationOptions options = [animationOptions[indexPath] integerValue]; + + // End the previous group if needed. + if (options != currentOptions && currentIndexPaths.count > 0) { + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:[currentIndexPaths copy] animationOptions:currentOptions presorted:YES]; + [result addObject:change]; + [currentIndexPaths removeAllObjects]; + } + + // Start a new group if needed. + if (currentIndexPaths.count == 0) { + currentOptions = options; + } + + [currentIndexPaths addObject:indexPath]; + } + + // Finish up the last group. + if (currentIndexPaths.count > 0) { + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:[currentIndexPaths copy] animationOptions:currentOptions presorted:YES]; + [result addObject:change]; + } + + [changes setArray:result]; +} + +#pragma mark - Debugging (Private) + ++ (NSString *)smallDescriptionForItemChanges:(NSArray<_ASHierarchyItemChange *> *)changes +{ + NSDictionary *map = [self sectionToIndexSetMapFromChanges:changes]; + NSMutableString *str = [NSMutableString stringWithString:@"{ "]; + [map enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull section, NSIndexSet * _Nonnull indexSet, BOOL * _Nonnull stop) { + [str appendFormat:@"@%lu : %@ ", (long)section.integerValue, [indexSet as_smallDescription]]; + }]; + [str appendString:@"}"]; + return str; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)debugDescription +{ + return ASObjectDescriptionMake(self, [self propertiesForDebugDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"indexPaths" : self.indexPaths }]; + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"anim" : @(_animationOptions) }]; + [result addObject:@{ @"type" : NSStringFromASHierarchyChangeType(_changeType) }]; + [result addObject:@{ @"indexPaths" : self.indexPaths }]; + return result; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASPendingState.h b/submodules/AsyncDisplayKit/Source/Private/_ASPendingState.h new file mode 100644 index 0000000000..0a96e7a8ad --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASPendingState.h @@ -0,0 +1,41 @@ +// +// _ASPendingState.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +/** + + Private header for ASDisplayNode.mm + + _ASPendingState is a proxy for a UIView that has yet to be created. + In response to its setters, it sets an internal property and a flag that indicates that that property has been set. + + When you want to configure a view from this pending state information, just call -applyToView: + */ + +@interface _ASPendingState : NSObject + +// Supports all of the properties included in the ASDisplayNodeViewProperties protocol + +- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)setFrameDirectly; +- (void)applyToLayer:(CALayer *)layer; + ++ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer; ++ (_ASPendingState *)pendingViewStateFromView:(UIView *)view; + +@property (nonatomic, readonly) BOOL hasSetNeedsLayout; +@property (nonatomic, readonly) BOOL hasSetNeedsDisplay; + +@property (nonatomic, readonly) BOOL hasChanges; + +- (void)clearChanges; + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASPendingState.mm b/submodules/AsyncDisplayKit/Source/Private/_ASPendingState.mm new file mode 100644 index 0000000000..9e9778cf04 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASPendingState.mm @@ -0,0 +1,1379 @@ +// +// _ASPendingState.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import +#import + +#define __shouldSetNeedsDisplay(layer) (flags.needsDisplay \ + || (flags.setOpaque && opaque != (layer).opaque)\ + || (flags.setBackgroundColor && !CGColorEqualToColor(backgroundColor, (layer).backgroundColor))) + +typedef struct { + // Properties + int needsDisplay:1; + int needsLayout:1; + int layoutIfNeeded:1; + + // Flags indicating that a given property should be applied to the view at creation + int setClipsToBounds:1; + int setOpaque:1; + int setNeedsDisplayOnBoundsChange:1; + int setAutoresizesSubviews:1; + int setAutoresizingMask:1; + int setFrame:1; + int setBounds:1; + int setBackgroundColor:1; + int setTintColor:1; + int setHidden:1; + int setAlpha:1; + int setCornerRadius:1; + int setContentMode:1; + int setNeedsDisplay:1; + int setAnchorPoint:1; + int setPosition:1; + int setZPosition:1; + int setTransform:1; + int setSublayerTransform:1; + int setContents:1; + int setContentsGravity:1; + int setContentsRect:1; + int setContentsCenter:1; + int setContentsScale:1; + int setRasterizationScale:1; + int setUserInteractionEnabled:1; + int setExclusiveTouch:1; + int setShadowColor:1; + int setShadowOpacity:1; + int setShadowOffset:1; + int setShadowRadius:1; + int setBorderWidth:1; + int setBorderColor:1; + int setAsyncTransactionContainer:1; + int setAllowsGroupOpacity:1; + int setAllowsEdgeAntialiasing:1; + int setEdgeAntialiasingMask:1; + int setIsAccessibilityElement:1; + int setAccessibilityLabel:1; + int setAccessibilityAttributedLabel:1; + int setAccessibilityHint:1; + int setAccessibilityAttributedHint:1; + int setAccessibilityValue:1; + int setAccessibilityAttributedValue:1; + int setAccessibilityTraits:1; + int setAccessibilityFrame:1; + int setAccessibilityLanguage:1; + int setAccessibilityElementsHidden:1; + int setAccessibilityViewIsModal:1; + int setShouldGroupAccessibilityChildren:1; + int setAccessibilityIdentifier:1; + int setAccessibilityNavigationStyle:1; + int setAccessibilityHeaderElements:1; + int setAccessibilityActivationPoint:1; + int setAccessibilityPath:1; + int setSemanticContentAttribute:1; + int setLayoutMargins:1; + int setPreservesSuperviewLayoutMargins:1; + int setInsetsLayoutMarginsFromSafeArea:1; + int setAccessibilityCustomActions:1; +} ASPendingStateFlags; + +@implementation _ASPendingState +{ + @package //Expose all ivars for ASDisplayNode to bypass getters for efficiency + + UIViewAutoresizing autoresizingMask; + unsigned int edgeAntialiasingMask; + CGRect frame; // Frame is only to be used for synchronous views wrapped by nodes (see setFrame:) + CGRect bounds; + CGColorRef backgroundColor; + CGFloat alpha; + CGFloat cornerRadius; + UIViewContentMode contentMode; + CGPoint anchorPoint; + CGPoint position; + CGFloat zPosition; + CATransform3D transform; + CATransform3D sublayerTransform; + id contents; + NSString *contentsGravity; + CGRect contentsRect; + CGRect contentsCenter; + CGFloat contentsScale; + CGFloat rasterizationScale; + CGColorRef shadowColor; + CGFloat shadowOpacity; + CGSize shadowOffset; + CGFloat shadowRadius; + CGFloat borderWidth; + CGColorRef borderColor; + BOOL asyncTransactionContainer; + UIEdgeInsets layoutMargins; + BOOL preservesSuperviewLayoutMargins; + BOOL insetsLayoutMarginsFromSafeArea; + BOOL isAccessibilityElement; + NSString *accessibilityLabel; + NSAttributedString *accessibilityAttributedLabel; + NSString *accessibilityHint; + NSAttributedString *accessibilityAttributedHint; + NSString *accessibilityValue; + NSAttributedString *accessibilityAttributedValue; + UIAccessibilityTraits accessibilityTraits; + CGRect accessibilityFrame; + NSString *accessibilityLanguage; + BOOL accessibilityElementsHidden; + BOOL accessibilityViewIsModal; + BOOL shouldGroupAccessibilityChildren; + NSString *accessibilityIdentifier; + UIAccessibilityNavigationStyle accessibilityNavigationStyle; + NSArray *accessibilityHeaderElements; + CGPoint accessibilityActivationPoint; + UIBezierPath *accessibilityPath; + UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); + NSArray * accessibilityCustomActions; + + ASPendingStateFlags _flags; +} + +/** + * Apply the state's frame, bounds, and position to layer. This will not + * be called on synchronous view-backed nodes which require we directly + * call [view setFrame:]. + * + * FIXME: How should we reconcile order-of-operations between setting frame, bounds, position? + * Note we can't read bounds and position in the background, so we have to keep the frame + * value intact until application time (now). + */ +ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *state, CALayer *layer) { + ASPendingStateFlags flags = state->_flags; + if (flags.setFrame) { + CGRect _bounds = CGRectZero; + CGPoint _position = CGPointZero; + ASBoundsAndPositionForFrame(state->frame, layer.bounds.origin, layer.anchorPoint, &_bounds, &_position); + layer.bounds = _bounds; + layer.position = _position; + } else { + if (flags.setBounds) + layer.bounds = state->bounds; + if (flags.setPosition) + layer.position = state->position; + } +} + +@synthesize clipsToBounds=clipsToBounds; +@synthesize opaque=opaque; +@synthesize frame=frame; +@synthesize bounds=bounds; +@synthesize backgroundColor=backgroundColor; +@synthesize hidden=isHidden; +@synthesize needsDisplayOnBoundsChange=needsDisplayOnBoundsChange; +@synthesize allowsGroupOpacity=allowsGroupOpacity; +@synthesize allowsEdgeAntialiasing=allowsEdgeAntialiasing; +@synthesize edgeAntialiasingMask=edgeAntialiasingMask; +@synthesize autoresizesSubviews=autoresizesSubviews; +@synthesize autoresizingMask=autoresizingMask; +@synthesize tintColor=tintColor; +@synthesize alpha=alpha; +@synthesize cornerRadius=cornerRadius; +@synthesize contentMode=contentMode; +@synthesize anchorPoint=anchorPoint; +@synthesize position=position; +@synthesize zPosition=zPosition; +@synthesize transform=transform; +@synthesize sublayerTransform=sublayerTransform; +@synthesize contents=contents; +@synthesize contentsGravity=contentsGravity; +@synthesize contentsRect=contentsRect; +@synthesize contentsCenter=contentsCenter; +@synthesize contentsScale=contentsScale; +@synthesize rasterizationScale=rasterizationScale; +@synthesize userInteractionEnabled=userInteractionEnabled; +@synthesize exclusiveTouch=exclusiveTouch; +@synthesize shadowColor=shadowColor; +@synthesize shadowOpacity=shadowOpacity; +@synthesize shadowOffset=shadowOffset; +@synthesize shadowRadius=shadowRadius; +@synthesize borderWidth=borderWidth; +@synthesize borderColor=borderColor; +@synthesize asyncdisplaykit_asyncTransactionContainer=asyncTransactionContainer; +@synthesize semanticContentAttribute=semanticContentAttribute; +@synthesize layoutMargins=layoutMargins; +@synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; +@synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; + +static CGColorRef blackColorRef = NULL; +static UIColor *defaultTintColor = nil; + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Default UIKit color is an RGB color + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + blackColorRef = CGColorCreate(colorSpace, (CGFloat[]){0,0,0,1} ); + CFRetain(blackColorRef); + CGColorSpaceRelease(colorSpace); + defaultTintColor = [UIColor colorWithRed:0.0 green:0.478 blue:1.0 alpha:1.0]; + }); + + // Set defaults, these come from the defaults specified in CALayer and UIView + clipsToBounds = NO; + opaque = YES; + frame = CGRectZero; + bounds = CGRectZero; + backgroundColor = nil; + tintColor = defaultTintColor; + isHidden = NO; + needsDisplayOnBoundsChange = NO; + allowsGroupOpacity = ASDefaultAllowsGroupOpacity(); + allowsEdgeAntialiasing = ASDefaultAllowsEdgeAntialiasing(); + autoresizesSubviews = YES; + alpha = 1.0f; + cornerRadius = 0.0f; + contentMode = UIViewContentModeScaleToFill; + _flags.needsDisplay = NO; + anchorPoint = CGPointMake(0.5, 0.5); + position = CGPointZero; + zPosition = 0.0; + transform = CATransform3DIdentity; + sublayerTransform = CATransform3DIdentity; + contents = nil; + contentsGravity = kCAGravityResize; + contentsRect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); + contentsCenter = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); + contentsScale = 1.0f; + rasterizationScale = 1.0f; + userInteractionEnabled = YES; + shadowColor = blackColorRef; + shadowOpacity = 0.0; + shadowOffset = CGSizeMake(0, -3); + shadowRadius = 3; + borderWidth = 0; + borderColor = blackColorRef; + layoutMargins = UIEdgeInsetsMake(8, 8, 8, 8); + preservesSuperviewLayoutMargins = NO; + insetsLayoutMarginsFromSafeArea = YES; + isAccessibilityElement = NO; + accessibilityLabel = nil; + accessibilityAttributedLabel = nil; + accessibilityHint = nil; + accessibilityAttributedHint = nil; + accessibilityValue = nil; + accessibilityAttributedValue = nil; + accessibilityTraits = UIAccessibilityTraitNone; + accessibilityFrame = CGRectZero; + accessibilityLanguage = nil; + accessibilityElementsHidden = NO; + accessibilityViewIsModal = NO; + shouldGroupAccessibilityChildren = NO; + accessibilityIdentifier = nil; + accessibilityNavigationStyle = UIAccessibilityNavigationStyleAutomatic; + accessibilityHeaderElements = nil; + accessibilityActivationPoint = CGPointZero; + accessibilityPath = nil; + edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerRightEdge | kCALayerTopEdge | kCALayerBottomEdge); + semanticContentAttribute = UISemanticContentAttributeUnspecified; + + return self; +} + +- (void)setNeedsDisplay +{ + _flags.needsDisplay = YES; +} + +- (void)setNeedsLayout +{ + _flags.needsLayout = YES; +} + +- (void)layoutIfNeeded +{ + _flags.layoutIfNeeded = YES; +} + +- (void)setClipsToBounds:(BOOL)flag +{ + clipsToBounds = flag; + _flags.setClipsToBounds = YES; +} + +- (void)setOpaque:(BOOL)flag +{ + opaque = flag; + _flags.setOpaque = YES; +} + +- (void)setNeedsDisplayOnBoundsChange:(BOOL)flag +{ + needsDisplayOnBoundsChange = flag; + _flags.setNeedsDisplayOnBoundsChange = YES; +} + +- (void)setAllowsGroupOpacity:(BOOL)flag +{ + allowsGroupOpacity = flag; + _flags.setAllowsGroupOpacity = YES; +} + +- (void)setAllowsEdgeAntialiasing:(BOOL)flag +{ + allowsEdgeAntialiasing = flag; + _flags.setAllowsEdgeAntialiasing = YES; +} + +- (void)setEdgeAntialiasingMask:(unsigned int)mask +{ + edgeAntialiasingMask = mask; + _flags.setEdgeAntialiasingMask = YES; +} + +- (void)setAutoresizesSubviews:(BOOL)flag +{ + autoresizesSubviews = flag; + _flags.setAutoresizesSubviews = YES; +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)mask +{ + autoresizingMask = mask; + _flags.setAutoresizingMask = YES; +} + +- (void)setFrame:(CGRect)newFrame +{ + frame = newFrame; + _flags.setFrame = YES; +} + +- (void)setBounds:(CGRect)newBounds +{ + ASDisplayNodeAssert(!isnan(newBounds.size.width) && !isnan(newBounds.size.height), @"Invalid bounds %@ provided to %@", NSStringFromCGRect(newBounds), self); + if (isnan(newBounds.size.width)) + newBounds.size.width = 0.0; + if (isnan(newBounds.size.height)) + newBounds.size.height = 0.0; + bounds = newBounds; + _flags.setBounds = YES; +} + +- (CGColorRef)backgroundColor +{ + return backgroundColor; +} + +- (void)setBackgroundColor:(CGColorRef)color +{ + if (color == backgroundColor) { + return; + } + + CGColorRelease(backgroundColor); + backgroundColor = CGColorRetain(color); + _flags.setBackgroundColor = YES; +} + +- (void)setTintColor:(UIColor *)newTintColor +{ + tintColor = newTintColor; + _flags.setTintColor = YES; +} + +- (void)setHidden:(BOOL)flag +{ + isHidden = flag; + _flags.setHidden = YES; +} + +- (void)setAlpha:(CGFloat)newAlpha +{ + alpha = newAlpha; + _flags.setAlpha = YES; +} + +- (void)setCornerRadius:(CGFloat)newCornerRadius +{ + cornerRadius = newCornerRadius; + _flags.setCornerRadius = YES; +} + +- (void)setContentMode:(UIViewContentMode)newContentMode +{ + contentMode = newContentMode; + _flags.setContentMode = YES; +} + +- (void)setAnchorPoint:(CGPoint)newAnchorPoint +{ + anchorPoint = newAnchorPoint; + _flags.setAnchorPoint = YES; +} + +- (void)setPosition:(CGPoint)newPosition +{ + ASDisplayNodeAssert(!isnan(newPosition.x) && !isnan(newPosition.y), @"Invalid position %@ provided to %@", NSStringFromCGPoint(newPosition), self); + if (isnan(newPosition.x)) + newPosition.x = 0.0; + if (isnan(newPosition.y)) + newPosition.y = 0.0; + position = newPosition; + _flags.setPosition = YES; +} + +- (void)setZPosition:(CGFloat)newPosition +{ + zPosition = newPosition; + _flags.setZPosition = YES; +} + +- (void)setTransform:(CATransform3D)newTransform +{ + transform = newTransform; + _flags.setTransform = YES; +} + +- (void)setSublayerTransform:(CATransform3D)newSublayerTransform +{ + sublayerTransform = newSublayerTransform; + _flags.setSublayerTransform = YES; +} + +- (void)setContents:(id)newContents +{ + if (contents == newContents) { + return; + } + + contents = newContents; + _flags.setContents = YES; +} + +- (void)setContentsGravity:(NSString *)newContentsGravity +{ + contentsGravity = newContentsGravity; + _flags.setContentsGravity = YES; +} + +- (void)setContentsRect:(CGRect)newContentsRect +{ + contentsRect = newContentsRect; + _flags.setContentsRect = YES; +} + +- (void)setContentsCenter:(CGRect)newContentsCenter +{ + contentsCenter = newContentsCenter; + _flags.setContentsCenter = YES; +} + +- (void)setContentsScale:(CGFloat)newContentsScale +{ + contentsScale = newContentsScale; + _flags.setContentsScale = YES; +} + +- (void)setRasterizationScale:(CGFloat)newRasterizationScale +{ + rasterizationScale = newRasterizationScale; + _flags.setRasterizationScale = YES; +} + +- (void)setUserInteractionEnabled:(BOOL)flag +{ + userInteractionEnabled = flag; + _flags.setUserInteractionEnabled = YES; +} + +- (void)setExclusiveTouch:(BOOL)flag +{ + exclusiveTouch = flag; + _flags.setExclusiveTouch = YES; +} + +- (void)setShadowColor:(CGColorRef)color +{ + if (shadowColor == color) { + return; + } + + if (shadowColor != blackColorRef) { + CGColorRelease(shadowColor); + } + shadowColor = color; + CGColorRetain(shadowColor); + + _flags.setShadowColor = YES; +} + +- (void)setShadowOpacity:(CGFloat)newOpacity +{ + shadowOpacity = newOpacity; + _flags.setShadowOpacity = YES; +} + +- (void)setShadowOffset:(CGSize)newOffset +{ + shadowOffset = newOffset; + _flags.setShadowOffset = YES; +} + +- (void)setShadowRadius:(CGFloat)newRadius +{ + shadowRadius = newRadius; + _flags.setShadowRadius = YES; +} + +- (void)setBorderWidth:(CGFloat)newWidth +{ + borderWidth = newWidth; + _flags.setBorderWidth = YES; +} + +- (void)setBorderColor:(CGColorRef)color +{ + if (borderColor == color) { + return; + } + + if (borderColor != blackColorRef) { + CGColorRelease(borderColor); + } + borderColor = color; + CGColorRetain(borderColor); + + _flags.setBorderColor = YES; +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)flag +{ + asyncTransactionContainer = flag; + _flags.setAsyncTransactionContainer = YES; +} + +- (void)setLayoutMargins:(UIEdgeInsets)margins +{ + layoutMargins = margins; + _flags.setLayoutMargins = YES; +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)flag +{ + preservesSuperviewLayoutMargins = flag; + _flags.setPreservesSuperviewLayoutMargins = YES; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)flag +{ + insetsLayoutMarginsFromSafeArea = flag; + _flags.setInsetsLayoutMarginsFromSafeArea = YES; +} + +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AVAILABLE(ios(9.0), tvos(9.0)) { + semanticContentAttribute = attribute; + _flags.setSemanticContentAttribute = YES; +} + +- (void)setAccessibilityCustomActions:(NSArray *)accessibilityCustomActions { + self->accessibilityCustomActions = accessibilityCustomActions; + _flags.setAccessibilityCustomActions = YES; +} + +- (BOOL)isAccessibilityElement +{ + return isAccessibilityElement; +} + +- (void)setIsAccessibilityElement:(BOOL)newIsAccessibilityElement +{ + isAccessibilityElement = newIsAccessibilityElement; + _flags.setIsAccessibilityElement = YES; +} + +- (NSString *)accessibilityLabel +{ + if (_flags.setAccessibilityAttributedLabel) { + return accessibilityAttributedLabel.string; + } + return accessibilityLabel; +} + +- (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel +{ + ASCompareAssignCopy(accessibilityLabel, newAccessibilityLabel); + _flags.setAccessibilityLabel = YES; + _flags.setAccessibilityAttributedLabel = NO; +} + +- (NSAttributedString *)accessibilityAttributedLabel +{ + if (_flags.setAccessibilityLabel) { + return [[NSAttributedString alloc] initWithString:accessibilityLabel]; + } + return accessibilityAttributedLabel; +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)newAccessibilityAttributedLabel +{ + ASCompareAssignCopy(accessibilityAttributedLabel, newAccessibilityAttributedLabel); + _flags.setAccessibilityAttributedLabel = YES; + _flags.setAccessibilityLabel = NO; +} + +- (NSString *)accessibilityHint +{ + if (_flags.setAccessibilityAttributedHint) { + return accessibilityAttributedHint.string; + } + return accessibilityHint; +} + +- (void)setAccessibilityHint:(NSString *)newAccessibilityHint +{ + ASCompareAssignCopy(accessibilityHint, newAccessibilityHint); + _flags.setAccessibilityHint = YES; + _flags.setAccessibilityAttributedHint = NO; +} + +- (NSAttributedString *)accessibilityAttributedHint +{ + if (_flags.setAccessibilityHint) { + return [[NSAttributedString alloc] initWithString:accessibilityHint]; + } + return accessibilityAttributedHint; +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)newAccessibilityAttributedHint +{ + ASCompareAssignCopy(accessibilityAttributedHint, newAccessibilityAttributedHint); + _flags.setAccessibilityAttributedHint = YES; + _flags.setAccessibilityHint = NO; +} + +- (NSString *)accessibilityValue +{ + if (_flags.setAccessibilityAttributedValue) { + return accessibilityAttributedValue.string; + } + return accessibilityValue; +} + +- (void)setAccessibilityValue:(NSString *)newAccessibilityValue +{ + ASCompareAssignCopy(accessibilityValue, newAccessibilityValue); + _flags.setAccessibilityValue = YES; + _flags.setAccessibilityAttributedValue = NO; +} + +- (NSAttributedString *)accessibilityAttributedValue +{ + if (_flags.setAccessibilityValue) { + return [[NSAttributedString alloc] initWithString:accessibilityValue]; + } + return accessibilityAttributedValue; +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)newAccessibilityAttributedValue +{ + ASCompareAssignCopy(accessibilityAttributedValue, newAccessibilityAttributedValue); + _flags.setAccessibilityAttributedValue = YES; + _flags.setAccessibilityValue = NO; +} + +- (UIAccessibilityTraits)accessibilityTraits +{ + return accessibilityTraits; +} + +- (void)setAccessibilityTraits:(UIAccessibilityTraits)newAccessibilityTraits +{ + accessibilityTraits = newAccessibilityTraits; + _flags.setAccessibilityTraits = YES; +} + +- (CGRect)accessibilityFrame +{ + return accessibilityFrame; +} + +- (void)setAccessibilityFrame:(CGRect)newAccessibilityFrame +{ + accessibilityFrame = newAccessibilityFrame; + _flags.setAccessibilityFrame = YES; +} + +- (NSString *)accessibilityLanguage +{ + return accessibilityLanguage; +} + +- (void)setAccessibilityLanguage:(NSString *)newAccessibilityLanguage +{ + _flags.setAccessibilityLanguage = YES; + accessibilityLanguage = newAccessibilityLanguage; +} + +- (BOOL)accessibilityElementsHidden +{ + return accessibilityElementsHidden; +} + +- (void)setAccessibilityElementsHidden:(BOOL)newAccessibilityElementsHidden +{ + accessibilityElementsHidden = newAccessibilityElementsHidden; + _flags.setAccessibilityElementsHidden = YES; +} + +- (BOOL)accessibilityViewIsModal +{ + return accessibilityViewIsModal; +} + +- (void)setAccessibilityViewIsModal:(BOOL)newAccessibilityViewIsModal +{ + accessibilityViewIsModal = newAccessibilityViewIsModal; + _flags.setAccessibilityViewIsModal = YES; +} + +- (BOOL)shouldGroupAccessibilityChildren +{ + return shouldGroupAccessibilityChildren; +} + +- (void)setShouldGroupAccessibilityChildren:(BOOL)newShouldGroupAccessibilityChildren +{ + shouldGroupAccessibilityChildren = newShouldGroupAccessibilityChildren; + _flags.setShouldGroupAccessibilityChildren = YES; +} + +- (NSString *)accessibilityIdentifier +{ + return accessibilityIdentifier; +} + +- (void)setAccessibilityIdentifier:(NSString *)newAccessibilityIdentifier +{ + _flags.setAccessibilityIdentifier = YES; + if (accessibilityIdentifier != newAccessibilityIdentifier) { + accessibilityIdentifier = [newAccessibilityIdentifier copy]; + } +} + +- (UIAccessibilityNavigationStyle)accessibilityNavigationStyle +{ + return accessibilityNavigationStyle; +} + +- (void)setAccessibilityNavigationStyle:(UIAccessibilityNavigationStyle)newAccessibilityNavigationStyle +{ + _flags.setAccessibilityNavigationStyle = YES; + accessibilityNavigationStyle = newAccessibilityNavigationStyle; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (NSArray *)accessibilityHeaderElements +{ + return accessibilityHeaderElements; +} + +- (void)setAccessibilityHeaderElements:(NSArray *)newAccessibilityHeaderElements +{ + _flags.setAccessibilityHeaderElements = YES; + if (accessibilityHeaderElements != newAccessibilityHeaderElements) { + accessibilityHeaderElements = [newAccessibilityHeaderElements copy]; + } +} +#pragma clang diagnostic pop + +- (CGPoint)accessibilityActivationPoint +{ + if (_flags.setAccessibilityActivationPoint) { + return accessibilityActivationPoint; + } + + // Default == Mid-point of the accessibilityFrame + return CGPointMake(CGRectGetMidX(accessibilityFrame), CGRectGetMidY(accessibilityFrame)); +} + +- (void)setAccessibilityActivationPoint:(CGPoint)newAccessibilityActivationPoint +{ + _flags.setAccessibilityActivationPoint = YES; + accessibilityActivationPoint = newAccessibilityActivationPoint; +} + +- (UIBezierPath *)accessibilityPath +{ + return accessibilityPath; +} + +- (void)setAccessibilityPath:(UIBezierPath *)newAccessibilityPath +{ + _flags.setAccessibilityPath = YES; + if (accessibilityPath != newAccessibilityPath) { + accessibilityPath = newAccessibilityPath; + } +} + +- (void)applyToLayer:(CALayer *)layer +{ + ASPendingStateFlags flags = _flags; + + if (__shouldSetNeedsDisplay(layer)) { + [layer setNeedsDisplay]; + } + + if (flags.setAnchorPoint) + layer.anchorPoint = anchorPoint; + + if (flags.setZPosition) + layer.zPosition = zPosition; + + if (flags.setTransform) + layer.transform = transform; + + if (flags.setSublayerTransform) + layer.sublayerTransform = sublayerTransform; + + if (flags.setClipsToBounds) + layer.masksToBounds = clipsToBounds; + + if (flags.setBackgroundColor) + layer.backgroundColor = backgroundColor; + + if (flags.setOpaque) + layer.opaque = opaque; + + if (flags.setHidden) + layer.hidden = isHidden; + + if (flags.setAlpha) + layer.opacity = alpha; + + if (flags.setCornerRadius) + layer.cornerRadius = cornerRadius; + + if (flags.setContentMode) + layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode); + + if (flags.setShadowColor) + layer.shadowColor = shadowColor; + + if (flags.setShadowOpacity) + layer.shadowOpacity = shadowOpacity; + + if (flags.setShadowOffset) + layer.shadowOffset = shadowOffset; + + if (flags.setShadowRadius) + layer.shadowRadius = shadowRadius; + + if (flags.setBorderWidth) + layer.borderWidth = borderWidth; + + if (flags.setBorderColor) + layer.borderColor = borderColor; + + if (flags.setNeedsDisplayOnBoundsChange) + layer.needsDisplayOnBoundsChange = needsDisplayOnBoundsChange; + + if (flags.setAllowsGroupOpacity) + layer.allowsGroupOpacity = allowsGroupOpacity; + + if (flags.setAllowsEdgeAntialiasing) + layer.allowsEdgeAntialiasing = allowsEdgeAntialiasing; + + if (flags.setEdgeAntialiasingMask) + layer.edgeAntialiasingMask = edgeAntialiasingMask; + + if (flags.setAsyncTransactionContainer) + layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; + + if (flags.setOpaque) + ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + + ASPendingStateApplyMetricsToLayer(self, layer); + + if (flags.setContents) + layer.contents = contents; + + if (flags.setContentsScale) + layer.contentsScale = contentsScale; + + if (flags.setRasterizationScale) + layer.rasterizationScale = rasterizationScale; + + if (flags.setContentsGravity) + layer.contentsGravity = contentsGravity; + + if (flags.setContentsRect) + layer.contentsRect = contentsRect; + + if (flags.setContentsCenter) + layer.contentsCenter = contentsCenter; + + if (flags.needsLayout) + [layer setNeedsLayout]; + + if (flags.layoutIfNeeded) + [layer layoutIfNeeded]; +} + +- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling +{ + /* + Use our convenience setters blah here instead of layer.blah + We were accidentally setting some properties on layer here, but view in UIViewBridgeOptimizations. + + That could easily cause bugs where it mattered whether you set something up on a bg thread on in -didLoad + because a different setter would be called. + */ + + CALayer *layer = view.layer; + + ASPendingStateFlags flags = _flags; + if (__shouldSetNeedsDisplay(layer)) { + [view setNeedsDisplay]; + } + + if (flags.setAnchorPoint) + layer.anchorPoint = anchorPoint; + + if (flags.setPosition) + layer.position = position; + + if (flags.setZPosition) + layer.zPosition = zPosition; + + if (flags.setBounds) + view.bounds = bounds; + + if (flags.setTransform) + layer.transform = transform; + + if (flags.setSublayerTransform) + layer.sublayerTransform = sublayerTransform; + + if (flags.setClipsToBounds) + view.clipsToBounds = clipsToBounds; + + 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; + + if (flags.setOpaque) + layer.opaque = opaque; + + if (flags.setHidden) + view.hidden = isHidden; + + if (flags.setAlpha) + view.alpha = alpha; + + if (flags.setCornerRadius) + layer.cornerRadius = cornerRadius; + + if (flags.setContentMode) + view.contentMode = contentMode; + + if (flags.setUserInteractionEnabled) + view.userInteractionEnabled = userInteractionEnabled; + + #if TARGET_OS_IOS + if (flags.setExclusiveTouch) + view.exclusiveTouch = exclusiveTouch; + #endif + + if (flags.setShadowColor) + layer.shadowColor = shadowColor; + + if (flags.setShadowOpacity) + layer.shadowOpacity = shadowOpacity; + + if (flags.setShadowOffset) + layer.shadowOffset = shadowOffset; + + if (flags.setShadowRadius) + layer.shadowRadius = shadowRadius; + + if (flags.setBorderWidth) + layer.borderWidth = borderWidth; + + if (flags.setBorderColor) + layer.borderColor = borderColor; + + if (flags.setAutoresizingMask) + view.autoresizingMask = autoresizingMask; + + if (flags.setAutoresizesSubviews) + view.autoresizesSubviews = autoresizesSubviews; + + if (flags.setNeedsDisplayOnBoundsChange) + layer.needsDisplayOnBoundsChange = needsDisplayOnBoundsChange; + + if (flags.setAllowsGroupOpacity) + layer.allowsGroupOpacity = allowsGroupOpacity; + + if (flags.setAllowsEdgeAntialiasing) + layer.allowsEdgeAntialiasing = allowsEdgeAntialiasing; + + if (flags.setEdgeAntialiasingMask) + layer.edgeAntialiasingMask = edgeAntialiasingMask; + + if (flags.setAsyncTransactionContainer) + view.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; + + if (flags.setOpaque) + ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + + if (flags.setLayoutMargins) + view.layoutMargins = layoutMargins; + + if (flags.setPreservesSuperviewLayoutMargins) + view.preservesSuperviewLayoutMargins = preservesSuperviewLayoutMargins; + + if (AS_AVAILABLE_IOS(11.0)) { + if (flags.setInsetsLayoutMarginsFromSafeArea) { + view.insetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + } + } + + if (flags.setAccessibilityCustomActions) { + view.accessibilityCustomActions = accessibilityCustomActions; + } + + if (flags.setSemanticContentAttribute) { + view.semanticContentAttribute = semanticContentAttribute; + } + + if (flags.setIsAccessibilityElement) + view.isAccessibilityElement = isAccessibilityElement; + + if (flags.setAccessibilityLabel) + view.accessibilityLabel = accessibilityLabel; + + if (flags.setAccessibilityHint) + view.accessibilityHint = accessibilityHint; + + if (flags.setAccessibilityValue) + view.accessibilityValue = accessibilityValue; + + if (AS_AVAILABLE_IOS(11)) { + if (flags.setAccessibilityAttributedLabel) { + view.accessibilityAttributedLabel = accessibilityAttributedLabel; + } + if (flags.setAccessibilityAttributedHint) { + view.accessibilityAttributedHint = accessibilityAttributedHint; + } + if (flags.setAccessibilityAttributedValue) { + view.accessibilityAttributedValue = accessibilityAttributedValue; + } + } + + if (flags.setAccessibilityTraits) + view.accessibilityTraits = accessibilityTraits; + + if (flags.setAccessibilityFrame) + view.accessibilityFrame = accessibilityFrame; + + if (flags.setAccessibilityLanguage) + view.accessibilityLanguage = accessibilityLanguage; + + if (flags.setAccessibilityElementsHidden) + view.accessibilityElementsHidden = accessibilityElementsHidden; + + if (flags.setAccessibilityViewIsModal) + view.accessibilityViewIsModal = accessibilityViewIsModal; + + if (flags.setShouldGroupAccessibilityChildren) + view.shouldGroupAccessibilityChildren = shouldGroupAccessibilityChildren; + + if (flags.setAccessibilityIdentifier) + view.accessibilityIdentifier = accessibilityIdentifier; + + if (flags.setAccessibilityNavigationStyle) + view.accessibilityNavigationStyle = accessibilityNavigationStyle; + +#if TARGET_OS_TV + if (flags.setAccessibilityHeaderElements) + view.accessibilityHeaderElements = accessibilityHeaderElements; +#endif + + if (flags.setAccessibilityActivationPoint) + view.accessibilityActivationPoint = accessibilityActivationPoint; + + if (flags.setAccessibilityPath) + view.accessibilityPath = accessibilityPath; + + 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. +// ASDisplayNodeAssert(CATransform3DIsIdentity(layer.transform), @"-[ASDisplayNode setFrame:] - self.transform must be identity in order to set the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); +//#endif + view.frame = frame; + } else { + ASPendingStateApplyMetricsToLayer(self, layer); + } + + if (flags.setContents) + layer.contents = contents; + + if (flags.setContentsGravity) + layer.contentsGravity = contentsGravity; + + if (flags.setContentsRect) + layer.contentsRect = contentsRect; + + if (flags.setContentsCenter) + layer.contentsCenter = contentsCenter; + + if (flags.setContentsScale) + layer.contentsScale = contentsScale; + + if (flags.setRasterizationScale) + layer.rasterizationScale = rasterizationScale; + + if (flags.needsLayout) + [view setNeedsLayout]; + + if (flags.layoutIfNeeded) + [view layoutIfNeeded]; +} + +// FIXME: Make this more efficient by tracking which properties are set rather than reading everything. ++ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer +{ + if (!layer) { + return nil; + } + _ASPendingState *pendingState = [[_ASPendingState alloc] init]; + pendingState.anchorPoint = layer.anchorPoint; + pendingState.position = layer.position; + pendingState.zPosition = layer.zPosition; + pendingState.bounds = layer.bounds; + pendingState.transform = layer.transform; + pendingState.sublayerTransform = layer.sublayerTransform; + pendingState.contents = layer.contents; + pendingState.contentsGravity = layer.contentsGravity; + pendingState.contentsRect = layer.contentsRect; + pendingState.contentsCenter = layer.contentsCenter; + pendingState.contentsScale = layer.contentsScale; + pendingState.rasterizationScale = layer.rasterizationScale; + pendingState.clipsToBounds = layer.masksToBounds; + pendingState.backgroundColor = layer.backgroundColor; + pendingState.opaque = layer.opaque; + pendingState.hidden = layer.hidden; + pendingState.alpha = layer.opacity; + pendingState.cornerRadius = layer.cornerRadius; + pendingState.contentMode = ASDisplayNodeUIContentModeFromCAContentsGravity(layer.contentsGravity); + pendingState.shadowColor = layer.shadowColor; + pendingState.shadowOpacity = layer.shadowOpacity; + pendingState.shadowOffset = layer.shadowOffset; + pendingState.shadowRadius = layer.shadowRadius; + pendingState.borderWidth = layer.borderWidth; + pendingState.borderColor = layer.borderColor; + pendingState.needsDisplayOnBoundsChange = layer.needsDisplayOnBoundsChange; + pendingState.allowsGroupOpacity = layer.allowsGroupOpacity; + pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; + pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; + return pendingState; +} + +// FIXME: Make this more efficient by tracking which properties are set rather than reading everything. ++ (_ASPendingState *)pendingViewStateFromView:(UIView *)view +{ + if (!view) { + return nil; + } + _ASPendingState *pendingState = [[_ASPendingState alloc] init]; + + CALayer *layer = view.layer; + pendingState.anchorPoint = layer.anchorPoint; + pendingState.position = layer.position; + pendingState.zPosition = layer.zPosition; + pendingState.bounds = view.bounds; + pendingState.transform = layer.transform; + pendingState.sublayerTransform = layer.sublayerTransform; + pendingState.contents = layer.contents; + pendingState.contentsGravity = layer.contentsGravity; + pendingState.contentsRect = layer.contentsRect; + pendingState.contentsCenter = layer.contentsCenter; + pendingState.contentsScale = layer.contentsScale; + pendingState.rasterizationScale = layer.rasterizationScale; + pendingState.clipsToBounds = view.clipsToBounds; + pendingState.backgroundColor = layer.backgroundColor; + pendingState.tintColor = view.tintColor; + pendingState.opaque = layer.opaque; + pendingState.hidden = view.hidden; + pendingState.alpha = view.alpha; + pendingState.cornerRadius = layer.cornerRadius; + pendingState.contentMode = view.contentMode; + pendingState.userInteractionEnabled = view.userInteractionEnabled; +#if TARGET_OS_IOS + pendingState.exclusiveTouch = view.exclusiveTouch; +#endif + pendingState.shadowColor = layer.shadowColor; + pendingState.shadowOpacity = layer.shadowOpacity; + pendingState.shadowOffset = layer.shadowOffset; + pendingState.shadowRadius = layer.shadowRadius; + pendingState.borderWidth = layer.borderWidth; + pendingState.borderColor = layer.borderColor; + pendingState.autoresizingMask = view.autoresizingMask; + pendingState.autoresizesSubviews = view.autoresizesSubviews; + pendingState.needsDisplayOnBoundsChange = layer.needsDisplayOnBoundsChange; + pendingState.allowsGroupOpacity = layer.allowsGroupOpacity; + pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; + pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; + pendingState.semanticContentAttribute = view.semanticContentAttribute; + pendingState.layoutMargins = view.layoutMargins; + pendingState.preservesSuperviewLayoutMargins = view.preservesSuperviewLayoutMargins; + if (AS_AVAILABLE_IOS(11)) { + pendingState.insetsLayoutMarginsFromSafeArea = view.insetsLayoutMarginsFromSafeArea; + } + pendingState.isAccessibilityElement = view.isAccessibilityElement; + pendingState.accessibilityLabel = view.accessibilityLabel; + pendingState.accessibilityHint = view.accessibilityHint; + pendingState.accessibilityValue = view.accessibilityValue; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + pendingState.accessibilityAttributedLabel = view.accessibilityAttributedLabel; + pendingState.accessibilityAttributedHint = view.accessibilityAttributedHint; + pendingState.accessibilityAttributedValue = view.accessibilityAttributedValue; + } +#endif + pendingState.accessibilityTraits = view.accessibilityTraits; + pendingState.accessibilityFrame = view.accessibilityFrame; + pendingState.accessibilityLanguage = view.accessibilityLanguage; + pendingState.accessibilityElementsHidden = view.accessibilityElementsHidden; + pendingState.accessibilityViewIsModal = view.accessibilityViewIsModal; + pendingState.shouldGroupAccessibilityChildren = view.shouldGroupAccessibilityChildren; + pendingState.accessibilityIdentifier = view.accessibilityIdentifier; + pendingState.accessibilityNavigationStyle = view.accessibilityNavigationStyle; +#if TARGET_OS_TV + pendingState.accessibilityHeaderElements = view.accessibilityHeaderElements; +#endif + pendingState.accessibilityActivationPoint = view.accessibilityActivationPoint; + pendingState.accessibilityPath = view.accessibilityPath; + return pendingState; +} + +- (void)clearChanges +{ + _flags = (ASPendingStateFlags){ 0 }; +} + +- (BOOL)hasSetNeedsLayout +{ + return _flags.needsLayout; +} + +- (BOOL)hasSetNeedsDisplay +{ + return _flags.needsDisplay; +} + +- (BOOL)hasChanges +{ + ASPendingStateFlags flags = _flags; + + return (flags.setAnchorPoint + || flags.setPosition + || flags.setZPosition + || flags.setFrame + || flags.setBounds + || flags.setPosition + || flags.setTransform + || flags.setSublayerTransform + || flags.setContents + || flags.setContentsGravity + || flags.setContentsRect + || flags.setContentsCenter + || flags.setContentsScale + || flags.setRasterizationScale + || flags.setClipsToBounds + || flags.setBackgroundColor + || flags.setTintColor + || flags.setHidden + || flags.setAlpha + || flags.setCornerRadius + || flags.setContentMode + || flags.setUserInteractionEnabled + || flags.setExclusiveTouch + || flags.setShadowOpacity + || flags.setShadowOffset + || flags.setShadowRadius + || flags.setShadowColor + || flags.setBorderWidth + || flags.setBorderColor + || flags.setAutoresizingMask + || flags.setAutoresizesSubviews + || flags.setNeedsDisplayOnBoundsChange + || flags.setAllowsGroupOpacity + || flags.setAllowsEdgeAntialiasing + || flags.setEdgeAntialiasingMask + || flags.needsDisplay + || flags.needsLayout + || flags.setAsyncTransactionContainer + || flags.setOpaque + || flags.setSemanticContentAttribute + || flags.setLayoutMargins + || flags.setPreservesSuperviewLayoutMargins + || flags.setInsetsLayoutMarginsFromSafeArea + || flags.setIsAccessibilityElement + || flags.setAccessibilityLabel + || flags.setAccessibilityAttributedLabel + || flags.setAccessibilityHint + || flags.setAccessibilityAttributedHint + || flags.setAccessibilityValue + || flags.setAccessibilityAttributedValue + || flags.setAccessibilityTraits + || flags.setAccessibilityFrame + || flags.setAccessibilityLanguage + || flags.setAccessibilityElementsHidden + || flags.setAccessibilityViewIsModal + || flags.setShouldGroupAccessibilityChildren + || flags.setAccessibilityIdentifier + || flags.setAccessibilityNavigationStyle + || flags.setAccessibilityHeaderElements + || flags.setAccessibilityActivationPoint + || flags.setAccessibilityPath); +} + +- (void)dealloc +{ + CGColorRelease(backgroundColor); + + if (shadowColor != blackColorRef) { + CGColorRelease(shadowColor); + } + + if (borderColor != blackColorRef) { + CGColorRelease(borderColor); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/Private/_ASScopeTimer.h b/submodules/AsyncDisplayKit/Source/Private/_ASScopeTimer.h new file mode 100644 index 0000000000..523599dd0a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/Private/_ASScopeTimer.h @@ -0,0 +1,56 @@ +// +// _ASScopeTimer.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +/** + Must compile as c++ for this to work. + + Usage: + // Can be an ivar or local variable + NSTimeInterval placeToStoreTiming; + + { + // some scope + AS::ScopeTimer t(placeToStoreTiming); + DoPotentiallySlowWork(); + MorePotentiallySlowWork(); + } + + */ + +namespace AS { + struct ScopeTimer { + NSTimeInterval begin; + NSTimeInterval &outT; + ScopeTimer(NSTimeInterval &outRef) : outT(outRef) { + begin = CACurrentMediaTime(); + } + ~ScopeTimer() { + outT = CACurrentMediaTime() - begin; + } + }; + + // variant where repeated calls are summed + struct SumScopeTimer { + NSTimeInterval begin; + NSTimeInterval &outT; + BOOL enable; + SumScopeTimer(NSTimeInterval &outRef, BOOL enable = YES) : outT(outRef), enable(enable) { + if (enable) { + begin = CACurrentMediaTime(); + } + } + ~SumScopeTimer() { + if (enable) { + outT += CACurrentMediaTime() - begin; + } + } + }; +} diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASLayoutManager.h b/submodules/AsyncDisplayKit/Source/TextKit/ASLayoutManager.h new file mode 100644 index 0000000000..1396bf203e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASLayoutManager.h @@ -0,0 +1,16 @@ +// +// ASLayoutManager.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +AS_SUBCLASSING_RESTRICTED +@interface ASLayoutManager : NSLayoutManager + +@end diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASLayoutManager.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASLayoutManager.mm new file mode 100644 index 0000000000..fbb3b49ea4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASLayoutManager.mm @@ -0,0 +1,42 @@ +// +// ASLayoutManager.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@implementation ASLayoutManager + +- (void)showCGGlyphs:(const CGGlyph *)glyphs + positions:(const CGPoint *)positions + count:(NSUInteger)glyphCount + font:(UIFont *)font + matrix:(CGAffineTransform)textMatrix + attributes:(NSDictionary *)attributes + inContext:(CGContextRef)graphicsContext +{ + + // NSLayoutManager has a hard coded internal color for hyperlinks which ignores + // NSForegroundColorAttributeName. To get around this, we force the fill color + // in the current context to match NSForegroundColorAttributeName. + UIColor *foregroundColor = attributes[NSForegroundColorAttributeName]; + + if (foregroundColor) + { + CGContextSetFillColorWithColor(graphicsContext, foregroundColor.CGColor); + } + + [super showCGGlyphs:glyphs + positions:positions + count:glyphCount + font:font + matrix:textMatrix + attributes:attributes + inContext:graphicsContext]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitAttributes.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitAttributes.h new file mode 100644 index 0000000000..d8c21dbc23 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitAttributes.h @@ -0,0 +1,128 @@ +// +// ASTextKitAttributes.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +AS_EXTERN NSString *const ASTextKitTruncationAttributeName; +/** + Use ASTextKitEntityAttribute as the value of this attribute to embed a link or other interactable content inside the + text. + */ +AS_EXTERN NSString *const ASTextKitEntityAttributeName; + +/** + All NSObject values in this struct should be copied when passed into the TextComponent. + */ +struct ASTextKitAttributes { + /** + The string to be drawn. ASTextKit will not augment this string with default colors, etc. so this must be complete. + */ + NSAttributedString *attributedString; + /** + The string to use as the truncation string, usually just "...". If you have a range of text you would like to + restrict highlighting to (for instance if you have "... Continue Reading", use the ASTextKitTruncationAttributeName + to mark the specific range of the string that should be highlightable. + */ + NSAttributedString *truncationAttributedString; + /** + This is the character set that ASTextKit should attempt to avoid leaving as a trailing character before your + truncation token. By default this set includes "\s\t\n\r.,!?:;" so you don't end up with ugly looking truncation + text like "Hey, this is some fancy Truncation!\n\n...". Instead it would be truncated as "Hey, this is some fancy + truncation...". This is not always possible. + + Set this to the empty charset if you want to just use the "dumb" truncation behavior. A nil value will be + substituted with the default described above. + */ + NSCharacterSet *avoidTailTruncationSet; + /** + The line-break mode to apply to the text. Since this also impacts how TextKit will attempt to truncate the text + in your string, we only support NSLineBreakByWordWrapping and NSLineBreakByCharWrapping. + */ + NSLineBreakMode lineBreakMode; + /** + The maximum number of lines to draw in the drawable region. Leave blank or set to 0 to define no maximum. + This is required to apply scale factors to shrink text to fit within a number of lines + */ + NSUInteger maximumNumberOfLines; + /** + An array of UIBezierPath objects representing the exclusion paths inside the receiver's bounding rectangle. Default value: nil. + */ + NSArray *exclusionPaths; + /** + The shadow offset for any shadows applied to the text. The coordinate space for this is the same as UIKit, so a + positive width means towards the right, and a positive height means towards the bottom. + */ + CGSize shadowOffset; + /** + The color to use in drawing the text's shadow. + */ + UIColor *shadowColor; + /** + The opacity of the shadow from 0 to 1. + */ + CGFloat shadowOpacity; + /** + The radius that should be applied to the shadow blur. Larger values mean a larger, more blurred shadow. + */ + CGFloat shadowRadius; + /** + An array of scale factors in descending order to apply to the text to try to make it fit into a constrained size. + */ + NSArray *pointSizeScaleFactors; + + /** + We provide an explicit copy function so we can use aggregate initializer syntax while providing copy semantics for + the NSObjects inside. + */ + const ASTextKitAttributes copy() const + { + return { + [attributedString copy], + [truncationAttributedString copy], + [avoidTailTruncationSet copy], + lineBreakMode, + maximumNumberOfLines, + [exclusionPaths copy], + shadowOffset, + [shadowColor copy], + shadowOpacity, + shadowRadius, + pointSizeScaleFactors, + }; + }; + + bool operator==(const ASTextKitAttributes &other) const + { + // These comparisons are in a specific order to reduce the overall cost of this function. + return lineBreakMode == other.lineBreakMode + && maximumNumberOfLines == other.maximumNumberOfLines + && shadowOpacity == other.shadowOpacity + && shadowRadius == other.shadowRadius + && (pointSizeScaleFactors == other.pointSizeScaleFactors + || [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors]) + && CGSizeEqualToSize(shadowOffset, other.shadowOffset) + && ASObjectIsEqual(exclusionPaths, other.exclusionPaths) + && ASObjectIsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet) + && ASObjectIsEqual(shadowColor, other.shadowColor) + && ASObjectIsEqual(attributedString, other.attributedString) + && ASObjectIsEqual(truncationAttributedString, other.truncationAttributedString); + } + + size_t hash() const; +}; + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitAttributes.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitAttributes.mm new file mode 100644 index 0000000000..400ef437bb --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitAttributes.mm @@ -0,0 +1,50 @@ +// +// ASTextKitAttributes.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +NSString *const ASTextKitTruncationAttributeName = @"ck_truncation"; +NSString *const ASTextKitEntityAttributeName = @"ck_entity"; + +size_t ASTextKitAttributes::hash() const +{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + NSUInteger attrStringHash; + NSUInteger truncationStringHash; + NSUInteger avoidTrunactionSetHash; + NSLineBreakMode lineBreakMode; + NSUInteger maximumNumberOfLines; + NSUInteger exclusionPathsHash; + CGSize shadowOffset; + NSUInteger shadowColorHash; + CGFloat shadowOpacity; + CGFloat shadowRadius; +#pragma clang diagnostic pop + } data = { + [attributedString hash], + [truncationAttributedString hash], + [avoidTailTruncationSet hash], + lineBreakMode, + maximumNumberOfLines, + [exclusionPaths hash], + shadowOffset, + [shadowColor hash], + shadowOpacity, + shadowRadius, + }; + return ASHashBytes(&data, sizeof(data)); +} + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitComponents.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitComponents.h new file mode 100644 index 0000000000..df89db203d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitComponents.h @@ -0,0 +1,64 @@ +// +// ASTextKitComponents.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASTextKitComponentsTextView : UITextView +- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder __unavailable; +- (instancetype)init __unavailable; +@end + +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitComponents : NSObject + +/** + @abstract Creates the stack of TextKit components. + @param attributedSeedString The attributed string to seed the returned text storage with, or nil to receive an blank text storage. + @param textContainerSize The size of the text-container. Typically, size specifies the constraining width of the layout, and CGFLOAT_MAX for height. Pass CGSizeZero if these components will be hooked up to a UITextView, which will manage the text container's size itself. + @return An `ASTextKitComponents` containing the created components. The text view component will be nil. + @discussion The returned components will be hooked up together, so they are ready for use as a system upon return. + */ ++ (instancetype)componentsWithAttributedSeedString:(nullable NSAttributedString *)attributedSeedString + textContainerSize:(CGSize)textContainerSize NS_RETURNS_RETAINED; + +/** + @abstract Creates the stack of TextKit components. + @param textStorage The NSTextStorage to use. + @param textContainerSize The size of the text-container. Typically, size specifies the constraining width of the layout, and CGFLOAT_MAX for height. Pass CGSizeZero if these components will be hooked up to a UITextView, which will manage the text container's size itself. + @param layoutManager The NSLayoutManager to use. + @return An `ASTextKitComponents` containing the created components. The text view component will be nil. + @discussion The returned components will be hooked up together, so they are ready for use as a system upon return. + */ ++ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage + textContainerSize:(CGSize)textContainerSize + layoutManager:(NSLayoutManager *)layoutManager NS_RETURNS_RETAINED; + +/** + @abstract Returns the bounding size for the text view's text. + @param constrainedWidth The constraining width to be used during text-sizing. Usually, this value should be the receiver's calculated size. + @result A CGSize representing the bounding size for the receiver's text. + */ +- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth; + +- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth + forMaxNumberOfLines:(NSInteger)numberOfLines; + +@property (nonatomic, readonly) NSTextStorage *textStorage; +@property (nonatomic, readonly) NSTextContainer *textContainer; +@property (nonatomic, readonly) NSLayoutManager *layoutManager; +@property (nonatomic, nullable) ASTextKitComponentsTextView *textView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitComponents.mm new file mode 100644 index 0000000000..eeff17607d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitComponents.mm @@ -0,0 +1,194 @@ +// +// ASTextKitComponents.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +#import + +@interface ASTextKitComponentsTextView () { + // Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860 + BOOL _deallocating; +} +@property CGRect threadSafeBounds; +@end + +@implementation ASTextKitComponentsTextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer +{ + self = [super initWithFrame:frame textContainer:textContainer]; + if (self) { + _threadSafeBounds = self.bounds; + _deallocating = NO; + } + return self; +} + +- (void)dealloc +{ + _deallocating = YES; +} + +- (void)setFrame:(CGRect)frame +{ + ASDisplayNodeAssertMainThread(); + [super setFrame:frame]; + self.threadSafeBounds = self.bounds; +} + +- (void)setBounds:(CGRect)bounds +{ + ASDisplayNodeAssertMainThread(); + [super setBounds:bounds]; + self.threadSafeBounds = bounds; +} + +- (void)setContentOffset:(CGPoint)contentOffset +{ + if (_deallocating) { + return; + } + + [super setContentOffset:contentOffset]; +} + + +@end + +@interface ASTextKitComponents () + +// read-write redeclarations +@property (nonatomic) NSTextStorage *textStorage; +@property (nonatomic) NSTextContainer *textContainer; +@property (nonatomic) NSLayoutManager *layoutManager; + +@end + +@implementation ASTextKitComponents + +#pragma mark - Class + ++ (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString + textContainerSize:(CGSize)textContainerSize NS_RETURNS_RETAINED +{ + NSTextStorage *textStorage = attributedSeedString ? [[NSTextStorage alloc] initWithAttributedString:attributedSeedString] : [[NSTextStorage alloc] init]; + + return [self componentsWithTextStorage:textStorage + textContainerSize:textContainerSize + layoutManager:[[NSLayoutManager alloc] init]]; +} + ++ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage + textContainerSize:(CGSize)textContainerSize + layoutManager:(NSLayoutManager *)layoutManager NS_RETURNS_RETAINED +{ + ASTextKitComponents *components = [[self alloc] init]; + + components.textStorage = textStorage; + + components.layoutManager = layoutManager; + [components.textStorage addLayoutManager:components.layoutManager]; + + components.textContainer = [[NSTextContainer alloc] initWithSize:textContainerSize]; + components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. + [components.layoutManager addTextContainer:components.textContainer]; + + return components; +} + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +#pragma mark - Lifecycle + +- (void)dealloc +{ + // Nil out all delegates to prevent crash + if (_textView) { + ASDisplayNodeAssertMainThread(); + _textView.delegate = nil; + } + _layoutManager.delegate = nil; +} + +#pragma mark - Sizing + +- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth +{ + ASTextKitComponents *components = self; + + // If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation. + // Otherwise, we create a temporary stack to size for `constrainedWidth`. + if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) { + components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + } + + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:). + [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; + CGSize textSize = [components.layoutManager usedRectForTextContainer:components.textContainer].size; + + return textSize; +} + +- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth + forMaxNumberOfLines:(NSInteger)maxNumberOfLines +{ + if (maxNumberOfLines == 0) { + return [self sizeForConstrainedWidth:constrainedWidth]; + } + + ASTextKitComponents *components = self; + + // Always use temporary stack in case of threading issues + components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by - usedRectForTextContainer:). + [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; + + CGFloat width = [components.layoutManager usedRectForTextContainer:components.textContainer].size.width; + + // Calculate height based on line fragments + // Based on calculating number of lines from: http://asciiwwdc.com/2013/sessions/220 + NSRange glyphRange, lineRange = NSMakeRange(0, 0); + CGRect rect = CGRectZero; + CGFloat height = 0; + CGFloat lastOriginY = -1.0; + NSUInteger numberOfLines = 0; + + glyphRange = [components.layoutManager glyphRangeForTextContainer:components.textContainer]; + + while (lineRange.location < NSMaxRange(glyphRange)) { + rect = [components.layoutManager lineFragmentRectForGlyphAtIndex:lineRange.location + effectiveRange:&lineRange]; + + if (CGRectGetMinY(rect) > lastOriginY) { + ++numberOfLines; + if (numberOfLines == maxNumberOfLines) { + height = rect.origin.y + rect.size.height; + break; + } + } + + lastOriginY = CGRectGetMinY(rect); + lineRange.location = NSMaxRange(lineRange); + } + + CGFloat fragmentHeight = rect.origin.y + rect.size.height; + CGFloat finalHeight = std::ceil(std::fmax(height, fragmentHeight)); + + CGSize size = CGSizeMake(width, finalHeight); + + return size; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitContext.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitContext.h new file mode 100644 index 0000000000..82a40b7d8d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitContext.h @@ -0,0 +1,53 @@ +// +// ASTextKitContext.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +/** + A threadsafe container for the TextKit components that ASTextKit uses to lay out and truncate its text. + + This container is the sole owner and manager of the TextKit classes. This is an important model because of major + thread safety issues inside vanilla TextKit. It provides a central locking location for accessing TextKit methods. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitContext : NSObject + +/** + Initializes a context and its associated TextKit components. + + Initialization of TextKit components is a globally locking operation so be careful of bottlenecks with this class. + */ +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + lineBreakMode:(NSLineBreakMode)lineBreakMode + maximumNumberOfLines:(NSUInteger)maximumNumberOfLines + exclusionPaths:(NSArray *)exclusionPaths + constrainedSize:(CGSize)constrainedSize; + +/** + All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to + TextKit components may cause crashes. + + The block provided MUST not call out to client code from within its scope or it is possible for this to cause deadlocks + in your application. Use with EXTREME care. + + Callers MUST NOT keep a ref to these internal objects and use them later. This WILL cause crashes in your application. + */ +- (void)performBlockWithLockedTextKitComponents:(AS_NOESCAPE void (^)(NSLayoutManager *layoutManager, + NSTextStorage *textStorage, + NSTextContainer *textContainer))block; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitContext.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitContext.mm new file mode 100644 index 0000000000..8883f3175f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitContext.mm @@ -0,0 +1,84 @@ +// +// ASTextKitContext.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import +#import + +#include + +@implementation ASTextKitContext +{ + // All TextKit operations (even non-mutative ones) must be executed serially. + std::shared_ptr __instanceLock__; + + NSLayoutManager *_layoutManager; + NSTextStorage *_textStorage; + NSTextContainer *_textContainer; +} + +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + lineBreakMode:(NSLineBreakMode)lineBreakMode + maximumNumberOfLines:(NSUInteger)maximumNumberOfLines + exclusionPaths:(NSArray *)exclusionPaths + constrainedSize:(CGSize)constrainedSize + +{ + if (self = [super init]) { + static dispatch_once_t onceToken; + static AS::Mutex *mutex; + dispatch_once(&onceToken, ^{ + mutex = new AS::Mutex(); + }); + + // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. + AS::MutexLocker l(*mutex); + + __instanceLock__ = std::make_shared(); + + // Create the TextKit component stack with our default configuration. + + _textStorage = [[NSTextStorage alloc] init]; + _layoutManager = [[ASLayoutManager alloc] init]; + _layoutManager.usesFontLeading = NO; + [_textStorage addLayoutManager:_layoutManager]; + + // Instead of calling [NSTextStorage initWithAttributedString:], setting attributedString just after calling addlayoutManager can fix CJK language layout issues. + // See https://github.com/facebook/AsyncDisplayKit/issues/2894 + if (attributedString) { + [_textStorage setAttributedString:attributedString]; + } + + _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize]; + // We want the text laid out up to the very edges of the container. + _textContainer.lineFragmentPadding = 0; + _textContainer.lineBreakMode = lineBreakMode; + _textContainer.maximumNumberOfLines = maximumNumberOfLines; + _textContainer.exclusionPaths = exclusionPaths; + [_layoutManager addTextContainer:_textContainer]; + } + return self; +} + +- (void)performBlockWithLockedTextKitComponents:(NS_NOESCAPE void (^)(NSLayoutManager *, + NSTextStorage *, + NSTextContainer *))block +{ + AS::MutexLocker l(*__instanceLock__); + if (block) { + block(_layoutManager, _textStorage, _textContainer); + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitCoreTextAdditions.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitCoreTextAdditions.h new file mode 100644 index 0000000000..9a8f48050a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitCoreTextAdditions.h @@ -0,0 +1,89 @@ +// +// ASTextKitCoreTextAdditions.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract Returns whether a given attribute is an unsupported Core Text attribute. + @param attributeName The name of the attribute + @discussion The following Core Text attributes are not supported on NSAttributedString, and thus will not be preserved during the conversion: + - kCTForegroundColorFromContextAttributeName + - kCTSuperscriptAttributeName + - kCTGlyphInfoAttributeName + - kCTCharacterShapeAttributeName + - kCTLanguageAttributeName + - kCTRunDelegateAttributeName + - kCTBaselineClassAttributeName + - kCTBaselineInfoAttributeName + - kCTBaselineReferenceInfoAttributeName + - kCTWritingDirectionAttributeName + - kCTUnderlineColorAttributeName + @result Whether attributeName is an unsupported Core Text attribute. + */ +AS_EXTERN BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName); + + +/** + @abstract Returns an attributes dictionary for use by NSAttributedString, given a dictionary of Core Text attributes. + @param coreTextAttributes An NSDictionary whose keys are CFAttributedStringRef attributes. + @discussion The following Core Text attributes are not supported on NSAttributedString, and thus will not be preserved during the conversion: + - kCTForegroundColorFromContextAttributeName + - kCTSuperscriptAttributeName + - kCTGlyphInfoAttributeName + - kCTCharacterShapeAttributeName + - kCTLanguageAttributeName + - kCTRunDelegateAttributeName + - kCTBaselineClassAttributeName + - kCTBaselineInfoAttributeName + - kCTBaselineReferenceInfoAttributeName + - kCTWritingDirectionAttributeName + - kCTUnderlineColorAttributeName + @result An NSDictionary of attributes for use by NSAttributedString. + */ +AS_EXTERN NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *coreTextAttributes); + +/** + @abstract Returns an NSAttributedString whose Core Text attributes have been converted, where possible, to NSAttributedString attributes. + @param dirtyAttributedString An NSAttributedString that may contain Core Text attributes. + @result An NSAttributedString that's preserved as many CFAttributedString attributes as possible. + */ +AS_EXTERN NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedString *dirtyAttributedString); + +#pragma mark - +#pragma mark - +@interface NSParagraphStyle (ASTextKitCoreTextAdditions) + +/** + @abstract Returns an NSParagraphStyle initialized with the paragraph specifiers from the given CTParagraphStyleRef. + @param coreTextParagraphStyle A Core Text paragraph style. + @discussion It is important to note that not all CTParagraphStyle specifiers are supported by NSParagraphStyle, and consequently, this is a lossy conversion. Notably, the following specifiers will not preserved: + - kCTParagraphStyleSpecifierTabStops + - kCTParagraphStyleSpecifierDefaultTabInterval + - kCTParagraphStyleSpecifierMaximumLineSpacing + - kCTParagraphStyleSpecifierMinimumLineSpacing + - kCTParagraphStyleSpecifierLineSpacingAdjustment + - kCTParagraphStyleSpecifierLineBoundsOptions + @result An NSParagraphStyle initialized with as many of the paragraph specifiers from `coreTextParagraphStyle` as possible. + + */ ++ (NSParagraphStyle *)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle NS_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitCoreTextAdditions.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitCoreTextAdditions.mm new file mode 100644 index 0000000000..e98b236c56 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitCoreTextAdditions.mm @@ -0,0 +1,339 @@ +// +// ASTextKitCoreTextAdditions.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import +#import + +#import + +#pragma mark - Public +BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName) +{ + static NSSet *coreTextAttributes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + coreTextAttributes = [NSSet setWithObjects:(__bridge id)kCTForegroundColorAttributeName, + kCTForegroundColorFromContextAttributeName, + kCTForegroundColorAttributeName, + kCTStrokeColorAttributeName, + kCTUnderlineStyleAttributeName, + kCTVerticalFormsAttributeName, + kCTRunDelegateAttributeName, + kCTBaselineClassAttributeName, + kCTBaselineInfoAttributeName, + kCTBaselineReferenceInfoAttributeName, + kCTUnderlineColorAttributeName, + kCTParagraphStyleAttributeName, + nil]; + }); + return [coreTextAttributes containsObject:attributeName]; +} + +NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *coreTextAttributes) +{ + NSMutableDictionary *cleanAttributes = [[NSMutableDictionary alloc] initWithCapacity:coreTextAttributes.count]; + + [coreTextAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *coreTextKey, id coreTextValue, BOOL *stop) { + // The following attributes are not supported on NSAttributedString. Should they become available, we should add them. + /* + kCTForegroundColorFromContextAttributeName + kCTSuperscriptAttributeName + kCTGlyphInfoAttributeName + kCTCharacterShapeAttributeName + kCTLanguageAttributeName + kCTRunDelegateAttributeName + kCTBaselineClassAttributeName + kCTBaselineInfoAttributeName + kCTBaselineReferenceInfoAttributeName + kCTWritingDirectionAttributeName + kCTUnderlineColorAttributeName + */ + + // Conversely, the following attributes are not supported on CFAttributedString. Should they become available, we should add them. + /* + NSStrikethroughStyleAttributeName + NSShadowAttributeName + NSBackgroundColorAttributeName + */ + + // kCTFontAttributeName -> NSFontAttributeName + if ([coreTextKey isEqualToString:(NSString *)kCTFontAttributeName]) { + CTFontRef coreTextFont = (__bridge CTFontRef)coreTextValue; + NSString *fontName = (__bridge_transfer NSString *)CTFontCopyPostScriptName(coreTextFont); + CGFloat fontSize = CTFontGetSize(coreTextFont); + UIFont *font = [UIFont fontWithName:fontName size:fontSize]; + ASDisplayNodeCAssertNotNil(font, @"unable to load font %@ with size %f", fontName, fontSize); + if (font == nil) { + // Gracefully fail if we were unable to load the font. + font = [UIFont systemFontOfSize:fontSize]; + } + cleanAttributes[NSFontAttributeName] = font; + } + // kCTKernAttributeName -> NSKernAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTKernAttributeName]) { + cleanAttributes[NSKernAttributeName] = (NSNumber *)coreTextValue; + } + // kCTLigatureAttributeName -> NSLigatureAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTLigatureAttributeName]) { + cleanAttributes[NSLigatureAttributeName] = (NSNumber *)coreTextValue; + } + // kCTForegroundColorAttributeName -> NSForegroundColorAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTForegroundColorAttributeName]) { + cleanAttributes[NSForegroundColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue]; + } + // kCTParagraphStyleAttributeName -> NSParagraphStyleAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTParagraphStyleAttributeName]) { + if ([coreTextValue isKindOfClass:[NSParagraphStyle class]]) { + cleanAttributes[NSParagraphStyleAttributeName] = (NSParagraphStyle *)coreTextValue; + } + else { + cleanAttributes[NSParagraphStyleAttributeName] = [NSParagraphStyle paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextValue]; + } + } + // kCTStrokeWidthAttributeName -> NSStrokeWidthAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeWidthAttributeName]) { + cleanAttributes[NSStrokeWidthAttributeName] = (NSNumber *)coreTextValue; + } + // kCTStrokeColorAttributeName -> NSStrokeColorAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeColorAttributeName]) { + cleanAttributes[NSStrokeColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue]; + } + // kCTUnderlineStyleAttributeName -> NSUnderlineStyleAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTUnderlineStyleAttributeName]) { + cleanAttributes[NSUnderlineStyleAttributeName] = (NSNumber *)coreTextValue; + } + // kCTVerticalFormsAttributeName -> NSVerticalGlyphFormAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTVerticalFormsAttributeName]) { + BOOL flag = (BOOL)CFBooleanGetValue((CFBooleanRef)coreTextValue); + cleanAttributes[NSVerticalGlyphFormAttributeName] = @((int)flag); // NSVerticalGlyphFormAttributeName is documented to be an NSNumber with an integer that's either 0 or 1. + } + // Don't filter out any internal text attributes + else if (!ASAttributeWithNameIsUnsupportedCoreTextAttribute(coreTextKey)){ + cleanAttributes[coreTextKey] = coreTextValue; + } + }]; + + return cleanAttributes; +} + +NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedString *dirtyAttributedString) +{ + if (!dirtyAttributedString) + return nil; + + // First see if there are any core text attributes on the string + __block BOOL containsCoreTextAttributes = NO; + [dirtyAttributedString enumerateAttributesInRange:NSMakeRange(0, dirtyAttributedString.length) + options:0 + usingBlock:^(NSDictionary *dirtyAttributes, NSRange range, BOOL *stop) { + [dirtyAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *coreTextKey, id coreTextValue, BOOL *innerStop) { + if (ASAttributeWithNameIsUnsupportedCoreTextAttribute(coreTextKey)) { + containsCoreTextAttributes = YES; + *innerStop = YES; + } + }]; + *stop = containsCoreTextAttributes; + }]; + if (containsCoreTextAttributes) { + + NSString *plainString = dirtyAttributedString.string; + NSMutableAttributedString *cleanAttributedString = [[NSMutableAttributedString alloc] initWithString:plainString]; + + // Iterate over all of the attributes, cleaning them as appropriate and applying them as we go. + [dirtyAttributedString enumerateAttributesInRange:NSMakeRange(0, plainString.length) + options:0 + usingBlock:^(NSDictionary *dirtyAttributes, NSRange range, BOOL *stop) { + [cleanAttributedString addAttributes:NSAttributedStringAttributesForCoreTextAttributes(dirtyAttributes) range:range]; + }]; + + return cleanAttributedString; + } else { + return [dirtyAttributedString copy]; + } +} + +#pragma mark - +#pragma mark - +@implementation NSParagraphStyle (ASTextKitCoreTextAdditions) + ++ (NSParagraphStyle *)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle NS_RETURNS_RETAINED +{ + NSMutableParagraphStyle *newParagraphStyle = [[NSMutableParagraphStyle alloc] init]; + + if (!coreTextParagraphStyle) { + return newParagraphStyle; + } + + // The following paragraph style specifiers are not supported on NSParagraphStyle. Should they become available, we should add them. + /* + kCTParagraphStyleSpecifierTabStops + kCTParagraphStyleSpecifierDefaultTabInterval + kCTParagraphStyleSpecifierMaximumLineSpacing + kCTParagraphStyleSpecifierMinimumLineSpacing + kCTParagraphStyleSpecifierLineSpacingAdjustment + kCTParagraphStyleSpecifierLineBoundsOptions + */ + + // Conversely, the following paragraph styles are not supported on CTParagraphStyle. Should they become available, we should add them. + /* + hyphenationFactor + */ + + // kCTParagraphStyleSpecifierAlignment -> alignment + CTTextAlignment coreTextAlignment; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierAlignment, + sizeof(coreTextAlignment), + &coreTextAlignment)) { + newParagraphStyle.alignment = NSTextAlignmentFromCTTextAlignment(coreTextAlignment); + } + + // kCTParagraphStyleSpecifierFirstLineHeadIndent -> firstLineHeadIndent + CGFloat firstLineHeadIndent; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierFirstLineHeadIndent, + sizeof(firstLineHeadIndent), + &firstLineHeadIndent)) { + newParagraphStyle.firstLineHeadIndent = firstLineHeadIndent; + } + + // kCTParagraphStyleSpecifierHeadIndent -> headIndent + CGFloat headIndent; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierHeadIndent, + sizeof(headIndent), + &headIndent)) { + newParagraphStyle.headIndent = headIndent; + } + + // kCTParagraphStyleSpecifierTailIndent -> tailIndent + CGFloat tailIndent; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierTailIndent, + sizeof(tailIndent), + &tailIndent)) { + newParagraphStyle.tailIndent = tailIndent; + } + + // kCTParagraphStyleSpecifierLineBreakMode -> lineBreakMode + CTLineBreakMode coreTextLineBreakMode; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineBreakMode, + sizeof(coreTextLineBreakMode), + &coreTextLineBreakMode)) { + newParagraphStyle.lineBreakMode = (NSLineBreakMode)coreTextLineBreakMode; // They're the same enum. + } + + // kCTParagraphStyleSpecifierLineHeightMultiple -> lineHeightMultiple + CGFloat lineHeightMultiple; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineHeightMultiple, + sizeof(lineHeightMultiple), + &lineHeightMultiple)) { + newParagraphStyle.lineHeightMultiple = lineHeightMultiple; + } + + // kCTParagraphStyleSpecifierMaximumLineHeight -> maximumLineHeight + CGFloat maximumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMaximumLineHeight, + sizeof(maximumLineHeight), + &maximumLineHeight)) { + newParagraphStyle.maximumLineHeight = maximumLineHeight; + } + + // kCTParagraphStyleSpecifierMinimumLineHeight -> minimumLineHeight + CGFloat minimumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMinimumLineHeight, + sizeof(minimumLineHeight), + &minimumLineHeight)) { + newParagraphStyle.minimumLineHeight = minimumLineHeight; + } + + CGFloat lineSpacing = 0; +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // kCTParagraphStyleSpecifierLineSpacing -> lineSpacing + // Note that kCTParagraphStyleSpecifierLineSpacing is deprecated and will die soon. We should not be using it. + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineSpacing, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } +#pragma clang diagnostic pop +#endif + + // Attempt to weakly map the following onto -[NSParagraphStyle lineSpacing]: + // - kCTParagraphStyleSpecifierMinimumLineSpacing + // - kCTParagraphStyleSpecifierMaximumLineSpacing + // - kCTParagraphStyleSpecifierLineSpacingAdjustment + if (fabs(lineSpacing) <= FLT_EPSILON && + CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMinimumLineSpacing, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } + + if (fabs(lineSpacing) <= FLT_EPSILON && + CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMaximumLineSpacing, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } + + if (fabs(lineSpacing) <= FLT_EPSILON && + CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineSpacingAdjustment, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } + + // kCTParagraphStyleSpecifierParagraphSpacing -> paragraphSpacing + CGFloat paragraphSpacing; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierParagraphSpacing, + sizeof(paragraphSpacing), + ¶graphSpacing)) { + newParagraphStyle.paragraphSpacing = paragraphSpacing; + } + + // kCTParagraphStyleSpecifierParagraphSpacingBefore -> paragraphSpacingBefore + CGFloat paragraphSpacingBefore; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierParagraphSpacingBefore, + sizeof(paragraphSpacingBefore), + ¶graphSpacingBefore)) { + newParagraphStyle.paragraphSpacingBefore = paragraphSpacingBefore; + } + + // kCTParagraphStyleSpecifierBaseWritingDirection -> baseWritingDirection + CTWritingDirection coreTextBaseWritingDirection; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierBaseWritingDirection, + sizeof(coreTextBaseWritingDirection), + &coreTextBaseWritingDirection)) { + newParagraphStyle.baseWritingDirection = (NSWritingDirection)coreTextBaseWritingDirection; // They're the same enum. + } + + return newParagraphStyle; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitEntityAttribute.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitEntityAttribute.h new file mode 100644 index 0000000000..3655138be7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitEntityAttribute.h @@ -0,0 +1,36 @@ +// +// ASTextKitEntityAttribute.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +/** + The object that should be embedded with ASTextKitEntityAttributeName. Please note that the entity you provide MUST + implement a proper hash and isEqual function or your application performance will grind to a halt due to + NSMutableAttributedString's usage of a global hash table of all attributes. This means the entity should NOT be a + Foundation Collection (NSArray, NSDictionary, NSSet, etc.) since their hash function is a simple count of the values + in the collection, which causes pathological performance problems deep inside NSAttributedString's implementation. + + rdar://19352367 + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitEntityAttribute : NSObject + +@property (nonatomic, readonly) id entity; + +- (instancetype)initWithEntity:(id)entity; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitEntityAttribute.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitEntityAttribute.mm new file mode 100644 index 0000000000..fb87e9bd3a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitEntityAttribute.mm @@ -0,0 +1,43 @@ +// +// ASTextKitEntityAttribute.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +@implementation ASTextKitEntityAttribute + +- (instancetype)initWithEntity:(id)entity +{ + if (self = [super init]) { + _entity = entity; + } + return self; +} + +- (NSUInteger)hash +{ + return [_entity hash]; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + if (![object isKindOfClass:[self class]]) { + return NO; + } + ASTextKitEntityAttribute *other = (ASTextKitEntityAttribute *)object; + return _entity == other.entity || [_entity isEqual:other.entity]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.h new file mode 100644 index 0000000000..1b7b10ff5f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.h @@ -0,0 +1,58 @@ +// +// ASTextKitFontSizeAdjuster.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASTextKitContext; + +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitFontSizeAdjuster : NSObject + +@property (nonatomic) CGSize constrainedSize; + +/** + * Creates a class that will return a scale factor the will make a string fit inside the constrained size. + * + * "Fitting" means that both the longest word in the string will fit without breaking in the constrained + * size's width AND that the entire string will try to fit within attribute's maximumLineCount. The amount + * that the string will scale is based upon the attribute's pointSizeScaleFactors. If the string cannot fit + * in the given width/number of lines, the smallest scale factor will be returned. + * + * @param context The text kit context + * @param constrainedSize The constrained size to render into + * @param textComponentAttributes The renderer's text attributes + */ +- (instancetype)initWithContext:(ASTextKitContext *)context + constrainedSize:(CGSize)constrainedSize + textKitAttributes:(const ASTextKitAttributes &)textComponentAttributes; + +/** + * Returns the best fit scale factor for the text + */ +- (CGFloat)scaleFactor; + +/** + * Takes all of the attributed string attributes dealing with size (font size, line spacing, kerning, etc) and + * scales them by the scaleFactor. I wouldn't be surprised if I missed some in here. + */ ++ (void)adjustFontSizeForAttributeString:(NSMutableAttributedString *)attrString withScaleFactor:(CGFloat)scaleFactor; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.mm new file mode 100644 index 0000000000..aeea44d7cc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.mm @@ -0,0 +1,241 @@ +// +// ASTextKitFontSizeAdjuster.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import + +#if AS_ENABLE_TEXTNODE + +#import +#import + +#import +#import +#import + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +@interface ASTextKitFontSizeAdjuster() +@property (nonatomic, readonly) NSLayoutManager *sizingLayoutManager; +@property (nonatomic, readonly) NSTextContainer *sizingTextContainer; +@end + +@implementation ASTextKitFontSizeAdjuster +{ + __weak ASTextKitContext *_context; + ASTextKitAttributes _attributes; + BOOL _measured; + CGFloat _scaleFactor; + AS::Mutex __instanceLock__; +} + +@synthesize sizingLayoutManager = _sizingLayoutManager; +@synthesize sizingTextContainer = _sizingTextContainer; + +- (instancetype)initWithContext:(ASTextKitContext *)context + constrainedSize:(CGSize)constrainedSize + textKitAttributes:(const ASTextKitAttributes &)textComponentAttributes; +{ + if (self = [super init]) { + _context = context; + _constrainedSize = constrainedSize; + _attributes = textComponentAttributes; + } + return self; +} + ++ (void)adjustFontSizeForAttributeString:(NSMutableAttributedString *)attrString withScaleFactor:(CGFloat)scaleFactor +{ + if (scaleFactor == 1.0) return; + + [attrString beginEditing]; + + // scale all the attributes that will change the bounding box + [attrString enumerateAttributesInRange:NSMakeRange(0, attrString.length) options:0 usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { + if (attrs[NSFontAttributeName] != nil) { + UIFont *font = attrs[NSFontAttributeName]; + font = [font fontWithSize:std::round(font.pointSize * scaleFactor)]; + [attrString removeAttribute:NSFontAttributeName range:range]; + [attrString addAttribute:NSFontAttributeName value:font range:range]; + } + + if (attrs[NSKernAttributeName] != nil) { + NSNumber *kerning = attrs[NSKernAttributeName]; + [attrString removeAttribute:NSKernAttributeName range:range]; + [attrString addAttribute:NSKernAttributeName value:@([kerning floatValue] * scaleFactor) range:range]; + } + + if (attrs[NSParagraphStyleAttributeName] != nil) { + NSMutableParagraphStyle *paragraphStyle = [attrs[NSParagraphStyleAttributeName] mutableCopy]; + paragraphStyle.lineSpacing = (paragraphStyle.lineSpacing * scaleFactor); + paragraphStyle.paragraphSpacing = (paragraphStyle.paragraphSpacing * scaleFactor); + paragraphStyle.firstLineHeadIndent = (paragraphStyle.firstLineHeadIndent * scaleFactor); + paragraphStyle.headIndent = (paragraphStyle.headIndent * scaleFactor); + paragraphStyle.tailIndent = (paragraphStyle.tailIndent * scaleFactor); + paragraphStyle.minimumLineHeight = (paragraphStyle.minimumLineHeight * scaleFactor); + paragraphStyle.maximumLineHeight = (paragraphStyle.maximumLineHeight * scaleFactor); + + [attrString removeAttribute:NSParagraphStyleAttributeName range:range]; + [attrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + } + + }]; + + [attrString endEditing]; +} + +- (NSUInteger)lineCountForString:(NSAttributedString *)attributedString +{ + NSUInteger lineCount = 0; + + NSLayoutManager *sizingLayoutManager = [self sizingLayoutManager]; + NSTextContainer *sizingTextContainer = [self sizingTextContainer]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; + [textStorage addLayoutManager:sizingLayoutManager]; + + [sizingLayoutManager ensureLayoutForTextContainer:sizingTextContainer]; + for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [sizingLayoutManager numberOfGlyphs] && lineCount <= _attributes.maximumNumberOfLines; lineCount++) { + [sizingLayoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange]; + } + + [textStorage removeLayoutManager:sizingLayoutManager]; + return lineCount; +} + +- (CGSize)boundingBoxForString:(NSAttributedString *)attributedString +{ + NSLayoutManager *sizingLayoutManager = [self sizingLayoutManager]; + NSTextContainer *sizingTextContainer = [self sizingTextContainer]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; + [textStorage addLayoutManager:sizingLayoutManager]; + + [sizingLayoutManager ensureLayoutForTextContainer:sizingTextContainer]; + CGRect textRect = [sizingLayoutManager boundingRectForGlyphRange:NSMakeRange(0, [textStorage length]) + inTextContainer:sizingTextContainer]; + [textStorage removeLayoutManager:sizingLayoutManager]; + return textRect.size; +} + +- (NSLayoutManager *)sizingLayoutManager +{ + AS::MutexLocker l(__instanceLock__); + if (_sizingLayoutManager == nil) { + _sizingLayoutManager = [[ASLayoutManager alloc] init]; + _sizingLayoutManager.usesFontLeading = NO; + + if (_sizingTextContainer == nil) { + // make this text container unbounded in height so that the layout manager will compute the total + // number of lines and not stop counting when height runs out. + _sizingTextContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(_constrainedSize.width, CGFLOAT_MAX)]; + _sizingTextContainer.lineFragmentPadding = 0; + + // use 0 regardless of what is in the attributes so that we get an accurate line count + _sizingTextContainer.maximumNumberOfLines = 0; + + _sizingTextContainer.lineBreakMode = _attributes.lineBreakMode; + _sizingTextContainer.exclusionPaths = _attributes.exclusionPaths; + } + [_sizingLayoutManager addTextContainer:_sizingTextContainer]; + } + + return _sizingLayoutManager; +} + +- (CGFloat)scaleFactor +{ + if (_measured) { + return _scaleFactor; + } + + if ([_attributes.pointSizeScaleFactors count] == 0 || isinf(_constrainedSize.width)) { + _measured = YES; + _scaleFactor = 1.0; + return _scaleFactor; + } + + __block CGFloat adjustedScale = 1.0; + + // We add the scale factor of 1 to our scaleFactors array so that in the first iteration of the loop below, we are + // actually determining if we need to scale at all. If something doesn't fit, we will continue to iterate our scale factors. + NSArray *scaleFactors = [@[@(1)] arrayByAddingObjectsFromArray:_attributes.pointSizeScaleFactors]; + + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + + // Check for two different situations (and correct for both) + // 1. The longest word in the string fits without being wrapped + // 2. The entire text fits in the given constrained size. + + NSString *str = textStorage.string; + NSArray *words = [str componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *longestWordNeedingResize = @""; + for (NSString *word in words) { + if ([word length] > [longestWordNeedingResize length]) { + longestWordNeedingResize = word; + } + } + + // check to see if we may need to shrink for any of these things + BOOL longestWordFits = [longestWordNeedingResize length] ? NO : YES; + BOOL maxLinesFits = self->_attributes.maximumNumberOfLines > 0 ? NO : YES; + BOOL heightFits = isinf(self->_constrainedSize.height) ? YES : NO; + + CGSize longestWordSize = CGSizeZero; + if (longestWordFits == NO) { + NSRange longestWordRange = [str rangeOfString:longestWordNeedingResize]; + NSAttributedString *attrString = [textStorage attributedSubstringFromRange:longestWordRange]; + longestWordSize = [attrString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; + } + + // we may need to shrink for some reason, so let's iterate through our scale factors to see if we actually need to shrink + // Note: the first scale factor in the array is 1.0 so will make sure that things don't fit without shrinking + for (NSNumber *adjustedScaleObj in scaleFactors) { + if (longestWordFits && maxLinesFits && heightFits) { + break; + } + + adjustedScale = [adjustedScaleObj floatValue]; + + if (longestWordFits == NO) { + // we need to check the longest word to make sure it fits + longestWordFits = std::ceil((longestWordSize.width * adjustedScale) <= self->_constrainedSize.width); + } + + // if the longest word fits, go ahead and check max line and height. If it didn't fit continue to the next scale factor + if (longestWordFits == YES) { + + // scale our string by the current scale factor + NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage]; + [[self class] adjustFontSizeForAttributeString:scaledString withScaleFactor:adjustedScale]; + + // check to see if this scaled string fit in the max lines + if (maxLinesFits == NO) { + maxLinesFits = ([self lineCountForString:scaledString] <= self->_attributes.maximumNumberOfLines); + } + + // if max lines still doesn't fit, continue without checking that we fit in the constrained height + if (maxLinesFits == YES && heightFits == NO) { + // max lines fit so make sure that we fit in the constrained height. + CGSize stringSize = [self boundingBoxForString:scaledString]; + heightFits = (stringSize.height <= self->_constrainedSize.height); + } + } + } + + }]; + _measured = YES; + _scaleFactor = adjustedScale; + return _scaleFactor; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+Positioning.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+Positioning.h new file mode 100644 index 0000000000..c887282568 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+Positioning.h @@ -0,0 +1,106 @@ +// +// ASTextKitRenderer+Positioning.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +typedef void (^as_text_component_index_block_t)(NSUInteger characterIndex, + CGRect glyphBoundingRect, + BOOL *stop); + +/** + Measure options are used to specify which type of line height measurement to use. + + ASTextNodeRendererMeasureOptionLineHeight is faster and will give the height from the baseline to the next line. + + ASTextNodeRendererMeasureOptionCapHeight is a more nuanced measure of the glyphs in the given range that attempts to + produce a visually balanced rectangle above and below the glyphs to produce nice looking text highlights. + + ASTextNodeRendererMeasureOptionBlock uses the cap height option to generate each glyph index, but combines all but the + first and last line rect into a single block. Looks nice for multiline selection. + */ +typedef NS_ENUM(NSUInteger, ASTextKitRendererMeasureOption) { + ASTextKitRendererMeasureOptionLineHeight, + ASTextKitRendererMeasureOptionCapHeight, + ASTextKitRendererMeasureOptionBlock +}; + +@interface ASTextKitRenderer (Positioning) + +/** + Returns the bounding rect for the given character range. + + @param textRange The character range for which the bounding rect will be computed. Should be within the range of the + attributedString of this renderer. + + @discussion In the external, shadowed coordinate space. + */ +- (CGRect)frameForTextRange:(NSRange)textRange; + +/** + Returns an array of rects representing the lines in the given character range + + @param textRange The character range for which the rects will be computed. Should be within the range of the + attributedString of this renderer. + @param measureOption The measure option to use for construction of the rects. See ASTextKitRendererMeasureOption + docs for usage. + + @discussion This method is useful for providing highlighting text. Returned rects are in the coordinate space of the + renderer. + + Triggers initialization of textkit components, truncation, and sizing. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange + measureOption:(ASTextKitRendererMeasureOption)measureOption; + +/** + Enumerate the text character indexes at a position within the coordinate space of the renderer. + + @param position The point in the shadowed coordinate space at which text indexes will be enumerated. + @param block The block that will be executed for each index identified that may correspond to the given position. The + block is given the character index that corresponds to the glyph at each index in question, as well as the bounding + rect for that glyph. + + @discussion Glyph location based on a touch point is not an exact science because user touches are not well-represented + by a simple point, especially in the context of link-heavy text. So we have this method to make it a bit easier. This + method checks a grid of candidate positions around the touch point you give it, and computes the bounding rect of the + glyph corresponding to the character index given. + + The bounding rect of the glyph can be used to identify the best glyph index that corresponds to your touch. For + instance, comparing centroidal distance from the glyph bounding rect to the touch center is useful for identifying + which link a user actually intended to select. + + Triggers initialization of textkit components, truncation, and sizing. + */ +- (void)enumerateTextIndexesAtPosition:(CGPoint)position + usingBlock:(as_text_component_index_block_t)block; + +/** + Returns the single text index whose glyph's centroid is closest to the given position. + + @param position The point in the shadowed coordinate space that should be checked. + + @discussion This will use the grid enumeration function above, `enumerateTextIndexesAtPosition...`, in order to find + the closest glyph, so it is possible that a glyph could be missed, but ultimately unlikely. + */ +- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position; + +/** + Returns the trailing rect unused by the renderer in the last rendered line. + + @discussion In the external shadowed coordinate space. + + Triggers initialization of textkit components, truncation, and sizing. + */ +- (CGRect)trailingRect; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+Positioning.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+Positioning.mm new file mode 100644 index 0000000000..9dc770e1d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+Positioning.mm @@ -0,0 +1,386 @@ +// +// ASTextKitRenderer+Positioning.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import +#import + +#import + +#import +#import + +static const CGFloat ASTextKitRendererGlyphTouchHitSlop = 5.0; +static const CGFloat ASTextKitRendererTextCapHeightPadding = 1.3; + +@implementation ASTextKitRenderer (Tracking) + +- (NSArray *)rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption +{ + __block NSArray *textRects = nil; + [self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + textRects = [self unlockedRectsForTextRange:textRange measureOptions:measureOption layoutManager:layoutManager textStorage:textStorage textContainer:textContainer]; + }]; + return textRects; +} + +/** + Helper function that should be called within performBlockWithLockedTextKitComponents: in an already locked state to + prevent a deadlock + */ +- (NSArray *)unlockedRectsForTextRange:(NSRange)textRange measureOptions:(ASTextKitRendererMeasureOption)measureOption layoutManager:(NSLayoutManager *)layoutManager textStorage:(NSTextStorage *)textStorage textContainer:(NSTextContainer *)textContainer +{ + NSRange clampedRange = NSIntersectionRange(textRange, NSMakeRange(0, [textStorage length])); + if (clampedRange.location == NSNotFound || clampedRange.length == 0) { + return @[]; + } + + // Used for block measure option + __block CGRect firstRect = CGRectNull; + __block CGRect lastRect = CGRectNull; + __block CGRect blockRect = CGRectNull; + NSMutableArray *mutableTextRects = [NSMutableArray array]; + + NSString *string = textStorage.string; + + NSRange totalGlyphRange = [layoutManager glyphRangeForCharacterRange:clampedRange actualCharacterRange:NULL]; + + [layoutManager enumerateLineFragmentsForGlyphRange:totalGlyphRange usingBlock:^(CGRect rect, + CGRect usedRect, + NSTextContainer *innerTextContainer, + NSRange glyphRange, + BOOL *stop) { + + CGRect lineRect = CGRectNull; + // If we're empty, don't bother looping through glyphs, use the default. + if (CGRectIsEmpty(usedRect)) { + lineRect = usedRect; + } else { + // TextKit's bounding rect computations are just a touch off, so we actually + // compose the rects by hand from the center of the given TextKit bounds and + // imposing the font attributes returned by the glyph's font. + NSRange lineGlyphRange = NSIntersectionRange(totalGlyphRange, glyphRange); + for (NSUInteger i = lineGlyphRange.location; i < NSMaxRange(lineGlyphRange) && i < string.length; i++) { + // We grab the properly sized rect for the glyph + CGRect properGlyphRect = [self _internalRectForGlyphAtIndex:i + measureOption:measureOption + layoutManager:layoutManager + textContainer:textContainer + textStorage:textStorage]; + + // Don't count empty glyphs towards our line rect. + if (!CGRectIsEmpty(properGlyphRect)) { + lineRect = CGRectIsNull(lineRect) ? properGlyphRect + : CGRectUnion(lineRect, properGlyphRect); + } + } + } + + if (!CGRectIsNull(lineRect)) { + if (measureOption == ASTextKitRendererMeasureOptionBlock) { + // For the block measurement option we store the first & last rect as + // special cases, then merge everything else into a single block rect + if (CGRectIsNull(firstRect)) { + // We don't have a firstRect, so we must be on the first line. + firstRect = lineRect; + } else if(CGRectIsNull(lastRect)) { + // We don't have a lastRect, but we do have a firstRect, so we must + // be on the second line. No need to merge in the blockRect just yet + lastRect = lineRect; + } else if(CGRectIsNull(blockRect)) { + // We have both a first and last rect, so we must be on the third line + // we don't have any blockRect to merge it into, so we just set it + // directly. + blockRect = lastRect; + lastRect = lineRect; + } else { + // Everything is already set, so we just merge this line into the + // block. + blockRect = CGRectUnion(blockRect, lastRect); + lastRect = lineRect; + } + } else { + // If the block option isn't being used then each line is being treated + // individually. + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lineRect]]]; + } + } + }]; + + if (measureOption == ASTextKitRendererMeasureOptionBlock) { + // Block measure option is handled differently with just 3 vars for the entire range. + if (!CGRectIsNull(firstRect)) { + if (!CGRectIsNull(blockRect)) { + CGFloat rightEdge = MAX(CGRectGetMaxX(blockRect), CGRectGetMaxX(lastRect)); + if (rightEdge > CGRectGetMaxX(firstRect)) { + // Force the right side of the first rect to properly align with the + // right side of the rightmost of the block and last rect + firstRect.size.width += rightEdge - CGRectGetMaxX(firstRect); + } + + // Force the left side of the block rect to properly align with the + // left side of the leftmost of the first and last rect + blockRect.origin.x = MIN(CGRectGetMinX(firstRect), CGRectGetMinX(lastRect)); + // Force the right side of the block rect to properly align with the + // right side of the rightmost of the first and last rect + blockRect.size.width += MAX(CGRectGetMaxX(firstRect), CGRectGetMaxX(lastRect)) - CGRectGetMaxX(blockRect); + } + if (!CGRectIsNull(lastRect)) { + // Force the left edge of the last rect to properly align with the + // left side of the leftmost of the first and block rect, if necessary. + CGFloat leftEdge = MIN(CGRectGetMinX(blockRect), CGRectGetMinX(firstRect)); + CGFloat lastRectNudgeAmount = MAX(CGRectGetMinX(lastRect) - leftEdge, 0); + lastRect.origin.x = MIN(leftEdge, CGRectGetMinX(lastRect)); + lastRect.size.width += lastRectNudgeAmount; + } + + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:firstRect]]]; + } + if (!CGRectIsNull(blockRect)) { + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:blockRect]]]; + } + if (!CGRectIsNull(lastRect)) { + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lastRect]]]; + } + } + + return [mutableTextRects copy]; +} + +- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position +{ + // Check in a 9-point region around the actual touch point so we make sure + // we get the best attribute for the touch. + __block CGFloat minimumGlyphDistance = CGFLOAT_MAX; + __block NSUInteger minimumGlyphCharacterIndex = NSNotFound; + + [self enumerateTextIndexesAtPosition:position usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) { + CGPoint glyphLocation = CGPointMake(CGRectGetMidX(glyphBoundingRect), CGRectGetMidY(glyphBoundingRect)); + CGFloat currentDistance = std::sqrt(std::pow(position.x - glyphLocation.x, 2.f) + std::pow(position.y - glyphLocation.y, 2.f)); + if (currentDistance < minimumGlyphDistance) { + minimumGlyphDistance = currentDistance; + minimumGlyphCharacterIndex = characterIndex; + } + }]; + return minimumGlyphCharacterIndex; +} + +/** + Measured from the internal coordinate space of the context, not accounting for shadow offsets. Actually uses CoreText + as an approximation to work around problems in TextKit's glyph sizing. + */ +- (CGRect)_internalRectForGlyphAtIndex:(NSUInteger)glyphIndex + measureOption:(ASTextKitRendererMeasureOption)measureOption + layoutManager:(NSLayoutManager *)layoutManager + textContainer:(NSTextContainer *)textContainer + textStorage:(NSTextStorage *)textStorage +{ + NSUInteger charIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + CGGlyph glyph = [layoutManager glyphAtIndex:glyphIndex]; + CTFontRef font = (__bridge_retained CTFontRef)[textStorage attribute:NSFontAttributeName + atIndex:charIndex + effectiveRange:NULL]; + if (font == nil) { + font = (__bridge_retained CTFontRef)[UIFont systemFontOfSize:12.0]; + } + + // Glyph Advance + // +-------------------------+ + // | | + // | | + // +------------------------+--|-------------------------|--+-----------+-----+ What TextKit returns sometimes + // | | | XXXXXXXXXXX + | | | (approx. correct height, but + // | ---------|--+---------+ XXX XXXX +|-----------|-----| sometimes inaccurate bounding + // | | | XXX XXXXX| | | widths) + // | | | XX XX | | | + // | | | XX | | | + // | | | XXX | | | + // | | | XX | | | + // | | | XXXXXXXXXXX | | | + // | Cap Height->| | XX | | | + // | | | XX | Ascent-->| | + // | | | XX | | | + // | | | XX | | | + // | | | X | | | + // | | | X | | | + // | | | X | | | + // | | | XX | | | + // | | | X | | | + // | ---------|-------+ X +-------------------------------------| + // | | XX | | + // | | X | | + // | | XX Descent------>| | + // | | XXXXXX | | + // | | XXX | | + // +------------------------+-------------------------------------------------+ + // | + // +--+Actual bounding box + + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) + inTextContainer:textContainer]; + + // If it is a NSTextAttachment, we don't have the matched glyph and use width of glyphRect instead of advance. + CGFloat advance = (glyph == kCGFontIndexInvalid) ? glyphRect.size.width : CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, &glyph, NULL, 1); + + // We treat the center of the glyph's bounding box as the center of our new rect + CGPoint glyphCenter = CGPointMake(CGRectGetMidX(glyphRect), CGRectGetMidY(glyphRect)); + + CGRect properGlyphRect; + if (measureOption == ASTextKitRendererMeasureOptionCapHeight + || measureOption == ASTextKitRendererMeasureOptionBlock) { + CGFloat ascent = CTFontGetAscent(font); + CGFloat descent = CTFontGetDescent(font); + CGFloat capHeight = CTFontGetCapHeight(font); + CGFloat leading = CTFontGetLeading(font); + CGFloat glyphHeight = ascent + descent; + + // For visual balance, we add the cap height padding above the cap, and + // below the baseline, we scale by the descent so it grows with the size of + // the text. + CGFloat topPadding = ASTextKitRendererTextCapHeightPadding * descent; + CGFloat bottomPadding = topPadding; + + properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5, + glyphCenter.y - glyphHeight * 0.5 + (ascent - capHeight) - topPadding + leading, + advance, + capHeight + topPadding + bottomPadding); + } else { + // We are just measuring the line heights here, so we can use the + // heights used by TextKit, which tend to be pretty good. + properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5, + glyphRect.origin.y, + advance, + glyphRect.size.height); + } + + CFRelease(font); + + return properGlyphRect; +} + +- (void)enumerateTextIndexesAtPosition:(CGPoint)externalPosition usingBlock:(as_text_component_index_block_t)block +{ + // This method is a little complex because it has to call out to client code from inside an enumeration that needs + // to achieve a lock on the textkit components. It cannot call out to client code from within that lock so we just + // perform the textkit-locked ops inside the locked context. + ASTextKitContext *lockingContext = self.context; + CGPoint internalPosition = [self.shadower offsetPointWithExternalPoint:externalPosition]; + __block BOOL invalidPosition = NO; + [lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + invalidPosition = internalPosition.x > textContainer.size.width + || internalPosition.y > textContainer.size.height + || block == NULL; + }]; + if (invalidPosition) { + // Short circuit if the position is outside the size of this renderer, or if the block is null. + return; + } + + // We break it up into a 44pt box for the touch, and find the closest link attribute-containing glyph to the center of + // the touch. + CGFloat squareSide = 44.f; + // Should be odd if you want to test the center of the touch. + NSInteger pointsOnASide = 3; + + // The distance between any 2 of the adjacent points + CGFloat pointSeparation = squareSide / pointsOnASide; + // These are for tracking which point we're on. We start with -pointsOnASide/2 and go to pointsOnASide/2. So if + // pointsOnASide=3, we go from -1 to 1. + NSInteger endIndex = pointsOnASide / 2; + NSInteger startIndex = -endIndex; + + BOOL stop = NO; + for (NSInteger i = startIndex; i <= endIndex && !stop; i++) { + for (NSInteger j = startIndex; j <= endIndex && !stop; j++) { + CGPoint currentPoint = CGPointMake(internalPosition.x + i * pointSeparation, + internalPosition.y + j * pointSeparation); + + __block NSUInteger characterIndex = NSNotFound; + __block BOOL isValidGlyph = NO; + __block CGRect glyphRect = CGRectNull; + + [lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + // We ask the layout manager for the proper glyph at the touch point + NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:currentPoint + inTextContainer:textContainer]; + + // If it's an invalid glyph, quit. + + [layoutManager glyphAtIndex:glyphIndex isValidIndex:&isValidGlyph]; + if (!isValidGlyph) { + return; + } + + characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + + glyphRect = [self _internalRectForGlyphAtIndex:glyphIndex + measureOption:ASTextKitRendererMeasureOptionLineHeight + layoutManager:layoutManager + textContainer:textContainer + textStorage:textStorage]; + }]; + + // Sometimes TextKit plays jokes on us and returns glyphs that really aren't close to the point in question. + // Silly TextKit... + if (!isValidGlyph || !CGRectContainsPoint(CGRectInset(glyphRect, -ASTextKitRendererGlyphTouchHitSlop, -ASTextKitRendererGlyphTouchHitSlop), currentPoint)) { + continue; + } + + block(characterIndex, [self.shadower offsetRectWithInternalRect:glyphRect], &stop); + } + } +} + +- (CGRect)trailingRect +{ + __block CGRect trailingRect = CGRectNull; + [self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + CGSize calculatedSize = textContainer.size; + // If have an empty string, then our whole bounds constitute trailing space. + if ([textStorage length] == 0) { + trailingRect = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height); + return; + } + + // Take everything after our final character as trailing space. + NSRange textRange = NSMakeRange([textStorage length] - 1, 1); + NSArray *finalRects = [self unlockedRectsForTextRange:textRange measureOptions:ASTextKitRendererMeasureOptionLineHeight layoutManager:layoutManager textStorage:textStorage textContainer:textContainer]; + CGRect finalGlyphRect = [[finalRects lastObject] CGRectValue]; + CGPoint origin = CGPointMake(CGRectGetMaxX(finalGlyphRect), CGRectGetMinY(finalGlyphRect)); + CGSize size = CGSizeMake(calculatedSize.width - origin.x, calculatedSize.height - origin.y); + trailingRect = (CGRect){origin, size}; + }]; + return trailingRect; +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + __block CGRect textRect = CGRectNull; + [self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + // Bail on invalid range. + if (NSMaxRange(textRange) > [textStorage length]) { + ASDisplayNodeCFailAssert(@"Invalid range"); + return; + } + + // Force glyph generation and layout. + [layoutManager ensureLayoutForTextContainer:textContainer]; + + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + textRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; + }]; + return textRect; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+TextChecking.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+TextChecking.h new file mode 100644 index 0000000000..d4ba74fd32 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+TextChecking.h @@ -0,0 +1,32 @@ +// +// ASTextKitRenderer+TextChecking.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +/** + Application extensions to NSTextCheckingType. We're allowed to do this (see NSTextCheckingAllCustomTypes). + */ +static uint64_t const ASTextKitTextCheckingTypeEntity = 1ULL << 33; +static uint64_t const ASTextKitTextCheckingTypeTruncation = 1ULL << 34; + +@class ASTextKitEntityAttribute; + +@interface ASTextKitTextCheckingResult : NSTextCheckingResult +@property (nonatomic, readonly) ASTextKitEntityAttribute *entityAttribute; +@end + +@interface ASTextKitRenderer (TextChecking) + +- (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+TextChecking.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+TextChecking.mm new file mode 100644 index 0000000000..e556393572 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer+TextChecking.mm @@ -0,0 +1,104 @@ +// +// ASTextKitRenderer+TextChecking.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import +#import +#import + +@implementation ASTextKitTextCheckingResult + +{ + // Be explicit about the fact that we are overriding the super class' implementation of -range and -resultType + // and substituting our own custom values. (We could use @synthesize to make these ivars, but our linter correctly + // complains; it's weird to use @synthesize for properties that are redeclared on top of an original declaration in + // the superclass. We only do it here because NSTextCheckingResult doesn't expose an initializer, which is silly.) + NSRange _rangeOverride; + NSTextCheckingType _resultTypeOverride; +} + +- (instancetype)initWithType:(NSTextCheckingType)type + entityAttribute:(ASTextKitEntityAttribute *)entityAttribute + range:(NSRange)range +{ + if ((self = [super init])) { + _resultTypeOverride = type; + _rangeOverride = range; + _entityAttribute = entityAttribute; + } + return self; +} + +- (NSTextCheckingType)resultType +{ + return _resultTypeOverride; +} + +- (NSRange)range +{ + return _rangeOverride; +} + +@end + +@implementation ASTextKitRenderer (TextChecking) + +- (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point +{ + __block NSTextCheckingResult *result = nil; + NSAttributedString *attributedString = self.attributes.attributedString; + NSAttributedString *truncationAttributedString = self.attributes.truncationAttributedString; + + // get the index of the last character, so we can handle text in the truncation token + __block NSRange truncationTokenRange = { NSNotFound, 0 }; + + [truncationAttributedString enumerateAttribute:ASTextKitTruncationAttributeName inRange:NSMakeRange(0, truncationAttributedString.length) + options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + if (value != nil && range.length > 0) { + truncationTokenRange = range; + } + }]; + + if (truncationTokenRange.location == NSNotFound) { + // The truncation string didn't specify a substring which should be highlighted, so we just highlight it all + truncationTokenRange = { 0, truncationAttributedString.length }; + } + + NSRange visibleRange = self.truncater.firstVisibleRange; + truncationTokenRange.location += NSMaxRange(visibleRange); + + __block CGFloat minDistance = CGFLOAT_MAX; + [self enumerateTextIndexesAtPosition:point usingBlock:^(NSUInteger index, CGRect glyphBoundingRect, BOOL *stop){ + if (index >= truncationTokenRange.location) { + result = [[ASTextKitTextCheckingResult alloc] initWithType:ASTextKitTextCheckingTypeTruncation + entityAttribute:nil + range:truncationTokenRange]; + } else { + NSRange range; + NSDictionary *attributes = [attributedString attributesAtIndex:index effectiveRange:&range]; + ASTextKitEntityAttribute *entityAttribute = attributes[ASTextKitEntityAttributeName]; + CGFloat distance = hypot(CGRectGetMidX(glyphBoundingRect) - point.x, CGRectGetMidY(glyphBoundingRect) - point.y); + if (entityAttribute && distance < minDistance) { + result = [[ASTextKitTextCheckingResult alloc] initWithType:ASTextKitTextCheckingTypeEntity + entityAttribute:entityAttribute + range:range]; + minDistance = distance; + } + } + }]; + return result; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer.h new file mode 100644 index 0000000000..0f0cc00e0a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer.h @@ -0,0 +1,108 @@ +// +// ASTextKitRenderer.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +#import + +@class ASTextKitContext; +@class ASTextKitShadower; +@class ASTextKitFontSizeAdjuster; +@protocol ASTextKitTruncating; + +/** + ASTextKitRenderer is a modular object that is responsible for laying out and drawing text. + + A renderer will hold onto the TextKit layouts for the given attributes after initialization. This may constitute a + large amount of memory for large enough applications, so care must be taken when keeping many of these around in-memory + at once. + + This object is designed to be modular and simple. All complex maintenance of state should occur in sub-objects or be + derived via pure functions or categories. No touch-related handling belongs in this class. + + ALL sizing and layout information from this class is in the external coordinate space of the TextKit components. This + is an important distinction because all internal sizing and layout operations are carried out within the shadowed + coordinate space. Padding will be added for you in order to ensure clipping does not occur, and additional information + on this transform is available via the shadower should you need it. + */ +@interface ASTextKitRenderer : NSObject + +/** + Designated Initializer + @discussion Sizing will occur as a result of initialization, so be careful when/where you use this. + */ +- (instancetype)initWithTextKitAttributes:(const ASTextKitAttributes &)textComponentAttributes + constrainedSize:(const CGSize)constrainedSize; + +@property (nonatomic, readonly) ASTextKitContext *context; + +@property (nonatomic, readonly) id truncater; + +@property (nonatomic, readonly) ASTextKitFontSizeAdjuster *fontSizeAdjuster; + +@property (nonatomic, readonly) ASTextKitShadower *shadower; + +@property (nonatomic, readonly) ASTextKitAttributes attributes; + +@property (nonatomic, readonly) CGSize constrainedSize; + +@property (nonatomic, readonly) CGFloat currentScaleFactor; + +#pragma mark - Drawing +/** + Draw the renderer's text content into the bounds provided. + + @param bounds The rect in which to draw the contents of the renderer. + */ +- (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds; + +#pragma mark - Layout + +/** + Returns the computed size of the renderer given the constrained size and other parameters in the initializer. + */ +- (CGSize)size; + +#pragma mark - Text Ranges + +/** + The character range from the original attributedString that is displayed by the renderer given the parameters in the + initializer. + */ +@property (nonatomic, readonly) std::vector visibleRanges; + +/** + The number of lines shown in the string. + */ +- (NSUInteger)lineCount; + +/** + Whether or not the text is truncated. + */ +- (BOOL)isTruncated; + +@end + +@interface ASTextKitRenderer (ASTextKitRendererConvenience) + +/** + Returns the first visible range or an NSRange with location of NSNotFound and size of 0 if no first visible + range exists + */ +@property (nonatomic, readonly) NSRange firstVisibleRange; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer.mm new file mode 100644 index 0000000000..b724d70747 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitRenderer.mm @@ -0,0 +1,295 @@ +// +// ASTextKitRenderer.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +#import +#import +#import +#import +#import +#import + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +static NSCharacterSet *_defaultAvoidTruncationCharacterSet() +{ + static NSCharacterSet *truncationCharacterSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableCharacterSet *mutableCharacterSet = [[NSMutableCharacterSet alloc] init]; + [mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [mutableCharacterSet addCharactersInString:@".,!?:;"]; + truncationCharacterSet = mutableCharacterSet; + }); + return truncationCharacterSet; +} + +@implementation ASTextKitRenderer { + CGSize _calculatedSize; +} +@synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster; + +#pragma mark - Initialization + +- (instancetype)initWithTextKitAttributes:(const ASTextKitAttributes &)attributes + constrainedSize:(const CGSize)constrainedSize +{ + if (self = [super init]) { + _constrainedSize = constrainedSize; + _attributes = attributes; + _currentScaleFactor = 1; + + // As the renderer should be thread safe, create all subcomponents in the initialization method + _shadower = [ASTextKitShadower shadowerWithShadowOffset:attributes.shadowOffset + shadowColor:attributes.shadowColor + shadowOpacity:attributes.shadowOpacity + shadowRadius:attributes.shadowRadius]; + + // We must inset the constrained size by the size of the shadower. + CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; + + _context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString + lineBreakMode:attributes.lineBreakMode + maximumNumberOfLines:attributes.maximumNumberOfLines + exclusionPaths:attributes.exclusionPaths + constrainedSize:shadowConstrainedSize]; + + NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet(); + _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] + truncationAttributedString:attributes.truncationAttributedString + avoidTailTruncationSet:avoidTailTruncationSet]; + + ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. + _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] + constrainedSize:shadowConstrainedSize + textKitAttributes:attributes]; + + // Calcualate size immediately + [self _calculateSize]; + } + return self; +} + +- (NSStringDrawingContext *)stringDrawingContext +{ + // String drawing contexts are not safe to use from more than one thread. + // i.e. if they are created on one thread, it is unsafe to use them on another. + // Therefore we always need to create a new one. + // + // http://web.archive.org/web/20140703122636/https://developer.apple.com/library/ios/documentation/uikit/reference/NSAttributedString_UIKit_Additions/Reference/Reference.html + NSStringDrawingContext *stringDrawingContext = [[NSStringDrawingContext alloc] init]; + + if (isinf(_constrainedSize.width) == NO && _attributes.maximumNumberOfLines > 0) { + ASDisplayNodeAssert(_attributes.maximumNumberOfLines != 1, @"Max line count 1 is not supported in fast-path."); + [stringDrawingContext setValue:@(_attributes.maximumNumberOfLines) forKey:@"maximumNumberOfLines"]; + } + return stringDrawingContext; +} + +#pragma mark - Sizing + +- (CGSize)size +{ + return _calculatedSize; +} + +- (void)_calculateSize +{ + // if we have no scale factors or an unconstrained width, there is no reason to try to adjust the font size + if (isinf(_constrainedSize.width) == NO && [_attributes.pointSizeScaleFactors count] > 0) { + _currentScaleFactor = [[self fontSizeAdjuster] scaleFactor]; + } + + const CGRect constrainedRect = {CGPointZero, _constrainedSize}; + + // If we do not scale, do exclusion, or do custom truncation, we should just use NSAttributedString drawing for a fast-path. + if (self.canUseFastPath) { + CGRect rect = [_attributes.attributedString boundingRectWithSize:_constrainedSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:self.stringDrawingContext]; + // Intersect with constrained rect, in case text kit goes out-of-bounds. + rect = CGRectIntersection(rect, constrainedRect); + _calculatedSize = [self.shadower outsetSizeWithInsetSize:rect.size]; + return; + } + + BOOL isScaled = [self isScaled]; + __block NSTextStorage *scaledTextStorage = nil; + if (isScaled) { + // apply the string scale before truncating or else we may truncate the string after we've done the work to shrink it. + [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage]; + [ASTextKitFontSizeAdjuster adjustFontSizeForAttributeString:scaledString withScaleFactor:self->_currentScaleFactor]; + scaledTextStorage = [[NSTextStorage alloc] initWithAttributedString:scaledString]; + + [textStorage removeLayoutManager:layoutManager]; + [scaledTextStorage addLayoutManager:layoutManager]; + }]; + } + + [[self truncater] truncate]; + + __block CGRect boundingRect; + + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by + // -usedRectForTextContainer:). + [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + [layoutManager ensureLayoutForTextContainer:textContainer]; + boundingRect = [layoutManager usedRectForTextContainer:textContainer]; + if (isScaled) { + // put the non-scaled version back + [scaledTextStorage removeLayoutManager:layoutManager]; + [textStorage addLayoutManager:layoutManager]; + } + }]; + + // TextKit often returns incorrect glyph bounding rects in the horizontal direction, so we clip to our bounding rect + // to make sure our width calculations aren't being offset by glyphs going beyond the constrained rect. + boundingRect = CGRectIntersection(boundingRect, constrainedRect); + _calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; +} + +- (BOOL)isScaled +{ + return (_currentScaleFactor > 0 && _currentScaleFactor < 1.0); +} + +- (BOOL)usesCustomTruncation +{ + // NOTE: This code does not correctly handle if they set `…` with different attributes. + return _attributes.avoidTailTruncationSet != nil || [_attributes.truncationAttributedString.string isEqualToString:@"\u2026"] == NO; +} + +- (BOOL)usesExclusionPaths +{ + return _attributes.exclusionPaths.count > 0; +} + +- (BOOL)canUseFastPath +{ + return NO; +// Fast path is temporarily disabled, because it's crashing in production. +// NOTE: Remember to re-enable testFastPathTruncation when we re-enable this. +// return self.isScaled == NO +// && self.usesCustomTruncation == NO +// && self.usesExclusionPaths == NO +// // NSAttributedString drawing methods ignore usesLineFragmentOrigin if max line count 1, +// // rendering them useless: +// && (_attributes.maximumNumberOfLines != 1 || isinf(_constrainedSize.width)); +} + +#pragma mark - Drawing + +- (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds; +{ + // We add an assertion so we can track the rare conditions where a graphics context is not present + ASDisplayNodeAssertNotNil(context, @"This is no good without a context."); + + bounds = CGRectIntersection(bounds, { .size = _constrainedSize }); + CGRect shadowInsetBounds = [[self shadower] insetRectWithConstrainedRect:bounds]; + + CGContextSaveGState(context); + [[self shadower] setShadowInContext:context]; + UIGraphicsPushContext(context); + + LOG(@"%@, shadowInsetBounds = %@",self, NSStringFromCGRect(shadowInsetBounds)); + + // If we use default options, we can use NSAttributedString for a + // fast path. + if (self.canUseFastPath) { + CGRect drawingBounds = shadowInsetBounds; + [_attributes.attributedString drawWithRect:drawingBounds options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:self.stringDrawingContext]; + } else { + BOOL isScaled = [self isScaled]; + [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + + NSTextStorage *scaledTextStorage = nil; + + if (isScaled) { + // if we are going to scale the text, swap out the non-scaled text for the scaled version. + NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage]; + [ASTextKitFontSizeAdjuster adjustFontSizeForAttributeString:scaledString withScaleFactor:self->_currentScaleFactor]; + scaledTextStorage = [[NSTextStorage alloc] initWithAttributedString:scaledString]; + + [textStorage removeLayoutManager:layoutManager]; + [scaledTextStorage addLayoutManager:layoutManager]; + } + + LOG(@"usedRect: %@", NSStringFromCGRect([layoutManager usedRectForTextContainer:textContainer])); + + NSRange glyphRange = [layoutManager glyphRangeForBoundingRect:(CGRect){ .size = textContainer.size } inTextContainer:textContainer]; + LOG(@"boundingRect: %@", NSStringFromCGRect([layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer])); + + [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; + [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; + + if (isScaled) { + // put the non-scaled version back + [scaledTextStorage removeLayoutManager:layoutManager]; + [textStorage addLayoutManager:layoutManager]; + } + }]; + } + + UIGraphicsPopContext(); + CGContextRestoreGState(context); +} + +#pragma mark - String Ranges + +- (NSUInteger)lineCount +{ + __block NSUInteger lineCount = 0; + [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [layoutManager numberOfGlyphs]; lineCount++) { + [layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange]; + } + }]; + return lineCount; +} + +- (BOOL)isTruncated +{ + if (self.canUseFastPath) { + CGRect boundedRect = [_attributes.attributedString boundingRectWithSize:CGSizeMake(_constrainedSize.width, CGFLOAT_MAX) + options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine + context:nil]; + return boundedRect.size.height > _constrainedSize.height; + } else { + return self.firstVisibleRange.length < _attributes.attributedString.length; + } +} + +- (std::vector)visibleRanges +{ + return _truncater.visibleRanges; +} + +@end + +@implementation ASTextKitRenderer (ASTextKitRendererConvenience) + +- (NSRange)firstVisibleRange +{ + std::vector visibleRanges = self.visibleRanges; + if (visibleRanges.size() > 0) { + return visibleRanges[0]; + } + + return NSMakeRange(0, 0); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitShadower.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitShadower.h new file mode 100644 index 0000000000..2f9c604fbc --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitShadower.h @@ -0,0 +1,78 @@ +// +// ASTextKitShadower.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +/** + * @abstract an immutable class for calculating shadow padding drawing a shadowed background for text + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitShadower : NSObject + ++ (ASTextKitShadower *)shadowerWithShadowOffset:(CGSize)shadowOffset + shadowColor:(UIColor *)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius; + +/** + * @abstract The offset from the top-left corner at which the shadow starts. + * @discussion A positive width will move the shadow to the right. + * A positive height will move the shadow downwards. + */ +@property (nonatomic, readonly) CGSize shadowOffset; + +//! CGColor in which the shadow is drawn +@property (nonatomic, copy, readonly) UIColor *shadowColor; + +//! Alpha of the shadow +@property (nonatomic, readonly) CGFloat shadowOpacity; + +//! Radius, in pixels +@property (nonatomic, readonly) CGFloat shadowRadius; + +/** + * @abstract The edge insets which represent shadow padding + * @discussion Each edge inset is less than or equal to zero. + * + * Example: + * CGRect boundsWithoutShadowPadding; // Large enough to fit text, not large enough to fit the shadow as well + * UIEdgeInsets shadowPadding = [shadower shadowPadding]; + * CGRect boundsWithShadowPadding = UIEdgeInsetsRect(boundsWithoutShadowPadding, shadowPadding); + */ +- (UIEdgeInsets)shadowPadding; + +- (CGSize)insetSizeWithConstrainedSize:(CGSize)constrainedSize; + +- (CGRect)insetRectWithConstrainedRect:(CGRect)constrainedRect; + +- (CGSize)outsetSizeWithInsetSize:(CGSize)insetSize; + +- (CGRect)outsetRectWithInsetRect:(CGRect)insetRect; + +- (CGRect)offsetRectWithInternalRect:(CGRect)internalRect; + +- (CGPoint)offsetPointWithInternalPoint:(CGPoint)internalPoint; + +- (CGPoint)offsetPointWithExternalPoint:(CGPoint)externalPoint; + +/** + * @abstract draws the shadow for text in the provided CGContext + * @discussion Call within the text node's +drawRect method + */ +- (void)setShadowInContext:(CGContextRef)context; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitShadower.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitShadower.mm new file mode 100644 index 0000000000..a2f37f7e06 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitShadower.mm @@ -0,0 +1,177 @@ +// +// ASTextKitShadower.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +static inline CGSize _insetSize(CGSize size, UIEdgeInsets insets) +{ + size.width -= (insets.left + insets.right); + size.height -= (insets.top + insets.bottom); + return size; +} + +static inline UIEdgeInsets _invertInsets(UIEdgeInsets insets) +{ + return { + .top = -insets.top, + .left = -insets.left, + .bottom = -insets.bottom, + .right = -insets.right + }; +} + +@implementation ASTextKitShadower { + UIEdgeInsets _calculatedShadowPadding; +} + ++ (ASTextKitShadower *)shadowerWithShadowOffset:(CGSize)shadowOffset + shadowColor:(UIColor *)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius +{ + /** + * For all cases where no shadow is drawn, we share this singleton shadower to save resources. + */ + static dispatch_once_t onceToken; + static ASTextKitShadower *sharedNonShadower; + dispatch_once(&onceToken, ^{ + sharedNonShadower = [[ASTextKitShadower alloc] initWithShadowOffset:CGSizeZero shadowColor:nil shadowOpacity:0 shadowRadius:0]; + }); + + BOOL hasShadow = shadowOpacity > 0 && (shadowRadius > 0 || CGSizeEqualToSize(shadowOffset, CGSizeZero) == NO) && CGColorGetAlpha(shadowColor.CGColor) > 0; + if (hasShadow == NO) { + return sharedNonShadower; + } else { + return [[ASTextKitShadower alloc] initWithShadowOffset:shadowOffset shadowColor:shadowColor shadowOpacity:shadowOpacity shadowRadius:shadowRadius]; + } +} + +- (instancetype)initWithShadowOffset:(CGSize)shadowOffset + shadowColor:(UIColor *)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius +{ + if (self = [super init]) { + _shadowOffset = shadowOffset; + _shadowColor = shadowColor; + _shadowOpacity = shadowOpacity; + _shadowRadius = shadowRadius; + _calculatedShadowPadding = UIEdgeInsetsMake(-INFINITY, -INFINITY, INFINITY, INFINITY); + } + return self; +} + +/* + * This method is duplicated here because it gets called frequently, and we were + * wasting valuable time constructing a state object to ask it. + */ +- (BOOL)_shouldDrawShadow +{ + return _shadowOpacity != 0.0 && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)) && CGColorGetAlpha(_shadowColor.CGColor) > 0; +} + +- (void)setShadowInContext:(CGContextRef)context +{ + if ([self _shouldDrawShadow]) { + CGColorRef textShadowColor = CGColorRetain(_shadowColor.CGColor); + CGSize textShadowOffset = _shadowOffset; + CGFloat textShadowOpacity = _shadowOpacity; + CGFloat textShadowRadius = _shadowRadius; + + if (textShadowOpacity != 1.0) { + CGFloat inherentAlpha = CGColorGetAlpha(textShadowColor); + + CGColorRef oldTextShadowColor = textShadowColor; + textShadowColor = CGColorCreateCopyWithAlpha(textShadowColor, inherentAlpha * textShadowOpacity); + CGColorRelease(oldTextShadowColor); + } + + CGContextSetShadowWithColor(context, textShadowOffset, textShadowRadius, textShadowColor); + + CGColorRelease(textShadowColor); + } +} + + +- (UIEdgeInsets)shadowPadding +{ + if (_calculatedShadowPadding.top == -INFINITY) { + if (![self _shouldDrawShadow]) { + return UIEdgeInsetsZero; + } + + UIEdgeInsets shadowPadding = UIEdgeInsetsZero; + + // min values are expected to be negative for most typical shadowOffset and + // blurRadius settings: + shadowPadding.top = std::fmin(0.0f, _shadowOffset.height - _shadowRadius); + shadowPadding.left = std::fmin(0.0f, _shadowOffset.width - _shadowRadius); + + shadowPadding.bottom = std::fmin(0.0f, -_shadowOffset.height - _shadowRadius); + shadowPadding.right = std::fmin(0.0f, -_shadowOffset.width - _shadowRadius); + + _calculatedShadowPadding = shadowPadding; + } + + return _calculatedShadowPadding; +} + +- (CGSize)insetSizeWithConstrainedSize:(CGSize)constrainedSize +{ + return _insetSize(constrainedSize, _invertInsets([self shadowPadding])); +} + +- (CGRect)insetRectWithConstrainedRect:(CGRect)constrainedRect +{ + return UIEdgeInsetsInsetRect(constrainedRect, _invertInsets([self shadowPadding])); +} + +- (CGSize)outsetSizeWithInsetSize:(CGSize)insetSize +{ + return _insetSize(insetSize, [self shadowPadding]); +} + +- (CGRect)outsetRectWithInsetRect:(CGRect)insetRect +{ + return UIEdgeInsetsInsetRect(insetRect, [self shadowPadding]); +} + +- (CGRect)offsetRectWithInternalRect:(CGRect)internalRect +{ + return (CGRect){ + .origin = [self offsetPointWithInternalPoint:internalRect.origin], + .size = internalRect.size + }; +} + +- (CGPoint)offsetPointWithInternalPoint:(CGPoint)internalPoint +{ + UIEdgeInsets shadowPadding = [self shadowPadding]; + return (CGPoint){ + internalPoint.x + shadowPadding.left, + internalPoint.y + shadowPadding.top + }; +} + +- (CGPoint)offsetPointWithExternalPoint:(CGPoint)externalPoint +{ + UIEdgeInsets shadowPadding = [self shadowPadding]; + return (CGPoint){ + externalPoint.x - shadowPadding.left, + externalPoint.y - shadowPadding.top + }; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTailTruncater.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTailTruncater.h new file mode 100644 index 0000000000..70734d9e2c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTailTruncater.h @@ -0,0 +1,21 @@ +// +// ASTextKitTailTruncater.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitTailTruncater : NSObject + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTailTruncater.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTailTruncater.mm new file mode 100644 index 0000000000..b81e27be05 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTailTruncater.mm @@ -0,0 +1,196 @@ +// +// ASTextKitTailTruncater.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +@implementation ASTextKitTailTruncater +{ + __weak ASTextKitContext *_context; + NSAttributedString *_truncationAttributedString; + NSCharacterSet *_avoidTailTruncationSet; +} +@synthesize visibleRanges = _visibleRanges; + +- (instancetype)initWithContext:(ASTextKitContext *)context + truncationAttributedString:(NSAttributedString *)truncationAttributedString + avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet +{ + if (self = [super init]) { + _context = context; + _truncationAttributedString = truncationAttributedString; + _avoidTailTruncationSet = avoidTailTruncationSet; + } + return self; +} + +/** + Calculates the intersection of the truncation message within the end of the last line. + */ +- (NSUInteger)_calculateCharacterIndexBeforeTruncationMessage:(NSLayoutManager *)layoutManager + textStorage:(NSTextStorage *)textStorage + textContainer:(NSTextContainer *)textContainer +{ + CGRect constrainedRect = (CGRect){ .size = textContainer.size }; + + NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:constrainedRect + inTextContainer:textContainer]; + NSInteger lastVisibleGlyphIndex = (NSMaxRange(visibleGlyphRange) - 1); + + if (lastVisibleGlyphIndex < 0) { + return NSNotFound; + } + + CGRect lastLineRect = [layoutManager lineFragmentRectForGlyphAtIndex:lastVisibleGlyphIndex + effectiveRange:NULL]; + CGRect lastLineUsedRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:lastVisibleGlyphIndex + effectiveRange:NULL]; + NSParagraphStyle *paragraphStyle = [textStorage attributesAtIndex:[layoutManager characterIndexForGlyphAtIndex:lastVisibleGlyphIndex] + effectiveRange:NULL][NSParagraphStyleAttributeName]; + + // We assume LTR so long as the writing direction is not + BOOL rtlWritingDirection = paragraphStyle ? paragraphStyle.baseWritingDirection == NSWritingDirectionRightToLeft : NO; + // We only want to treat the truncation rect as left-aligned in the case that we are right-aligned and our writing + // direction is RTL. + BOOL leftAligned = CGRectGetMinX(lastLineRect) == CGRectGetMinX(lastLineUsedRect) || !rtlWritingDirection; + + // Calculate the bounding rectangle for the truncation message + ASTextKitContext *truncationContext = [[ASTextKitContext alloc] initWithAttributedString:_truncationAttributedString + lineBreakMode:NSLineBreakByWordWrapping + maximumNumberOfLines:1 + exclusionPaths:nil + constrainedSize:constrainedRect.size]; + __block CGRect truncationUsedRect; + + [truncationContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *truncationLayoutManager, NSTextStorage *truncationTextStorage, NSTextContainer *truncationTextContainer) { + // Size the truncation message + [truncationLayoutManager ensureLayoutForTextContainer:truncationTextContainer]; + NSRange truncationGlyphRange = [truncationLayoutManager glyphRangeForTextContainer:truncationTextContainer]; + truncationUsedRect = [truncationLayoutManager boundingRectForGlyphRange:truncationGlyphRange + inTextContainer:truncationTextContainer]; + }]; + CGFloat truncationOriginX = (leftAligned ? + CGRectGetMaxX(constrainedRect) - truncationUsedRect.size.width : + CGRectGetMinX(constrainedRect)); + CGRect translatedTruncationRect = CGRectMake(truncationOriginX, + CGRectGetMinY(lastLineRect), + truncationUsedRect.size.width, + truncationUsedRect.size.height); + + // Determine which glyph is the first to be clipped / overlaps the truncation message. + CGFloat truncationMessageX = (leftAligned ? + CGRectGetMinX(translatedTruncationRect) : + CGRectGetMaxX(translatedTruncationRect)); + CGPoint beginningOfTruncationMessage = CGPointMake(truncationMessageX, + CGRectGetMidY(translatedTruncationRect)); + NSUInteger firstClippedGlyphIndex = [layoutManager glyphIndexForPoint:beginningOfTruncationMessage + inTextContainer:textContainer + fractionOfDistanceThroughGlyph:NULL]; + // If it didn't intersect with any text then it should just return the last visible character index, since the + // truncation rect can fully fit on the line without clipping any other text. + if (firstClippedGlyphIndex == NSNotFound) { + return [layoutManager characterIndexForGlyphAtIndex:lastVisibleGlyphIndex]; + } + NSUInteger firstCharacterIndexToReplace = [layoutManager characterIndexForGlyphAtIndex:firstClippedGlyphIndex]; + + // Break on word boundaries + return [self _findTruncationInsertionPointAtOrBeforeCharacterIndex:firstCharacterIndexToReplace + layoutManager:layoutManager + textStorage:textStorage]; +} + +/** + Finds the first whitespace at or before the character index do we don't truncate in the middle of words + If there are multiple whitespaces together (say a space and a newline), this will backtrack to the first one + */ +- (NSUInteger)_findTruncationInsertionPointAtOrBeforeCharacterIndex:(NSUInteger)firstCharacterIndexToReplace + layoutManager:(NSLayoutManager *)layoutManager + textStorage:(NSTextStorage *)textStorage +{ + // Don't attempt to truncate beyond the end of the string + if (firstCharacterIndexToReplace >= textStorage.length) { + return 0; + } + + // Find the glyph range of the line fragment containing the first character to replace. + NSRange lineGlyphRange; + [layoutManager lineFragmentRectForGlyphAtIndex:[layoutManager glyphIndexForCharacterAtIndex:firstCharacterIndexToReplace] + effectiveRange:&lineGlyphRange]; + + // Look for the first whitespace from the end of the line, starting from the truncation point + NSUInteger startingSearchIndex = [layoutManager characterIndexForGlyphAtIndex:lineGlyphRange.location]; + NSUInteger endingSearchIndex = firstCharacterIndexToReplace; + NSRange rangeToSearch = NSMakeRange(startingSearchIndex, (endingSearchIndex - startingSearchIndex)); + + NSRange rangeOfLastVisibleAvoidedChars = { .location = NSNotFound }; + if (_avoidTailTruncationSet) { + rangeOfLastVisibleAvoidedChars = [textStorage.string rangeOfCharacterFromSet:_avoidTailTruncationSet + options:NSBackwardsSearch + range:rangeToSearch]; + } + + // Couldn't find a good place to truncate. Might be because there is no whitespace in the text, or we're dealing + // with a foreign language encoding. Settle for truncating at the original place, which may be mid-word. + if (rangeOfLastVisibleAvoidedChars.location == NSNotFound) { + return firstCharacterIndexToReplace; + } else { + return rangeOfLastVisibleAvoidedChars.location; + } +} + +- (void)truncate +{ + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + NSUInteger originalStringLength = textStorage.length; + + [layoutManager ensureLayoutForTextContainer:textContainer]; + + NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:{ .size = textContainer.size } + inTextContainer:textContainer]; + NSRange visibleCharacterRange = [layoutManager characterRangeForGlyphRange:visibleGlyphRange + actualGlyphRange:NULL]; + + // Check if text is truncated, and if so apply our truncation string + if (visibleCharacterRange.length < originalStringLength && self->_truncationAttributedString.length > 0) { + NSInteger firstCharacterIndexToReplace = [self _calculateCharacterIndexBeforeTruncationMessage:layoutManager + textStorage:textStorage + textContainer:textContainer]; + if (firstCharacterIndexToReplace == 0 || firstCharacterIndexToReplace == NSNotFound) { + return; + } + + // Update/truncate the visible range of text + visibleCharacterRange = NSMakeRange(0, firstCharacterIndexToReplace); + NSRange truncationReplacementRange = NSMakeRange(firstCharacterIndexToReplace, + textStorage.length - firstCharacterIndexToReplace); + // Replace the end of the visible message with the truncation string + [textStorage replaceCharactersInRange:truncationReplacementRange + withAttributedString:self->_truncationAttributedString]; + } + + self->_visibleRanges = { visibleCharacterRange }; + }]; +} + +- (NSRange)firstVisibleRange +{ + std::vector visibleRanges = _visibleRanges; + if (visibleRanges.size() > 0) { + return visibleRanges[0]; + } + + return NSMakeRange(NSNotFound, 0); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTruncating.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTruncating.h new file mode 100644 index 0000000000..35392fd6b3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitTruncating.h @@ -0,0 +1,61 @@ +// +// ASTextKitTruncating.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASTextKitContext; + +@protocol ASTextKitTruncating + +/** + The character range from the original attributedString that is displayed by the renderer given the parameters in the + initializer. + */ +@property (nonatomic, readonly) std::vector visibleRanges; + +/** + Returns the first visible range or an NSRange with location of NSNotFound and size of 0 if no first visible + range exists + */ +@property (nonatomic, readonly) NSRange firstVisibleRange; + +/** + A truncater object is initialized with the full state of the text. It is a Single Responsibility Object that is + mutative. It configures the state of the TextKit components (layout manager, text container, text storage) to achieve + the intended truncation, then it stores the resulting state for later fetching. + + The truncater may mutate the state of the text storage such that only the drawn string is actually present in the + text storage itself. + + The truncater should not store a strong reference to the context to prevent retain cycles. + */ +- (instancetype)initWithContext:(ASTextKitContext *)context + truncationAttributedString:(NSAttributedString * _Nullable)truncationAttributedString + avoidTailTruncationSet:(NSCharacterSet * _Nullable)avoidTailTruncationSet; + +/** + Actually do the truncation. + */ +- (void)truncate; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeTypes.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeTypes.h new file mode 100644 index 0000000000..694d9ca7fa --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeTypes.h @@ -0,0 +1,13 @@ +// +// ASTextNodeTypes.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +// Use this attribute name to add "word kerning" +static NSString *const ASTextNodeWordKerningAttributeName = @"ASAttributedStringWordKerning"; diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeWordKerner.h b/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeWordKerner.h new file mode 100644 index 0000000000..795c4ea099 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeWordKerner.h @@ -0,0 +1,37 @@ +// +// ASTextNodeWordKerner.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract This class acts as the NSLayoutManagerDelegate for ASTextNode. + @discussion Its current job is word kerning, i.e. adjusting the width of spaces to match the set + wordKernedSpaceWidth. If word kerning is not needed, set the layoutManager's delegate to nil. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTextNodeWordKerner : NSObject + +/** + The following @optional NSLayoutManagerDelegate methods are implemented: + +- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange NS_AVAILABLE_IOS(7_0); + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0); + +- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0); + */ + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeWordKerner.mm b/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeWordKerner.mm new file mode 100644 index 0000000000..67e640557c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/TextKit/ASTextNodeWordKerner.mm @@ -0,0 +1,130 @@ +// +// ASTextNodeWordKerner.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +@implementation ASTextNodeWordKerner + +#pragma mark - NSLayoutManager Delegate +- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange +{ + NSUInteger glyphCount = glyphRange.length; + NSGlyphProperty *newGlyphProperties = NULL; + + BOOL usesWordKerning = NO; + + // If our typing attributes specify word kerning, specify the spaces as whitespace control characters so we can customize their width. + // Are any of the characters spaces? + NSString *textStorageString = layoutManager.textStorage.string; + for (NSUInteger arrayIndex = 0; arrayIndex < glyphCount; arrayIndex++) { + NSUInteger characterIndex = characterIndexes[arrayIndex]; + if ([textStorageString characterAtIndex:characterIndex] != ' ') + continue; + + // If we've set the whitespace control character for this space already, we have nothing to do. + if (properties[arrayIndex] == NSGlyphPropertyControlCharacter) { + usesWordKerning = YES; + continue; + } + + // Create new glyph properties, if necessary. + if (!newGlyphProperties) { + newGlyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount); + memcpy(newGlyphProperties, properties, (sizeof(NSGlyphProperty) * glyphCount)); + } + + // It's a space. Make it a whitespace control character. + newGlyphProperties[arrayIndex] = NSGlyphPropertyControlCharacter; + } + + // If we don't have any custom glyph properties, return 0 to indicate to the layout manager that it should use the standard glyphs+properties. + if (!newGlyphProperties) { + if (usesWordKerning) { + // If the text does use word kerning we have to make sure we return the correct glyphCount, or the layout manager will just use the default properties and ignore our kerning. + [layoutManager setGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange]; + return glyphCount; + } else { + return 0; + } + } + + // Otherwise, use our custom glyph properties. + [layoutManager setGlyphs:glyphs properties:newGlyphProperties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange]; + free(newGlyphProperties); + + return glyphCount; +} + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex +{ + // If it's a space character and we have custom word kerning, use the whitespace action control character. + if ([layoutManager.textStorage.string characterAtIndex:characterIndex] == ' ') + return NSControlCharacterActionWhitespace; + + return defaultAction; +} + +- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex +{ + CGFloat wordKernedSpaceWidth = [self _wordKernedSpaceWidthForCharacterAtIndex:characterIndex atGlyphPosition:glyphPosition forTextContainer:textContainer layoutManager:layoutManager]; + return CGRectMake(glyphPosition.x, glyphPosition.y, wordKernedSpaceWidth, CGRectGetHeight(proposedRect)); +} + +- (CGFloat)_wordKernedSpaceWidthForCharacterAtIndex:(NSUInteger)characterIndex atGlyphPosition:(CGPoint)glyphPosition forTextContainer:(NSTextContainer *)textContainer layoutManager:(NSLayoutManager *)layoutManager +{ + // We use a map table for pointer equality and non-copying keys. + static NSMapTable *spaceSizes; + // NSMapTable is a defined thread unsafe class, so we need to synchronize + // access in a light manner. So we use dispatch_sync on this queue for all + // access to the map table. + static dispatch_queue_t mapQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + spaceSizes = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:1]; + mapQueue = dispatch_queue_create("org.AsyncDisplayKit.wordKerningQueue", DISPATCH_QUEUE_SERIAL); + }); + CGFloat ordinarySpaceWidth; + UIFont *font = [layoutManager.textStorage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL]; + CGFloat wordKerning = [[layoutManager.textStorage attribute:ASTextNodeWordKerningAttributeName atIndex:characterIndex effectiveRange:NULL] floatValue]; + __block NSNumber *ordinarySpaceSizeValue; + dispatch_sync(mapQueue, ^{ + ordinarySpaceSizeValue = [spaceSizes objectForKey:font]; + }); + if (ordinarySpaceSizeValue == nil) { + ordinarySpaceWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width; + dispatch_async(mapQueue, ^{ + [spaceSizes setObject:@(ordinarySpaceWidth) forKey:font]; + }); + } else { + ordinarySpaceWidth = [ordinarySpaceSizeValue floatValue]; + } + + CGFloat totalKernedWidth = (ordinarySpaceWidth + wordKerning); + + // TextKit normally handles whitespace by increasing the advance of the previous glyph, rather than displaying an + // actual glyph for the whitespace itself. However, in order to implement word kerning, we explicitly require a + // discrete glyph whose bounding box we can specify. The problem is that TextKit does not know this glyph is + // invisible. From TextKit's perspective, this whitespace glyph is a glyph that MUST be displayed. Thus when it + // comes to determining linebreaks, the width of this trailing whitespace glyph is considered. This causes + // our text to wrap sooner than it otherwise would, as room is allocated at the end of each line for a glyph that + // isn't actually visible. To implement our desired behavior, we check to see if the current whitespace glyph + // would break to the next line. If it breaks to the next line, then this constitutes trailing whitespace, and + // we specify enough room to fill up the remainder of the line, but nothing more. + if (glyphPosition.x + totalKernedWidth > textContainer.size.width) { + return (textContainer.size.width - glyphPosition.x); + } + + return totalKernedWidth; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/UIImage+ASConvenience.h b/submodules/AsyncDisplayKit/Source/UIImage+ASConvenience.h new file mode 100644 index 0000000000..b24c97d1c5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/UIImage+ASConvenience.h @@ -0,0 +1,109 @@ +// +// UIImage+ASConvenience.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Dramatically faster version of +[UIImage imageNamed:]. Although it is believed that imageNamed: + * has a cache and is fast, it actually performs expensive asset catalog lookups and is often a + * performance bottleneck (verified on iOS 7 through iOS 10). + * + * Use [UIImage as_imageNamed:] anywhere in your app, even if you aren't using other parts of ASDK. + * Although not the best choice for extremely large assets that are only used once, it is the ideal + * choice for any assets used in tab bars, nav bars, buttons, table or collection cells, etc. + */ + +@interface UIImage (ASDKFastImageNamed) + +/** + * A version of imageNamed that caches results because loading an image is expensive. + * Calling with the same name value will usually return the same object. A UIImage, + * after creation, is immutable and thread-safe so it's fine to share these objects across multiple threads. + * + * @param imageName The name of the image to load + * @return The loaded image or nil + */ ++ (nullable UIImage *)as_imageNamed:(NSString *)imageName NS_RETURNS_RETAINED; + +/** + * A version of imageNamed that caches results because loading an image is expensive. + * Calling with the same name value will usually return the same object. A UIImage, + * after creation, is immutable and thread-safe so it's fine to share these objects across multiple threads. + * + * @param imageName The name of the image to load + * @param traitCollection The traits associated with the intended environment for the image. + * @return The loaded image or nil + */ ++ (nullable UIImage *)as_imageNamed:(NSString *)imageName compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection NS_RETURNS_RETAINED; + +@end + +/** + * High-performance flat-colored, rounded-corner resizable images + * + * For "Baked-in Opaque" corners, set cornerColor equal to the color behind the rounded image object, + * i.e. the background color. + * For "Baked-in Alpha" corners, set cornerColor = [UIColor clearColor] + * + * See http://asyncdisplaykit.org/docs/corner-rounding.html for an explanation. + */ + +@interface UIImage (ASDKResizableRoundedRects) + +/** + * This generates a flat-color, rounded-corner resizeable image + * + * @param cornerRadius The radius of the rounded-corner + * @param cornerColor The fill color of the corners (For Alpha corners use clearColor) + * @param fillColor The fill color of the rounded-corner image + */ ++ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius + cornerColor:(nullable UIColor *)cornerColor + fillColor:(UIColor *)fillColor NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * This generates a flat-color, rounded-corner resizeable image with a border + * + * @param cornerRadius The radius of the rounded-corner + * @param cornerColor The fill color of the corners (For Alpha corners use clearColor) + * @param fillColor The fill color of the rounded-corner image + * @param borderColor The border color. Set to nil for no border. + * @param borderWidth The border width. Dummy value if borderColor = nil. + */ ++ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius + cornerColor:(UIColor *)cornerColor + fillColor:(UIColor *)fillColor + borderColor:(nullable UIColor *)borderColor + borderWidth:(CGFloat)borderWidth NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +/** + * This generates a flat-color, rounded-corner resizeable image with a border + * + * @param cornerRadius The radius of the rounded-corner + * @param cornerColor The fill color of the corners (For Alpha corners use clearColor) + * @param fillColor The fill color of the rounded-corner image + * @param borderColor The border color. Set to nil for no border. + * @param borderWidth The border width. Dummy value if borderColor = nil. + * @param roundedCorners Select individual or multiple corners to round. Set to UIRectCornerAllCorners to round all 4 corners. + * @param scale The number of pixels per point. Provide 0.0 to use the screen scale. + */ ++ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius + cornerColor:(nullable UIColor *)cornerColor + fillColor:(UIColor *)fillColor + borderColor:(nullable UIColor *)borderColor + borderWidth:(CGFloat)borderWidth + roundedCorners:(UIRectCorner)roundedCorners + scale:(CGFloat)scale NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/UIImage+ASConvenience.mm b/submodules/AsyncDisplayKit/Source/UIImage+ASConvenience.mm new file mode 100644 index 0000000000..936514cf14 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/UIImage+ASConvenience.mm @@ -0,0 +1,172 @@ +// +// UIImage+ASConvenience.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#pragma mark - ASDKFastImageNamed + +@implementation UIImage (ASDKFastImageNamed) + +UIImage *cachedImageNamed(NSString *imageName, UITraitCollection *traitCollection) NS_RETURNS_RETAINED +{ + static NSCache *imageCache = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Because NSCache responds to memory warnings, we do not need an explicit limit. + // all of these objects contain compressed image data and are relatively small + // compared to the backing stores of text and image views. + imageCache = [[NSCache alloc] init]; + }); + + UIImage *image = nil; + if ([imageName length] > 0) { + NSString *imageKey = imageName; + if (traitCollection) { + char imageKeyBuffer[256]; + snprintf(imageKeyBuffer, sizeof(imageKeyBuffer), "%s|%ld|%ld", imageName.UTF8String, (long)traitCollection.horizontalSizeClass, (long)traitCollection.verticalSizeClass); + imageKey = [NSString stringWithUTF8String:imageKeyBuffer]; + } + + image = [imageCache objectForKey:imageKey]; + if (!image) { + image = [UIImage imageNamed:imageName inBundle:nil compatibleWithTraitCollection:traitCollection]; + if (image) { + [imageCache setObject:image forKey:imageKey]; + } + } + } + return image; +} + ++ (UIImage *)as_imageNamed:(NSString *)imageName NS_RETURNS_RETAINED +{ + return cachedImageNamed(imageName, nil); +} + ++ (UIImage *)as_imageNamed:(NSString *)imageName compatibleWithTraitCollection:(UITraitCollection *)traitCollection NS_RETURNS_RETAINED +{ + return cachedImageNamed(imageName, traitCollection); +} + +@end + +#pragma mark - ASDKResizableRoundedRects + +@implementation UIImage (ASDKResizableRoundedRects) + ++ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius + cornerColor:(UIColor *)cornerColor + fillColor:(UIColor *)fillColor NS_RETURNS_RETAINED +{ + return [self as_resizableRoundedImageWithCornerRadius:cornerRadius + cornerColor:cornerColor + fillColor:fillColor + borderColor:nil + borderWidth:1.0 + roundedCorners:UIRectCornerAllCorners + scale:0.0]; +} + ++ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius + cornerColor:(UIColor *)cornerColor + fillColor:(UIColor *)fillColor + borderColor:(UIColor *)borderColor + borderWidth:(CGFloat)borderWidth NS_RETURNS_RETAINED +{ + return [self as_resizableRoundedImageWithCornerRadius:cornerRadius + cornerColor:cornerColor + fillColor:fillColor + borderColor:borderColor + borderWidth:borderWidth + roundedCorners:UIRectCornerAllCorners + scale:0.0]; +} + ++ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius + cornerColor:(UIColor *)cornerColor + fillColor:(UIColor *)fillColor + borderColor:(UIColor *)borderColor + borderWidth:(CGFloat)borderWidth + roundedCorners:(UIRectCorner)roundedCorners + scale:(CGFloat)scale NS_RETURNS_RETAINED +{ + static NSCache *__pathCache = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __pathCache = [[NSCache alloc] init]; + // UIBezierPath objects are fairly small and these are equally sized. 20 should be plenty for many different parameters. + __pathCache.countLimit = 20; + }); + + // Treat clear background color as no background color + if ([cornerColor isEqual:[UIColor clearColor]]) { + cornerColor = nil; + } + + CGFloat dimension = (cornerRadius * 2) + 1; + CGRect bounds = CGRectMake(0, 0, dimension, dimension); + + typedef struct { + UIRectCorner corners; + CGFloat radius; + } PathKey; + PathKey key = { roundedCorners, cornerRadius }; + NSValue *pathKeyObject = [[NSValue alloc] initWithBytes:&key objCType:@encode(PathKey)]; + + CGSize cornerRadii = CGSizeMake(cornerRadius, cornerRadius); + UIBezierPath *path = [__pathCache objectForKey:pathKeyObject]; + if (path == nil) { + path = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:roundedCorners cornerRadii:cornerRadii]; + [__pathCache setObject:path forKey:pathKeyObject]; + } + + // We should probably check if the background color has any alpha component but that + // might be expensive due to needing to check mulitple color spaces. + ASGraphicsBeginImageContextWithOptions(bounds.size, cornerColor != nil, scale); + + BOOL contextIsClean = YES; + if (cornerColor) { + contextIsClean = NO; + [cornerColor setFill]; + // Copy "blend" mode is extra fast because it disregards any value currently in the buffer and overrides directly. + UIRectFillUsingBlendMode(bounds, kCGBlendModeCopy); + } + + BOOL canUseCopy = contextIsClean || (CGColorGetAlpha(fillColor.CGColor) == 1); + [fillColor setFill]; + [path fillWithBlendMode:(canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal) alpha:1]; + + if (borderColor) { + [borderColor setStroke]; + + // Inset border fully inside filled path (not halfway on each side of path) + CGRect strokeRect = CGRectInset(bounds, borderWidth / 2.0, borderWidth / 2.0); + + // It is rarer to have a stroke path, and our cache key only handles rounded rects for the exact-stretchable + // size calculated by cornerRadius, so we won't bother caching this path. Profiling validates this decision. + UIBezierPath *strokePath = [UIBezierPath bezierPathWithRoundedRect:strokeRect + byRoundingCorners:roundedCorners + cornerRadii:cornerRadii]; + [strokePath setLineWidth:borderWidth]; + BOOL canUseCopy = (CGColorGetAlpha(borderColor.CGColor) == 1); + [strokePath strokeWithBlendMode:(canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal) alpha:1]; + } + + UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); + + UIEdgeInsets capInsets = UIEdgeInsetsMake(cornerRadius, cornerRadius, cornerRadius, cornerRadius); + result = [result resizableImageWithCapInsets:capInsets resizingMode:UIImageResizingModeStretch]; + + return result; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.h b/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.h new file mode 100644 index 0000000000..17afab3ae2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.h @@ -0,0 +1,25 @@ +// +// UIResponder+AsyncDisplayKit.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIResponder (AsyncDisplayKit) + +/** + * The nearest view controller above this responder, if one exists. + * + * This property must be accessed on the main thread. + */ +@property (nonatomic, nullable, readonly) __kindof UIViewController *asdk_associatedViewController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm b/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm new file mode 100644 index 0000000000..8c3ec24de3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm @@ -0,0 +1,32 @@ +// +// UIResponder+AsyncDisplayKit.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "UIResponder+AsyncDisplayKit.h" + +#import +#import +#import + +@implementation UIResponder (AsyncDisplayKit) + +- (__kindof UIViewController *)asdk_associatedViewController +{ + ASDisplayNodeAssertMainThread(); + + for (UIResponder *responder in [self asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc) { + return vc; + } + } + return nil; +} + +@end + diff --git a/submodules/AsyncDisplayKit/Source/_ASTransitionContext.h b/submodules/AsyncDisplayKit/Source/_ASTransitionContext.h new file mode 100644 index 0000000000..44c4906c39 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASTransitionContext.h @@ -0,0 +1,49 @@ +// +// _ASTransitionContext.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@class ASLayout; +@class _ASTransitionContext; + +@protocol _ASTransitionContextLayoutDelegate + +- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context; + +- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context; +- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context; + +- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key; +- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key; + +@end + +@protocol _ASTransitionContextCompletionDelegate + +- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete; + +@end + +@interface _ASTransitionContext : NSObject + +@property (nonatomic, readonly, getter=isAnimated) BOOL animated; + +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate; + +@end + +@interface _ASAnimatedTransitionContext : NSObject +@property (nonatomic, readonly) ASDisplayNode *node; +@property (nonatomic, readonly) CGFloat alpha; ++ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alphaValue NS_RETURNS_RETAINED; +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm b/submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm new file mode 100644 index 0000000000..40a3573c15 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm @@ -0,0 +1,104 @@ +// +// _ASTransitionContext.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + + +NSString * const ASTransitionContextFromLayoutKey = @"org.asyncdisplaykit.ASTransitionContextFromLayoutKey"; +NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransitionContextToLayoutKey"; + +@interface _ASTransitionContext () + +@property (weak, nonatomic) id<_ASTransitionContextLayoutDelegate> layoutDelegate; +@property (weak, nonatomic) id<_ASTransitionContextCompletionDelegate> completionDelegate; + +@end + +@implementation _ASTransitionContext + +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate +{ + self = [super init]; + if (self) { + _animated = animated; + _layoutDelegate = layoutDelegate; + _completionDelegate = completionDelegate; + } + return self; +} + +#pragma mark - ASContextTransitioning Protocol Implementation + +- (ASLayout *)layoutForKey:(NSString *)key +{ + return [_layoutDelegate transitionContext:self layoutForKey:key]; +} + +- (ASSizeRange)constrainedSizeForKey:(NSString *)key +{ + return [_layoutDelegate transitionContext:self constrainedSizeForKey:key]; +} + +- (CGRect)initialFrameForNode:(ASDisplayNode *)node +{ + return [[self layoutForKey:ASTransitionContextFromLayoutKey] frameForElement:node]; +} + +- (CGRect)finalFrameForNode:(ASDisplayNode *)node +{ + return [[self layoutForKey:ASTransitionContextToLayoutKey] frameForElement:node]; +} + +- (NSArray *)subnodesForKey:(NSString *)key +{ + NSMutableArray *subnodes = [[NSMutableArray alloc] init]; + for (ASLayout *sublayout in [self layoutForKey:key].sublayouts) { + [subnodes addObject:(ASDisplayNode *)sublayout.layoutElement]; + } + return subnodes; +} + +- (NSArray *)insertedSubnodes +{ + return [_layoutDelegate insertedSubnodesWithTransitionContext:self]; +} + +- (NSArray *)removedSubnodes +{ + return [_layoutDelegate removedSubnodesWithTransitionContext:self]; +} + +- (void)completeTransition:(BOOL)didComplete +{ + [_completionDelegate transitionContext:self didComplete:didComplete]; +} + +@end + + +@interface _ASAnimatedTransitionContext () +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) CGFloat alpha; +@end + +@implementation _ASAnimatedTransitionContext + ++ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alpha NS_RETURNS_RETAINED +{ + _ASAnimatedTransitionContext *context = [[_ASAnimatedTransitionContext alloc] init]; + context.node = node; + context.alpha = alpha; + return context; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/asdkmodule.modulemap b/submodules/AsyncDisplayKit/Source/asdkmodule.modulemap new file mode 100644 index 0000000000..fd7d49e620 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/asdkmodule.modulemap @@ -0,0 +1,19 @@ +framework module AsyncDisplayKit { + umbrella header "AsyncDisplayKit.h" + + export * + module * { + export * + } + + explicit module ASControlNode_Subclasses { + header "ASControlNode+Subclasses.h" + export * + } + + explicit module ASDisplayNode_Subclasses { + header "ASDisplayNode+Subclasses.h" + export * + } + +} diff --git a/submodules/AsyncDisplayKit/Source/tvOS/ASControlNode+tvOS.mm b/submodules/AsyncDisplayKit/Source/tvOS/ASControlNode+tvOS.mm new file mode 100644 index 0000000000..a823d9625c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/tvOS/ASControlNode+tvOS.mm @@ -0,0 +1,92 @@ +// +// ASControlNode+tvOS.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#if TARGET_OS_TV +#import +#import + +@implementation ASControlNode (tvOS) + +#pragma mark - tvOS +- (void)_pressDown +{ + [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ + [self setPressedState]; + } completion:^(BOOL finished) { + if (finished) { + [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ + [self setFocusedState]; + } completion:nil]; + } + }]; +} + +- (BOOL)canBecomeFocused +{ + return YES; +} + +- (BOOL)shouldUpdateFocusInContext:(nonnull UIFocusUpdateContext *)context +{ + return YES; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + //FIXME: This is never valid inside an ASCellNode + if (context.nextFocusedView && context.nextFocusedView == self.view) { + //Focused + [coordinator addCoordinatedAnimations:^{ + [self setFocusedState]; + } completion:nil]; + } else{ + //Not focused + [coordinator addCoordinatedAnimations:^{ + [self setDefaultFocusAppearance]; + } completion:nil]; + } +} + +- (void)setFocusedState +{ + CALayer *layer = self.layer; + layer.shadowOffset = CGSizeMake(2, 10); + [self applyDefaultShadowProperties: layer]; + self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.1, 1.1); +} + +- (void)setPressedState +{ + CALayer *layer = self.layer; + layer.shadowOffset = CGSizeMake(2, 2); + [self applyDefaultShadowProperties: layer]; + self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1); +} + +- (void)applyDefaultShadowProperties:(CALayer *)layer +{ + layer.shadowColor = [UIColor blackColor].CGColor; + layer.shadowRadius = 12.0; + layer.shadowOpacity = 0.45; + layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; +} + +- (void)setDefaultFocusAppearance +{ + CALayer *layer = self.layer; + layer.shadowOffset = CGSizeZero; + layer.shadowColor = [UIColor blackColor].CGColor; + layer.shadowRadius = 0; + layer.shadowOpacity = 0; + layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; + self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1); +} +@end +#endif diff --git a/submodules/AsyncDisplayKit/Source/tvOS/ASImageNode+tvOS.mm b/submodules/AsyncDisplayKit/Source/tvOS/ASImageNode+tvOS.mm new file mode 100644 index 0000000000..fa760f1291 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/tvOS/ASImageNode+tvOS.mm @@ -0,0 +1,191 @@ +// +// ASImageNode+tvOS.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#if TARGET_OS_TV +#import +#import + +// TODO: Remove this – we don't need to link GLKit just to convert degrees to radians. +#import +#import + +#import + +@implementation ASImageNode (tvOS) + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + self.isDefaultFocusAppearance = NO; + UIView *view = [self getView]; + CALayer *layer = view.layer; + + CGSize targetShadowOffset = CGSizeMake(0.0, self.bounds.size.height/8); + [layer removeAllAnimations]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + layer.shadowOffset = targetShadowOffset; + }]; + + CABasicAnimation *shadowOffsetAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOffset"]; + shadowOffsetAnimation.toValue = [NSValue valueWithCGSize:targetShadowOffset]; + shadowOffsetAnimation.duration = 0.4; + shadowOffsetAnimation.removedOnCompletion = NO; + shadowOffsetAnimation.fillMode = kCAFillModeForwards; + shadowOffsetAnimation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeOut"]; + [layer addAnimation:shadowOffsetAnimation forKey:@"shadowOffset"]; + [CATransaction commit]; + + CABasicAnimation *shadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; + shadowOpacityAnimation.toValue = [NSNumber numberWithFloat:0.45]; + shadowOpacityAnimation.duration = 0.4; + shadowOpacityAnimation.removedOnCompletion = false; + shadowOpacityAnimation.fillMode = kCAFillModeForwards; + shadowOpacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeOut"]; + [layer addAnimation:shadowOpacityAnimation forKey:@"shadowOpacityAnimation"]; + + view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.25, 1.25); + + [CATransaction commit]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; + + // TODO: Clean up, and improve visuals. + + if (!self.isDefaultFocusAppearance) { + // This view may correspond to either self.view + // or our superview if we are in a ASCellNode + UIView *view = [self getView]; + + UITouch *touch = [touches anyObject]; + // Get the specific point that was touched + + // This is quite messy in it's current state so is not ready for production. + // The reason it is here is for others to contribute and to make it clear what is occuring. + + // We get the touch location in self.view because + // we are operating in that coordinate system. + // BUT we apply our transforms to *view since we want to apply + // the transforms to the root view (L: 107) + CGPoint point = [touch locationInView:self.view]; + CGFloat pitch = 0; + CGFloat yaw = 0; + BOOL topHalf = NO; + if (point.y > CGRectGetHeight(self.view.frame)) { + pitch = 15; + } else if (point.y < -CGRectGetHeight(self.view.frame)) { + pitch = -15; + } else { + pitch = (point.y/CGRectGetHeight(self.view.frame))*15; + } + if (pitch < 0) { + topHalf = YES; + } + + if (point.x > CGRectGetWidth(self.view.frame)) { + yaw = 10; + } else if (point.x < -CGRectGetWidth(self.view.frame)) { + yaw = -10; + } else { + yaw = (point.x/CGRectGetWidth(self.view.frame))*10; + } + if (!topHalf) { + if (yaw > 0) { + yaw = -yaw; + } else { + yaw = fabs(yaw); + } + } + + CATransform3D pitchTransform = CATransform3DMakeRotation(GLKMathDegreesToRadians(pitch),1.0,0.0,0.0); + CATransform3D yawTransform = CATransform3DMakeRotation(GLKMathDegreesToRadians(yaw),0.0,1.0,0.0); + CATransform3D transform = CATransform3DConcat(pitchTransform, yawTransform); + CATransform3D scaleAndTransform = CATransform3DConcat(transform, CATransform3DMakeAffineTransform(CGAffineTransformScale(CGAffineTransformIdentity, 1.25, 1.25))); + + [UIView animateWithDuration:0.5 animations:^{ + view.layer.transform = scaleAndTransform; + }]; + } else { + [self setDefaultFocusAppearance]; + } +} + + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + [self finishTouches]; +} + +- (void)finishTouches +{ + if (!self.isDefaultFocusAppearance) { + UIView *view = [self getView]; + CALayer *layer = view.layer; + + CGSize targetShadowOffset = CGSizeMake(0.0, self.bounds.size.height/8); + CATransform3D targetScaleTransform = CATransform3DMakeScale(1.2, 1.2, 1.2); + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + layer.shadowOffset = targetShadowOffset; + }]; + [CATransaction commit]; + + [UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + view.layer.transform = targetScaleTransform; + } completion:^(BOOL finished) { + if (finished) { + [layer removeAnimationForKey:@"shadowOffset"]; + [layer removeAnimationForKey:@"shadowOpacity"]; + } + }]; + } else { + [self setDefaultFocusAppearance]; + } +} + +- (void)setFocusedState +{ + UIView *view = [self getView]; + CALayer *layer = view.layer; + layer.shadowOffset = CGSizeMake(2, 10); + layer.shadowColor = [UIColor blackColor].CGColor; + layer.shadowRadius = 12.0; + layer.shadowOpacity = 0.45; + layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; + view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.25, 1.25); +} + +- (void)setDefaultFocusAppearance +{ + UIView *view = [self getView]; + CALayer *layer = view.layer; + view.transform = CGAffineTransformIdentity; + layer.shadowOpacity = 0; + layer.shadowOffset = CGSizeZero; + layer.shadowRadius = 0; + layer.shadowPath = nil; + [layer removeAnimationForKey:@"shadowOffset"]; + [layer removeAnimationForKey:@"shadowOpacity"]; + self.isDefaultFocusAppearance = YES; +} + +- (UIView *)getView +{ + // TODO: This needs to be re-visited to handle all possibilities. + // If we are inside a ASCellNode, then we need to apply our focus effects to the ASCellNode view/layer rather than the ASImageNode view/layer. + return ASDisplayNodeUltimateParentOfNode(self).view; +} + +@end +#endif diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e275becc19 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj @@ -0,0 +1,419 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + BD860CAB842324FDF0B3105C /* libPods-ASDKListKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FEACA22D54B3609C19BB4CE /* libPods-ASDKListKitTests.a */; }; + CC5532391E16F2A90011C01F /* ASListTestSupplementarySource.m in Sources */ = {isa = PBXBuildFile; fileRef = CC55322D1E16F2A90011C01F /* ASListTestSupplementarySource.m */; }; + CC55323A1E16F2A90011C01F /* ASListTestSupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC55322F1E16F2A90011C01F /* ASListTestSupplementaryNode.m */; }; + CC55323B1E16F2A90011C01F /* ASListKitTestAdapterDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532311E16F2A90011C01F /* ASListKitTestAdapterDataSource.m */; }; + CC55323C1E16F2A90011C01F /* ASListTestSection.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532331E16F2A90011C01F /* ASListTestSection.m */; }; + CC55323D1E16F2A90011C01F /* ASListTestCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532351E16F2A90011C01F /* ASListTestCellNode.m */; }; + CC55323E1E16F2A90011C01F /* ASListTestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532371E16F2A90011C01F /* ASListTestObject.m */; }; + CC55323F1E16F2A90011C01F /* ASListKitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532381E16F2A90011C01F /* ASListKitTests.m */; }; + CCC31FAD1EF9B96600E41731 /* ASDisplayNode+OCMock.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC31FA51EF9B96600E41731 /* ASDisplayNode+OCMock.m */; }; + CCC31FAE1EF9B96600E41731 /* ASTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC31FA71EF9B96600E41731 /* ASTestCase.m */; }; + CCC31FAF1EF9B96600E41731 /* NSInvocation+ASTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC31FAA1EF9B96600E41731 /* NSInvocation+ASTestHelpers.m */; }; + CCC31FB01EF9B96600E41731 /* OCMockObject+ASAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC31FAC1EF9B96600E41731 /* OCMockObject+ASAdditions.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1FEACA22D54B3609C19BB4CE /* libPods-ASDKListKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ASDKListKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + B1FDA57F88BB590E403D7BB8 /* Pods-ASDKListKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKListKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests.debug.xcconfig"; sourceTree = ""; }; + CC5532231E16EB9D0011C01F /* ASDKListKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASDKListKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CC5532281E16EB9D0011C01F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CC55322C1E16F2A90011C01F /* ASListTestSupplementarySource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestSupplementarySource.h; sourceTree = ""; }; + CC55322D1E16F2A90011C01F /* ASListTestSupplementarySource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestSupplementarySource.m; sourceTree = ""; }; + CC55322E1E16F2A90011C01F /* ASListTestSupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestSupplementaryNode.h; sourceTree = ""; }; + CC55322F1E16F2A90011C01F /* ASListTestSupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestSupplementaryNode.m; sourceTree = ""; }; + CC5532301E16F2A90011C01F /* ASListKitTestAdapterDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListKitTestAdapterDataSource.h; sourceTree = ""; }; + CC5532311E16F2A90011C01F /* ASListKitTestAdapterDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListKitTestAdapterDataSource.m; sourceTree = ""; }; + CC5532321E16F2A90011C01F /* ASListTestSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestSection.h; sourceTree = ""; }; + CC5532331E16F2A90011C01F /* ASListTestSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestSection.m; sourceTree = ""; }; + CC5532341E16F2A90011C01F /* ASListTestCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestCellNode.h; sourceTree = ""; }; + CC5532351E16F2A90011C01F /* ASListTestCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestCellNode.m; sourceTree = ""; }; + CC5532361E16F2A90011C01F /* ASListTestObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestObject.h; sourceTree = ""; }; + CC5532371E16F2A90011C01F /* ASListTestObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestObject.m; sourceTree = ""; }; + CC5532381E16F2A90011C01F /* ASListKitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListKitTests.m; sourceTree = ""; }; + CCC31FA51EF9B96600E41731 /* ASDisplayNode+OCMock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASDisplayNode+OCMock.m"; sourceTree = ""; }; + CCC31FA61EF9B96600E41731 /* ASTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTestCase.h; sourceTree = ""; }; + CCC31FA71EF9B96600E41731 /* ASTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTestCase.m; sourceTree = ""; }; + CCC31FA81EF9B96600E41731 /* ASXCTExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASXCTExtensions.h; sourceTree = ""; }; + CCC31FA91EF9B96600E41731 /* NSInvocation+ASTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+ASTestHelpers.h"; sourceTree = ""; }; + CCC31FAA1EF9B96600E41731 /* NSInvocation+ASTestHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+ASTestHelpers.m"; sourceTree = ""; }; + CCC31FAB1EF9B96600E41731 /* OCMockObject+ASAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMockObject+ASAdditions.h"; sourceTree = ""; }; + CCC31FAC1EF9B96600E41731 /* OCMockObject+ASAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCMockObject+ASAdditions.m"; sourceTree = ""; }; + D6BDED6F23A72F40F571EEF0 /* Pods-ASDKListKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKListKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CC5532201E16EB9D0011C01F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BD860CAB842324FDF0B3105C /* libPods-ASDKListKitTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 72C6817154F7AC11E373624A /* Pods */ = { + isa = PBXGroup; + children = ( + B1FDA57F88BB590E403D7BB8 /* Pods-ASDKListKitTests.debug.xcconfig */, + D6BDED6F23A72F40F571EEF0 /* Pods-ASDKListKitTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 74DAA5F5D522433F103348B7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1FEACA22D54B3609C19BB4CE /* libPods-ASDKListKitTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + CC5532181E16EB7A0011C01F = { + isa = PBXGroup; + children = ( + CC5532251E16EB9D0011C01F /* ASDKListKitTests */, + CC5532241E16EB9D0011C01F /* Products */, + 72C6817154F7AC11E373624A /* Pods */, + 74DAA5F5D522433F103348B7 /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + CC5532241E16EB9D0011C01F /* Products */ = { + isa = PBXGroup; + children = ( + CC5532231E16EB9D0011C01F /* ASDKListKitTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CC5532251E16EB9D0011C01F /* ASDKListKitTests */ = { + isa = PBXGroup; + children = ( + CCC31FA41EF9B96600E41731 /* Common */, + CC55326E1E170A740011C01F /* ListKit Fixtures */, + CC5532381E16F2A90011C01F /* ASListKitTests.m */, + CC5532281E16EB9D0011C01F /* Info.plist */, + ); + path = ASDKListKitTests; + sourceTree = ""; + }; + CC55326E1E170A740011C01F /* ListKit Fixtures */ = { + isa = PBXGroup; + children = ( + CC55322C1E16F2A90011C01F /* ASListTestSupplementarySource.h */, + CC55322D1E16F2A90011C01F /* ASListTestSupplementarySource.m */, + CC55322E1E16F2A90011C01F /* ASListTestSupplementaryNode.h */, + CC55322F1E16F2A90011C01F /* ASListTestSupplementaryNode.m */, + CC5532301E16F2A90011C01F /* ASListKitTestAdapterDataSource.h */, + CC5532311E16F2A90011C01F /* ASListKitTestAdapterDataSource.m */, + CC5532321E16F2A90011C01F /* ASListTestSection.h */, + CC5532331E16F2A90011C01F /* ASListTestSection.m */, + CC5532341E16F2A90011C01F /* ASListTestCellNode.h */, + CC5532351E16F2A90011C01F /* ASListTestCellNode.m */, + CC5532361E16F2A90011C01F /* ASListTestObject.h */, + CC5532371E16F2A90011C01F /* ASListTestObject.m */, + ); + name = "ListKit Fixtures"; + sourceTree = ""; + }; + CCC31FA41EF9B96600E41731 /* Common */ = { + isa = PBXGroup; + children = ( + CCC31FA51EF9B96600E41731 /* ASDisplayNode+OCMock.m */, + CCC31FA61EF9B96600E41731 /* ASTestCase.h */, + CCC31FA71EF9B96600E41731 /* ASTestCase.m */, + CCC31FA81EF9B96600E41731 /* ASXCTExtensions.h */, + CCC31FA91EF9B96600E41731 /* NSInvocation+ASTestHelpers.h */, + CCC31FAA1EF9B96600E41731 /* NSInvocation+ASTestHelpers.m */, + CCC31FAB1EF9B96600E41731 /* OCMockObject+ASAdditions.h */, + CCC31FAC1EF9B96600E41731 /* OCMockObject+ASAdditions.m */, + ); + name = Common; + path = ../../Tests/Common; + sourceTree = SOURCE_ROOT; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CC5532221E16EB9D0011C01F /* ASDKListKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CC5532291E16EB9D0011C01F /* Build configuration list for PBXNativeTarget "ASDKListKitTests" */; + buildPhases = ( + 614B24BFF3DA58512D2E2147 /* [CP] Check Pods Manifest.lock */, + CC55321F1E16EB9D0011C01F /* Sources */, + CC5532201E16EB9D0011C01F /* Frameworks */, + CC5532211E16EB9D0011C01F /* Resources */, + 989E6C194A1983B8B21AB82F /* [CP] Embed Pods Frameworks */, + 876CE14CAF6A87E34577E157 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ASDKListKitTests; + productName = ASDKListKitTests; + productReference = CC5532231E16EB9D0011C01F /* ASDKListKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CC5532191E16EB7A0011C01F /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0820; + TargetAttributes = { + CC5532221E16EB9D0011C01F = { + CreatedOnToolsVersion = 8.2.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = CC55321C1E16EB7A0011C01F /* Build configuration list for PBXProject "ASDKListKit" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CC5532181E16EB7A0011C01F; + productRefGroup = CC5532241E16EB9D0011C01F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CC5532221E16EB9D0011C01F /* ASDKListKitTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CC5532211E16EB9D0011C01F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 614B24BFF3DA58512D2E2147 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 876CE14CAF6A87E34577E157 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 989E6C194A1983B8B21AB82F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CC55321F1E16EB9D0011C01F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CCC31FAD1EF9B96600E41731 /* ASDisplayNode+OCMock.m in Sources */, + CC55323E1E16F2A90011C01F /* ASListTestObject.m in Sources */, + CCC31FAF1EF9B96600E41731 /* NSInvocation+ASTestHelpers.m in Sources */, + CC5532391E16F2A90011C01F /* ASListTestSupplementarySource.m in Sources */, + CCC31FB01EF9B96600E41731 /* OCMockObject+ASAdditions.m in Sources */, + CC55323D1E16F2A90011C01F /* ASListTestCellNode.m in Sources */, + CC55323B1E16F2A90011C01F /* ASListKitTestAdapterDataSource.m in Sources */, + CCC31FAE1EF9B96600E41731 /* ASTestCase.m in Sources */, + CC55323C1E16F2A90011C01F /* ASListTestSection.m in Sources */, + CC55323F1E16F2A90011C01F /* ASListKitTests.m in Sources */, + CC55323A1E16F2A90011C01F /* ASListTestSupplementaryNode.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + CC55321D1E16EB7A0011C01F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Debug; + }; + CC55321E1E16EB7A0011C01F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Release; + }; + CC55322A1E16EB9D0011C01F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B1FDA57F88BB590E403D7BB8 /* Pods-ASDKListKitTests.debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = ASDKListKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = asyncdisplaykit.ASDKListKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + CC55322B1E16EB9D0011C01F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D6BDED6F23A72F40F571EEF0 /* Pods-ASDKListKitTests.release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = ASDKListKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = asyncdisplaykit.ASDKListKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CC55321C1E16EB7A0011C01F /* Build configuration list for PBXProject "ASDKListKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CC55321D1E16EB7A0011C01F /* Debug */, + CC55321E1E16EB7A0011C01F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CC5532291E16EB9D0011C01F /* Build configuration list for PBXNativeTarget "ASDKListKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CC55322A1E16EB9D0011C01F /* Debug */, + CC55322B1E16EB9D0011C01F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CC5532191E16EB7A0011C01F /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..5f9c1bf23a --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..4d0db5485c --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h new file mode 100644 index 0000000000..c3cdab0bad --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h @@ -0,0 +1,17 @@ +// +// ASListKitTestAdapterDataSource.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASListKitTestAdapterDataSource : NSObject + +// array of numbers which is then passed to -[IGListTestSection setItems:] +@property (nonatomic, strong) NSArray *objects; + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m new file mode 100644 index 0000000000..b0cd9a8a6d --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m @@ -0,0 +1,31 @@ +// +// ASListKitTestAdapterDataSource.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASListKitTestAdapterDataSource.h" +#import "ASListTestSection.h" + +@implementation ASListKitTestAdapterDataSource + +- (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter +{ + return self.objects; +} + +- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object +{ + ASListTestSection *section = [[ASListTestSection alloc] init]; + return section; +} + +- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter +{ + return nil; +} + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTests.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTests.m new file mode 100644 index 0000000000..1bb2c6fed0 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListKitTests.m @@ -0,0 +1,112 @@ +// +// ASListKitTests.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import "ASListKitTestAdapterDataSource.h" +#import "ASXCTExtensions.h" +#import +#import "ASTestCase.h" + +@interface ASListKitTests : ASTestCase + +@property (nonatomic, strong) ASCollectionNode *collectionNode; +@property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, strong) IGListAdapter *adapter; +@property (nonatomic, strong) ASListKitTestAdapterDataSource *dataSource; +@property (nonatomic, strong) UICollectionViewFlowLayout *layout; +@property (nonatomic, strong) UIWindow *window; +@property (nonatomic) NSInteger reloadDataCount; + +@end + +@implementation ASListKitTests + +- (void)setUp +{ + [super setUp]; + + [ASCollectionView swizzleInstanceMethod:@selector(reloadData) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, ASCollectionView *) { + JGOriginalImplementation(void); + _reloadDataCount++; + }; + }]; + + self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + + self.layout = [[UICollectionViewFlowLayout alloc] init]; + self.collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:self.layout]; + self.collectionNode.frame = self.window.bounds; + self.collectionView = self.collectionNode.view; + + [self.window addSubnode:self.collectionNode]; + + IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; + + self.dataSource = [[ASListKitTestAdapterDataSource alloc] init]; + self.adapter = [[IGListAdapter alloc] initWithUpdater:updater + viewController:nil + workingRangeSize:0]; + self.adapter.dataSource = self.dataSource; + [self.adapter setASDKCollectionNode:self.collectionNode]; + XCTAssertNotNil(self.adapter.collectionView, @"Adapter was not bound to collection view. You may have a stale copy of AsyncDisplayKit that was built without IG_LIST_KIT. Clean Builder Folder IMO."); +} + +- (void)tearDown +{ + [super tearDown]; + XCTAssert([ASCollectionView deswizzleAllMethods]); + self.reloadDataCount = 0; + self.window = nil; + self.collectionNode = nil; + self.collectionView = nil; + self.adapter = nil; + self.dataSource = nil; + self.layout = nil; +} + +- (void)test_whenAdapterUpdated_withObjectsOverflow_thatVisibleObjectsIsSubsetOfAllObjects +{ + // each section controller returns n items sized 100x10 + self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; + XCTestExpectation *e = [self expectationWithDescription:@"Data update completed"]; + + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + [e fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + self.collectionNode.view.contentOffset = CGPointMake(0, 30); + [self.collectionNode.view layoutIfNeeded]; + + + NSArray *visibleObjects = [[self.adapter visibleObjects] sortedArrayUsingSelector:@selector(compare:)]; + NSArray *expectedObjects = @[@3, @4, @5]; + XCTAssertEqualObjects(visibleObjects, expectedObjects); +} + +- (void)test_whenCollectionViewIsNotInAWindow_updaterDoesNotJustCallReloadData +{ + [self.collectionView removeFromSuperview]; + + [self.collectionView layoutIfNeeded]; + self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; + XCTestExpectation *e = [self expectationWithDescription:@"Data update completed"]; + + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + [e fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + [self.collectionView layoutIfNeeded]; + + XCTAssertEqual(self.reloadDataCount, 2); +} + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestCellNode.h b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestCellNode.h new file mode 100644 index 0000000000..e0a4ad7f44 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestCellNode.h @@ -0,0 +1,14 @@ +// +// ASListTestCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASListTestCellNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestCellNode.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestCellNode.m new file mode 100644 index 0000000000..cd3bfb3a8d --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestCellNode.m @@ -0,0 +1,14 @@ +// +// ASListTestCellNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASListTestCellNode.h" + +@implementation ASListTestCellNode + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestObject.h b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestObject.h new file mode 100644 index 0000000000..1362ebf560 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestObject.h @@ -0,0 +1,23 @@ +// +// ASListTestObject.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASListTestObject : NSObject + +- (instancetype)initWithKey:(id )key value:(id)value; + +@property (nonatomic, strong, readonly) id key; +@property (nonatomic, strong) id value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestObject.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestObject.m new file mode 100644 index 0000000000..64cd83649b --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestObject.m @@ -0,0 +1,50 @@ +// +// ASListTestObject.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASListTestObject.h" + +@implementation ASListTestObject + +- (instancetype)initWithKey:(id)key value:(id)value +{ + if (self = [super init]) { + _key = [key copy]; + _value = value; + } + return self; +} + +- (instancetype)copyWithZone:(NSZone *)zone +{ + return [[ASListTestObject alloc] initWithKey:self.key value:self.value]; +} + +#pragma mark - IGListDiffable + +- (id)diffIdentifier +{ + return self.key; +} + +- (BOOL)isEqualToDiffableObject:(id)object +{ + if (object == self) { + return YES; + } + if ([object isKindOfClass:[ASListTestObject class]]) { + id k1 = self.key; + id k2 = [object key]; + id v1 = self.value; + id v2 = [(ASListTestObject *)object value]; + return (v1 == v2 || [v1 isEqual:v2]) && (k1 == k2 || [k1 isEqual:k2]); + } + return NO; +} + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.h b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.h new file mode 100644 index 0000000000..d62e21b1f6 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.h @@ -0,0 +1,19 @@ +// +// ASListTestSection.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASListTestSection : IGListSectionController + +@property (nonatomic) NSInteger itemCount; + +@property (nonatomic) NSInteger selectedItemIndex; + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m new file mode 100644 index 0000000000..edf939b7c0 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m @@ -0,0 +1,61 @@ +// +// ASListTestSection.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASListTestSection.h" +#import "ASListTestCellNode.h" + +@implementation ASListTestSection + +- (instancetype)init +{ + if (self = [super init]) +{ + _selectedItemIndex = NSNotFound; + } + return self; +} + +- (NSInteger)numberOfItems +{ + return self.itemCount; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index +{ + return [ASIGListSectionControllerMethods sizeForItemAtIndex:index]; +} + +- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index +{ + return [ASIGListSectionControllerMethods cellForItemAtIndex:index sectionController:self]; +} + +- (void)didUpdateToObject:(id)object +{ + if ([object isKindOfClass:[NSNumber class]]) +{ + self.itemCount = [object integerValue]; + } +} + +- (void)didSelectItemAtIndex:(NSInteger)index +{ + self.selectedItemIndex = index; +} + +- (ASCellNodeBlock)nodeBlockForItemAtIndex:(NSInteger)index +{ + return ^{ + ASListTestCellNode *node = [[ASListTestCellNode alloc] init]; + node.style.preferredSize = CGSizeMake(100, 10); + return node; + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h new file mode 100644 index 0000000000..c4f40e4ab7 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h @@ -0,0 +1,14 @@ +// +// ASListTestSupplementaryNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASListTestSupplementaryNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m new file mode 100644 index 0000000000..a10f6a8f69 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m @@ -0,0 +1,14 @@ +// +// ASListTestSupplementaryNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASListTestSupplementaryNode.h" + +@implementation ASListTestSupplementaryNode + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h new file mode 100644 index 0000000000..acdec1ef94 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h @@ -0,0 +1,21 @@ +// +// ASListTestSupplementarySource.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASListTestSupplementarySource : NSObject + +@property (nonatomic, strong, readwrite) NSArray *supportedElementKinds; + +@property (nonatomic, weak) id collectionContext; + +@property (nonatomic, weak) IGListSectionController *sectionController; + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m new file mode 100644 index 0000000000..07410131e2 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m @@ -0,0 +1,34 @@ +// +// ASListTestSupplementarySource.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASListTestSupplementarySource.h" +#import "ASListTestSupplementaryNode.h" + +@implementation ASListTestSupplementarySource + +- (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + return [ASIGListSupplementaryViewSourceMethods viewForSupplementaryElementOfKind:elementKind atIndex:index sectionController:self.sectionController]; +} + +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + return [ASIGListSupplementaryViewSourceMethods sizeForSupplementaryViewOfKind:elementKind atIndex:index]; +} + +- (ASCellNodeBlock)nodeBlockForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + return ^{ + ASListTestSupplementaryNode *node = [[ASListTestSupplementaryNode alloc] init]; + node.style.preferredSize = CGSizeMake(100, 10); + return node; + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/Info.plist b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/Info.plist new file mode 100644 index 0000000000..6c6c23c43a --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/Podfile b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/Podfile new file mode 100644 index 0000000000..36498c2473 --- /dev/null +++ b/submodules/AsyncDisplayKit/SubspecWorkspaces/ASDKListKit/Podfile @@ -0,0 +1,9 @@ +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '9.0' +target 'ASDKListKitTests' do + pod 'Texture/IGListKit', :path => '../..' + pod 'OCMock', '~> 3.4' + pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' +end + diff --git a/submodules/AsyncDisplayKit/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..a1b9b94596 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm @@ -0,0 +1,70 @@ +// +// ASAbsoluteLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import + +@interface ASAbsoluteLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASAbsoluteLayoutSpecSnapshotTests + +- (void)testSizingBehaviour +{ + [self testWithSizeRange:ASSizeRangeMake(CGSizeMake(150, 200), CGSizeMake(INFINITY, INFINITY)) + identifier:@"underflowChildren"]; + [self testWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(50, 100)) + identifier:@"overflowChildren"]; + // Expect the spec to wrap its content because children sizes are between constrained size + [self testWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY / 2, INFINITY / 2)) + identifier:@"wrappedChildren"]; +} + +- (void)testChildrenMeasuredWithAutoMaxSize +{ + ASDisplayNode *firstChild = ASDisplayNodeWithBackgroundColor([UIColor redColor], (CGSize){50, 50}); + firstChild.style.layoutPosition = CGPointMake(0, 0); + + ASDisplayNode *secondChild = ASDisplayNodeWithBackgroundColor([UIColor blueColor], (CGSize){100, 100}); + secondChild.style.layoutPosition = CGPointMake(10, 60); + + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(10, 10), CGSizeMake(110, 160)); + [self testWithChildren:@[firstChild, secondChild] sizeRange:sizeRange identifier:nil]; +} + +- (void)testWithSizeRange:(ASSizeRange)sizeRange identifier:(NSString *)identifier +{ + ASDisplayNode *firstChild = ASDisplayNodeWithBackgroundColor([UIColor redColor], (CGSize){50, 50}); + firstChild.style.layoutPosition = CGPointMake(0, 0); + + ASDisplayNode *secondChild = ASDisplayNodeWithBackgroundColor([UIColor blueColor], (CGSize){100, 100}); + secondChild.style.layoutPosition = CGPointMake(0, 50); + + [self testWithChildren:@[firstChild, secondChild] sizeRange:sizeRange identifier:identifier]; +} + +- (void)testWithChildren:(NSArray *)children sizeRange:(ASSizeRange)sizeRange identifier:(NSString *)identifier +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); + + NSMutableArray *subnodes = [NSMutableArray arrayWithArray:children]; + [subnodes insertObject:backgroundNode atIndex:0]; + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild: + [ASAbsoluteLayoutSpec + absoluteLayoutSpecWithChildren:children] + background:backgroundNode]; + + [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASBackgroundLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASBackgroundLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..1a6b4fc7c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASBackgroundLayoutSpecSnapshotTests.mm @@ -0,0 +1,40 @@ +// +// ASBackgroundLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import + +static const ASSizeRange kSize = {{320, 320}, {320, 320}}; + +@interface ASBackgroundLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase + +@end + +@implementation ASBackgroundLayoutSpecSnapshotTests + +- (void)testBackground +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor blueColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor blackColor], {20, 20}); + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASCenterLayoutSpec + centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY + sizingOptions:{} + child:foregroundNode] + background:backgroundNode]; + + [self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:@[backgroundNode, foregroundNode] identifier: nil]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASBasicImageDownloaderContextTests.mm b/submodules/AsyncDisplayKit/Tests/ASBasicImageDownloaderContextTests.mm new file mode 100644 index 0000000000..701174b7dd --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASBasicImageDownloaderContextTests.mm @@ -0,0 +1,75 @@ +// +// ASBasicImageDownloaderContextTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +#import + + +@interface ASBasicImageDownloaderContextTests : XCTestCase + +@end + +@implementation ASBasicImageDownloaderContextTests + +- (NSURL *)randomURL +{ + // random URL for each test, doesn't matter that this is not really a URL + return [NSURL URLWithString:[NSUUID UUID].UUIDString]; +} + +- (void)testContextCreation +{ + NSURL *url = [self randomURL]; + ASBasicImageDownloaderContext *c1 = [ASBasicImageDownloaderContext contextForURL:url]; + ASBasicImageDownloaderContext *c2 = [ASBasicImageDownloaderContext contextForURL:url]; + XCTAssert(c1 == c2, @"Context objects are not the same"); +} + +- (void)testContextInvalidation +{ + NSURL *url = [self randomURL]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:url]; + [context cancel]; + XCTAssert([context isCancelled], @"Context should be cancelled"); +} + +/* This test is currently unreliable. See https://github.com/facebook/AsyncDisplayKit/issues/459 +- (void)testAsyncContextInvalidation +{ + NSURL *url = [self randomURL]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:url]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Context invalidation"]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [expectation fulfill]; + XCTAssert([context isCancelled], @"Context should be cancelled"); + }); + + [context cancel]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} +*/ + +- (void)testContextSessionCanceled +{ + NSURL *url = [self randomURL]; + id task = [OCMockObject mockForClass:[NSURLSessionTask class]]; + ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:url]; + context.sessionTask = task; + + [[task expect] cancel]; + + [context cancel]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASBasicImageDownloaderTests.mm b/submodules/AsyncDisplayKit/Tests/ASBasicImageDownloaderTests.mm new file mode 100644 index 0000000000..8268dfcd64 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASBasicImageDownloaderTests.mm @@ -0,0 +1,45 @@ +// +// ASBasicImageDownloaderTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@interface ASBasicImageDownloaderTests : XCTestCase + +@end + +@implementation ASBasicImageDownloaderTests + +- (void)testAsynchronouslyDownloadTheSameURLTwice +{ + XCTestExpectation *firstExpectation = [self expectationWithDescription:@"First ASBasicImageDownloader completion handler should be called within 3 seconds"]; + XCTestExpectation *secondExpectation = [self expectationWithDescription:@"Second ASBasicImageDownloader completion handler should be called within 3 seconds"]; + + ASBasicImageDownloader *downloader = [ASBasicImageDownloader sharedImageDownloader]; + NSURL *URL = [NSURL URLWithString:@"http://wrongPath/wrongResource.png"]; + + [downloader downloadImageWithURL:URL + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + downloadProgress:nil + completion:^(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { + [firstExpectation fulfill]; + }]; + + [downloader downloadImageWithURL:URL + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + downloadProgress:nil + completion:^(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { + [secondExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASBatchFetchingTests.mm b/submodules/AsyncDisplayKit/Tests/ASBatchFetchingTests.mm new file mode 100644 index 0000000000..99b67f05c0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASBatchFetchingTests.mm @@ -0,0 +1,120 @@ +// +// ASBatchFetchingTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +@interface ASBatchFetchingTests : XCTestCase + +@end + +@implementation ASBatchFetchingTests + +#define PASSING_RECT CGRectMake(0,0,1,1) +#define PASSING_SIZE CGSizeMake(1,1) +#define PASSING_POINT CGPointMake(1,1) +#define VERTICAL_RECT(h) CGRectMake(0,0,1,h) +#define VERTICAL_SIZE(h) CGSizeMake(0,h) +#define VERTICAL_OFFSET(y) CGPointMake(0,y) +#define HORIZONTAL_RECT(w) CGRectMake(0,0,w,1) +#define HORIZONTAL_SIZE(w) CGSizeMake(w,0) +#define HORIZONTAL_OFFSET(x) CGPointMake(x,0) + +- (void)testBatchNullState { + ASBatchContext *context = [[ASBatchContext alloc] init]; + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, CGRectZero, CGSizeZero, CGPointZero, 0.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == NO, @"Should not fetch in the null state"); +} + +- (void)testBatchAlreadyFetching { + ASBatchContext *context = [[ASBatchContext alloc] init]; + [context beginBatchFetching]; + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == NO, @"Should not fetch when context is already fetching"); +} + +- (void)testUnsupportedScrollDirections { + ASBatchContext *context = [[ASBatchContext alloc] init]; + BOOL fetchRight = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, ASScrollDirectionHorizontalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0, YES, CGPointZero, nil); + XCTAssert(fetchRight == YES, @"Should fetch for scrolling right"); + BOOL fetchDown = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0, YES, CGPointZero, nil); + XCTAssert(fetchDown == YES, @"Should fetch for scrolling down"); + BOOL fetchUp = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, ASScrollDirectionVerticalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0, YES, CGPointZero, nil); + XCTAssert(fetchUp == NO, @"Should not fetch for scrolling up"); + BOOL fetchLeft = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, ASScrollDirectionHorizontalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0, YES, CGPointZero, nil); + XCTAssert(fetchLeft == NO, @"Should not fetch for scrolling left"); +} + +- (void)testVerticalScrollToExactLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // scroll to 1-screen top offset, height is 1 screen, so bottom is 1 screen away from end of content + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 1.0), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling to exactly 1 leading screen away"); +} + +- (void)testVerticalScrollToLessThanLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, scroll only 1/2 of one screen + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 0.5), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == NO, @"Fetch should not begin when vertically scrolling less than the leading distance away"); +} + +- (void)testVerticalScrollingPastContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, top offset to 3-screens, height 1 screen, so its 1 screen past the leading + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 3.0), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); +} + +- (void)testHorizontalScrollToExactLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // scroll to 1-screen left offset, width is 1 screen, so right is 1 screen away from end of content + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, ASScrollDirectionVerticalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 1.0), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == YES, @"Fetch should begin when horizontally scrolling to exactly 1 leading screen away"); +} + +- (void)testHorizontalScrollToLessThanLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, scroll only 1/2 of one screen + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, ASScrollDirectionHorizontalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 0.5), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == NO, @"Fetch should not begin when horizontally scrolling less than the leading distance away"); +} + +- (void)testHorizontalScrollingPastContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, left offset to 3-screens, width 1 screen, so its 1 screen past the leading + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionHorizontalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 3.0), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); +} + +- (void)testVerticalScrollingSmallContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // when the content size is < screen size, the target offset will always be 0 + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 0.5), VERTICAL_OFFSET(0.0), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == YES, @"Fetch should begin when the target is 0 and the content size is smaller than the scree"); +} + +- (void)testHorizontalScrollingSmallContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // when the content size is < screen size, the target offset will always be 0 + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, ASScrollDirectionHorizontalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 0.5), HORIZONTAL_OFFSET(0.0), 1.0, YES, CGPointZero, nil); + XCTAssert(shouldFetch == YES, @"Fetch should begin when the target is 0 and the content size is smaller than the scree"); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASBridgedPropertiesTests.mm b/submodules/AsyncDisplayKit/Tests/ASBridgedPropertiesTests.mm new file mode 100644 index 0000000000..8e4a202eb8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASBridgedPropertiesTests.mm @@ -0,0 +1,231 @@ +// +// ASBridgedPropertiesTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import + +@interface ASPendingStateController (Testing) +- (BOOL)test_isFlushScheduled; +@end + +@interface ASBridgedPropertiesTestView : UIView +@property (nonatomic, readonly) BOOL receivedSetNeedsLayout; +@end + +@implementation ASBridgedPropertiesTestView + +- (void)setNeedsLayout +{ + _receivedSetNeedsLayout = YES; + [super setNeedsLayout]; +} + +@end + +@interface ASBridgedPropertiesTestNode : ASDisplayNode +@property (nullable, nonatomic, copy) dispatch_block_t onDealloc; +@end + +@implementation ASBridgedPropertiesTestNode + +- (void)dealloc { + _onDealloc(); +} + +@end + +@interface ASBridgedPropertiesTests : XCTestCase +@end + +/// Dispatches the given block synchronously onto a different thread. +/// This is useful for testing non-main-thread behavior because `dispatch_sync` +/// will often use the current thread. +static inline void ASDispatchSyncOnOtherThread(dispatch_block_t block) { + dispatch_group_t group = dispatch_group_create(); + dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_group_enter(group); + dispatch_async(q, ^{ + ASDisplayNodeCAssertNotMainThread(); + block(); + dispatch_group_leave(group); + }); + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); +} + +@implementation ASBridgedPropertiesTests + +- (void)testTheresASharedInstance +{ + XCTAssertNotNil([ASPendingStateController sharedInstance]); +} + +/// FIXME: This test is unreliable for an as-yet unknown reason +/// but that being intermittent, and this test being so strict, it's +/// reasonable to assume for now the failures don't reflect a framework bug. +/// See https://github.com/facebook/AsyncDisplayKit/pull/1048 +- (void)DISABLED_testThatDirtyNodesAreNotRetained +{ + ASPendingStateController *ctrl = [ASPendingStateController sharedInstance]; + __block BOOL didDealloc = NO; + @autoreleasepool { + __attribute__((objc_precise_lifetime)) ASBridgedPropertiesTestNode *node = [ASBridgedPropertiesTestNode new]; + node.onDealloc = ^{ + didDealloc = YES; + }; + [node view]; + XCTAssertEqual(node.alpha, 1); + ASDispatchSyncOnOtherThread(^{ + node.alpha = 0; + }); + XCTAssertEqual(node.alpha, 1); + XCTAssert(ctrl.test_isFlushScheduled); + } + XCTAssertTrue(didDealloc); +} + +- (void)testThatSettingABridgedViewPropertyInBackgroundGetsFlushedOnNextRunLoop +{ + ASDisplayNode *node = [ASDisplayNode new]; + [node view]; + XCTAssertEqual(node.alpha, 1); + ASDispatchSyncOnOtherThread(^{ + node.alpha = 0; + }); + XCTAssertEqual(node.alpha, 1); + [self waitForMainDispatchQueueToFlush]; + XCTAssertEqual(node.alpha, 0); +} + +- (void)testThatSettingABridgedLayerPropertyInBackgroundGetsFlushedOnNextRunLoop +{ + ASDisplayNode *node = [ASDisplayNode new]; + [node view]; + XCTAssertEqual(node.shadowOpacity, 0); + ASDispatchSyncOnOtherThread(^{ + node.shadowOpacity = 1; + }); + XCTAssertEqual(node.shadowOpacity, 0); + [self waitForMainDispatchQueueToFlush]; + XCTAssertEqual(node.shadowOpacity, 1); +} + +- (void)testThatReadingABridgedViewPropertyInBackgroundThrowsAnException +{ + ASDisplayNode *node = [ASDisplayNode new]; + [node view]; + ASDispatchSyncOnOtherThread(^{ + XCTAssertThrows(node.alpha); + }); +} + +- (void)testThatReadingABridgedLayerPropertyInBackgroundThrowsAnException +{ + ASDisplayNode *node = [ASDisplayNode new]; + [node view]; + ASDispatchSyncOnOtherThread(^{ + XCTAssertThrows(node.contentsScale); + }); +} + +- (void)testThatManuallyFlushingTheSyncControllerImmediatelyAppliesChanges +{ + ASPendingStateController *ctrl = [ASPendingStateController sharedInstance]; + ASDisplayNode *node = [ASDisplayNode new]; + [node view]; + XCTAssertEqual(node.alpha, 1); + ASDispatchSyncOnOtherThread(^{ + node.alpha = 0; + }); + XCTAssertEqual(node.alpha, 1); + [ctrl flush]; + XCTAssertEqual(node.alpha, 0); + XCTAssertFalse(ctrl.test_isFlushScheduled); +} + +- (void)testThatFlushingTheControllerInBackgroundThrows +{ + ASPendingStateController *ctrl = [ASPendingStateController sharedInstance]; + ASDisplayNode *node = [ASDisplayNode new]; + [node view]; + XCTAssertEqual(node.alpha, 1); + ASDispatchSyncOnOtherThread(^{ + node.alpha = 0; + XCTAssertThrows([ctrl flush]); + }); +} + +- (void)testThatSettingABridgedPropertyOnMainThreadPassesDirectlyToView +{ + ASPendingStateController *ctrl = [ASPendingStateController sharedInstance]; + ASDisplayNode *node = [ASDisplayNode new]; + XCTAssertFalse(ASDisplayNodeGetPendingState(node).hasChanges); + [node view]; + XCTAssertEqual(node.alpha, 1); + node.alpha = 0; + XCTAssertEqual(node.view.alpha, 0); + XCTAssertEqual(node.alpha, 0); + XCTAssertFalse(ASDisplayNodeGetPendingState(node).hasChanges); + XCTAssertFalse(ctrl.test_isFlushScheduled); +} + +- (void)testThatCallingSetNeedsLayoutFromBackgroundCausesItToHappenLater +{ + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewClass:ASBridgedPropertiesTestView.class]; + ASBridgedPropertiesTestView *view = (ASBridgedPropertiesTestView *)node.view; + XCTAssertFalse(view.receivedSetNeedsLayout); + ASDispatchSyncOnOtherThread(^{ + XCTAssertNoThrow([node setNeedsLayout]); + }); + XCTAssertFalse(view.receivedSetNeedsLayout); + [self waitForMainDispatchQueueToFlush]; + XCTAssertTrue(view.receivedSetNeedsLayout); +} + +- (void)testThatCallingSetNeedsLayoutOnACellNodeFromBackgroundIsSafe +{ + ASCellNode *node = [ASCellNode new]; + [node view]; + ASDispatchSyncOnOtherThread(^{ + XCTAssertNoThrow([node setNeedsLayout]); + }); +} + +- (void)testThatCallingSetNeedsDisplayFromBackgroundCausesItToHappenLater +{ + ASDisplayNode *node = [ASDisplayNode new]; + [node.layer displayIfNeeded]; + XCTAssertFalse(node.layer.needsDisplay); + ASDispatchSyncOnOtherThread(^{ + XCTAssertNoThrow([node setNeedsDisplay]); + }); + XCTAssertFalse(node.layer.needsDisplay); + [self waitForMainDispatchQueueToFlush]; + XCTAssertTrue(node.layer.needsDisplay); +} + +/// [XCTExpectation expectationWithPredicate:] should handle this +/// but under Xcode 7.2.1 its polling interval is 1 second +/// which makes the tests really slow and I'm impatient. +- (void)waitForMainDispatchQueueToFlush +{ + __block BOOL done = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + done = YES; + }); + while (!done) { + [NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASButtonNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASButtonNodeTests.mm new file mode 100644 index 0000000000..244edd1f0e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASButtonNodeTests.mm @@ -0,0 +1,54 @@ +// +// ASButtonNodeTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +@interface ASButtonNodeTests : XCTestCase +@end + +@implementation ASButtonNodeTests + +- (void)testAccessibility +{ + // Setup a button with some title. + ASButtonNode *buttonNode = nil; + buttonNode = [[ASButtonNode alloc] init]; + NSString *title = @"foo"; + [buttonNode setTitle:title withFont:nil withColor:nil forState:UIControlStateNormal]; + + // Verify accessibility properties. + XCTAssertTrue(buttonNode.accessibilityTraits == UIAccessibilityTraitButton, + @"Should have button accessibility trait, instead has %llu", + buttonNode.accessibilityTraits); + XCTAssertTrue(buttonNode.defaultAccessibilityTraits == UIAccessibilityTraitButton, + @"Default accessibility traits should return button accessibility trait, instead " + @"returns %llu", + buttonNode.defaultAccessibilityTraits); + XCTAssertTrue([buttonNode.accessibilityLabel isEqualToString:title], + @"Accessibility label is incorrectly set to \n%@\n when it should be \n%@\n", + buttonNode.accessibilityLabel, title); + XCTAssertTrue([buttonNode.defaultAccessibilityLabel isEqualToString:title], + @"Default accessibility label incorrectly returns \n%@\n when it should be \n%@\n", + buttonNode.defaultAccessibilityLabel, title); + + // Disable the button and verify that accessibility traits has been updated correctly. + buttonNode.enabled = NO; + UIAccessibilityTraits disabledButtonTrait = UIAccessibilityTraitButton | UIAccessibilityTraitNotEnabled; + XCTAssertTrue(buttonNode.accessibilityTraits == disabledButtonTrait, + @"Should have disabled button accessibility trait, instead has %llu", + buttonNode.accessibilityTraits); + XCTAssertTrue(buttonNode.defaultAccessibilityTraits == disabledButtonTrait, + @"Default accessibility traits should return disabled button accessibility trait, " + @"instead returns %llu", + buttonNode.defaultAccessibilityTraits); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCALayerTests.mm b/submodules/AsyncDisplayKit/Tests/ASCALayerTests.mm new file mode 100644 index 0000000000..2b30da28fc --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCALayerTests.mm @@ -0,0 +1,107 @@ +// +// ASCALayerTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +/** + * Tests that confirm what we know about Core Animation behavior. + * + * These tests are not run during the normal test action. You can run them yourself + * to investigate and confirm CA behavior. + */ +@interface ASCALayerTests : XCTestCase + +@end + +#define DeclareLayerAndSublayer() \ + CALayer *realSublayer = [CALayer layer]; \ + id layer = [OCMockObject partialMockForObject:[CALayer layer]]; \ + id sublayer = [OCMockObject partialMockForObject:realSublayer]; \ + [layer addSublayer:realSublayer]; + +@implementation ASCALayerTests + +- (void)testThatLayerBeginsWithCleanLayout +{ + XCTAssertFalse([CALayer layer].needsLayout); +} + +- (void)testThatAddingSublayersDirtysLayout +{ + CALayer *layer = [CALayer layer]; + [layer addSublayer:[CALayer layer]]; + XCTAssertTrue([layer needsLayout]); +} + +- (void)testThatRemovingSublayersDirtysLayout +{ + DeclareLayerAndSublayer(); + [layer layoutIfNeeded]; + XCTAssertFalse([layer needsLayout]); + [sublayer removeFromSuperlayer]; + XCTAssertTrue([layer needsLayout]); +} + +- (void)testDirtySublayerLayoutDoesntDirtySuperlayer +{ + DeclareLayerAndSublayer(); + [layer layoutIfNeeded]; + + // Dirtying sublayer doesn't dirty superlayer. + [sublayer setNeedsLayout]; + XCTAssertTrue([sublayer needsLayout]); + XCTAssertFalse([layer needsLayout]); + [[[sublayer expect] andForwardToRealObject] layoutSublayers]; + // NOTE: We specifically don't expect layer to get -layoutSublayers + [sublayer layoutIfNeeded]; + [sublayer verify]; + [layer verify]; +} + +- (void)testDirtySuperlayerLayoutDoesntDirtySublayerLayout +{ + DeclareLayerAndSublayer(); + [layer layoutIfNeeded]; + + // Dirtying superlayer doesn't dirty sublayer. + [layer setNeedsLayout]; + XCTAssertTrue([layer needsLayout]); + XCTAssertFalse([sublayer needsLayout]); + [[[layer expect] andForwardToRealObject] layoutSublayers]; + // NOTE: We specifically don't expect sublayer to get -layoutSublayers + [layer layoutIfNeeded]; + [sublayer verify]; + [layer verify]; +} + +- (void)testDirtyHierarchyIsLaidOutTopDown +{ + DeclareLayerAndSublayer(); + [sublayer setNeedsLayout]; + + XCTAssertTrue([layer needsLayout]); + XCTAssertTrue([sublayer needsLayout]); + + __block BOOL superlayerLaidOut = NO; + [[[[layer expect] andDo:^(NSInvocation *i) { + superlayerLaidOut = YES; + }] andForwardToRealObject] layoutSublayers]; + + [[[[sublayer expect] andDo:^(NSInvocation *i) { + XCTAssertTrue(superlayerLaidOut); + }] andForwardToRealObject] layoutSublayers]; + + [layer layoutIfNeeded]; + [sublayer verify]; + [layer verify]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCenterLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASCenterLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..d0352bb446 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCenterLayoutSpecSnapshotTests.mm @@ -0,0 +1,112 @@ +// +// ASCenterLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import +#import + +static const ASSizeRange kSize = {{100, 120}, {320, 160}}; + +@interface ASCenterLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASCenterLayoutSpecSnapshotTests + +- (void)testWithOptions +{ + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringNone sizingOptions:{}]; + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:{}]; + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringX sizingOptions:{}]; + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringY sizingOptions:{}]; +} + +- (void)testWithSizingOptions +{ + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringNone + sizingOptions:ASCenterLayoutSpecSizingOptionDefault]; + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringNone + sizingOptions:ASCenterLayoutSpecSizingOptionMinimumX]; + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringNone + sizingOptions:ASCenterLayoutSpecSizingOptionMinimumY]; + [self testWithCenteringOptions:ASCenterLayoutSpecCenteringNone + sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY]; +} + +- (void)testWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)options + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], CGSizeMake(70, 100)); + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASCenterLayoutSpec + centerLayoutSpecWithCenteringOptions:options + sizingOptions:sizingOptions + child:foregroundNode] + background:backgroundNode]; + + [self testLayoutSpec:layoutSpec + sizeRange:kSize + subnodes:@[backgroundNode, foregroundNode] + identifier:suffixForCenteringOptions(options, sizingOptions)]; +} + +static NSString *suffixForCenteringOptions(ASCenterLayoutSpecCenteringOptions centeringOptions, + ASCenterLayoutSpecSizingOptions sizingOptinos) +{ + NSMutableString *suffix = [NSMutableString string]; + + if ((centeringOptions & ASCenterLayoutSpecCenteringX) != 0) { + [suffix appendString:@"CenteringX"]; + } + + if ((centeringOptions & ASCenterLayoutSpecCenteringY) != 0) { + [suffix appendString:@"CenteringY"]; + } + + if ((sizingOptinos & ASCenterLayoutSpecSizingOptionMinimumX) != 0) { + [suffix appendString:@"SizingMinimumX"]; + } + + if ((sizingOptinos & ASCenterLayoutSpecSizingOptionMinimumY) != 0) { + [suffix appendString:@"SizingMinimumY"]; + } + + return suffix; +} + +- (void)testMinimumSizeRangeIsGivenToChildWhenNotCentering +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor], CGSizeMake(10, 10)); + foregroundNode.style.flexGrow = 1; + + ASCenterLayoutSpec *layoutSpec = + [ASCenterLayoutSpec + centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringNone + sizingOptions:{} + child: + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[foregroundNode]] + background:backgroundNode]]; + + [self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:@[backgroundNode, foregroundNode] identifier:nil]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCollectionModernDataSourceTests.mm b/submodules/AsyncDisplayKit/Tests/ASCollectionModernDataSourceTests.mm new file mode 100644 index 0000000000..5d0a77899a --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCollectionModernDataSourceTests.mm @@ -0,0 +1,363 @@ +// +// ASCollectionModernDataSourceTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import "OCMockObject+ASAdditions.h" +#import "ASTestCase.h" + +@interface ASCollectionModernDataSourceTests : ASTestCase +@end + +@interface ASTestCellNode : ASCellNode +@end + +@interface ASTestSection : NSObject +@property (nonatomic, readonly) NSMutableArray *nodeModels; +@end + +@implementation ASCollectionModernDataSourceTests { +@private + id mockDataSource; + UIWindow *window; + UIViewController *viewController; + ASCollectionNode *collectionNode; + NSMutableArray *sections; +} + +- (void)setUp { + [super setUp]; + // Default is 2 sections: 2 items in first, 1 item in second. + sections = [NSMutableArray array]; + [sections addObject:[ASTestSection new]]; + [sections[0].nodeModels addObject:[NSObject new]]; + [sections[0].nodeModels addObject:[NSObject new]]; + [sections addObject:[ASTestSection new]]; + [sections[1].nodeModels addObject:[NSObject new]]; + window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + viewController = [[UIViewController alloc] init]; + + window.rootViewController = viewController; + [window makeKeyAndVisible]; + collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:[UICollectionViewFlowLayout new]]; + collectionNode.frame = viewController.view.bounds; + collectionNode.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [viewController.view addSubnode:collectionNode]; + + mockDataSource = OCMStrictProtocolMock(@protocol(ASCollectionDataSource)); + [mockDataSource addImplementedOptionalProtocolMethods: + @selector(numberOfSectionsInCollectionNode:), + @selector(collectionNode:numberOfItemsInSection:), + @selector(collectionNode:nodeBlockForItemAtIndexPath:), + @selector(collectionNode:nodeModelForItemAtIndexPath:), + @selector(collectionNode:contextForSection:), + nil]; + [mockDataSource setExpectationOrderMatters:YES]; + + // NOTE: Adding optionally-implemented methods after this point won't work due to ASCollectionNode selector caching. + collectionNode.dataSource = mockDataSource; +} + +- (void)tearDown +{ + [collectionNode waitUntilAllUpdatesAreProcessed]; + [super tearDown]; +} + +#pragma mark - Test Methods + +- (void)testInitialDataLoading +{ + [self loadInitialData]; +} + +- (void)testReloadingAnItem +{ + [self loadInitialData]; + + // Reload at (0, 0) + NSIndexPath *reloadedPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + [self performUpdateReloadingSections:nil + reloadingItems:@{ reloadedPath: [NSObject new] } + reloadMappings:@{ reloadedPath: reloadedPath } + insertingItems:nil + deletingItems:nil + skippedReloadIndexPaths:nil]; +} + +- (void)testInsertingAnItem +{ + [self loadInitialData]; + + // Insert at (1, 0) + NSIndexPath *insertedPath = [NSIndexPath indexPathForItem:0 inSection:1]; + + [self performUpdateReloadingSections:nil + reloadingItems:nil + reloadMappings:nil + insertingItems:@{ insertedPath: [NSObject new] } + deletingItems:nil + skippedReloadIndexPaths:nil]; +} + +- (void)testReloadingAnItemWithACompatibleNodeModel +{ + [self loadInitialData]; + + // Reload and delete together, for good measure. + NSIndexPath *reloadedPath = [NSIndexPath indexPathForItem:1 inSection:0]; + NSIndexPath *deletedPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + id nodeModel = [NSObject new]; + + // Cell node should get -canUpdateToNodeModel: + id mockCellNode = [collectionNode nodeForItemAtIndexPath:reloadedPath]; + OCMExpect([mockCellNode canUpdateToNodeModel:nodeModel]) + .andReturn(YES); + + [self performUpdateReloadingSections:nil + reloadingItems:@{ reloadedPath: nodeModel } + reloadMappings:@{ reloadedPath: [NSIndexPath indexPathForItem:0 inSection:0] } + insertingItems:nil + deletingItems:@[ deletedPath ] + skippedReloadIndexPaths:@[ reloadedPath ]]; +} + +- (void)testReloadingASection +{ + [self loadInitialData]; + + [self performUpdateReloadingSections:@{ @0: [ASTestSection new] } + reloadingItems:nil + reloadMappings:nil + insertingItems:nil + deletingItems:nil + skippedReloadIndexPaths:nil]; +} + +#pragma mark - Helpers + +- (void)loadInitialData +{ + // Count methods are called twice in a row for first data load. + // Since -reloadData is routed through our batch update system, + // the batch update latches the "old data source counts" if needed at -beginUpdates time + // and then verifies them against the "new data source counts" after the updates. + // This isn't ideal, but the cost is very small and the system works well. + for (int i = 0; i < 2; i++) { + // It reads all the counts + [self expectDataSourceCountMethods]; + } + + // It reads each section object. + for (NSInteger section = 0; section < sections.count; section++) { + [self expectContextMethodForSection:section]; + } + + // It reads the contents for each item. + for (NSInteger section = 0; section < sections.count; section++) { + NSArray *nodeModels = sections[section].nodeModels; + + // For each item: + for (NSInteger i = 0; i < nodeModels.count; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; + [self expectNodeModelMethodForItemAtIndexPath:indexPath nodeModel:nodeModels[i]]; + [self expectNodeBlockMethodForItemAtIndexPath:indexPath]; + } + } + + [window layoutIfNeeded]; + + // Assert item counts & content: + [self assertCollectionNodeContent]; +} + +/** + * Adds expectations for the sequence: + * + * numberOfSectionsInCollectionNode: + * for section in countsArray + * numberOfItemsInSection: + */ +- (void)expectDataSourceCountMethods +{ + // -numberOfSectionsInCollectionNode + OCMExpect([mockDataSource numberOfSectionsInCollectionNode:collectionNode]) + .andReturn(sections.count); + + // For each section: + // Note: Skip fast enumeration for readability. + for (NSInteger section = 0; section < sections.count; section++) { + OCMExpect([mockDataSource collectionNode:collectionNode numberOfItemsInSection:section]) + .andReturn(sections[section].nodeModels.count); + } +} + +- (void)expectNodeModelMethodForItemAtIndexPath:(NSIndexPath *)indexPath nodeModel:(id)nodeModel +{ + OCMExpect([mockDataSource collectionNode:collectionNode nodeModelForItemAtIndexPath:indexPath]) + .andReturn(nodeModel); +} + +- (void)expectContextMethodForSection:(NSInteger)section +{ + OCMExpect([mockDataSource collectionNode:collectionNode contextForSection:section]) + .andReturn(sections[section]); +} + +- (void)expectNodeBlockMethodForItemAtIndexPath:(NSIndexPath *)indexPath +{ + OCMExpect([mockDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]) + .andReturn((ASCellNodeBlock)^{ + ASCellNode *node = [ASTestCellNode new]; + // Generating multiple partial mocks of the same class is not thread-safe. + id mockNode; + @synchronized (NSNull.null) { + mockNode = OCMPartialMock(node); + } + [mockNode setExpectationOrderMatters:YES]; + return mockNode; + }); +} + +/// Asserts that counts match and all view-models are up-to-date between us and collectionNode. +- (void)assertCollectionNodeContent +{ + // Assert section count + XCTAssertEqual(collectionNode.numberOfSections, sections.count); + + for (NSInteger section = 0; section < sections.count; section++) { + ASTestSection *sectionObject = sections[section]; + NSArray *nodeModels = sectionObject.nodeModels; + + // Assert section object + XCTAssertEqualObjects([collectionNode contextForSection:section], sectionObject); + + // Assert item count + XCTAssertEqual([collectionNode numberOfItemsInSection:section], nodeModels.count); + for (NSInteger item = 0; item < nodeModels.count; item++) { + // Assert node model + // Could use pointer equality but the error message is less readable. + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; + id nodeModel = nodeModels[indexPath.item]; + XCTAssertEqualObjects(nodeModel, [collectionNode nodeModelForItemAtIndexPath:indexPath]); + ASCellNode *node = [collectionNode nodeForItemAtIndexPath:indexPath]; + XCTAssertEqualObjects(node.nodeModel, nodeModel); + } + } +} + +/** + * Updates the collection node, with expectations and assertions about the call-order and the correctness of the + * new data. You should update the data source _before_ calling this method. + * + * skippedReloadIndexPaths are the old index paths for nodes that should use -canUpdateToNodeModel: instead of being refetched. + */ +- (void)performUpdateReloadingSections:(NSDictionary *)reloadedSections + reloadingItems:(NSDictionary *)reloadedItems + reloadMappings:(NSDictionary *)reloadMappings + insertingItems:(NSDictionary *)insertedItems + deletingItems:(NSArray *)deletedItems + skippedReloadIndexPaths:(NSArray *)skippedReloadIndexPaths +{ + [collectionNode performBatchUpdates:^{ + // First update our data source. + [reloadedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + sections[key.section].nodeModels[key.item] = obj; + }]; + [reloadedSections enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + sections[key.integerValue] = obj; + }]; + + // Deletion paths, sorted descending + for (NSIndexPath *indexPath in [deletedItems sortedArrayUsingSelector:@selector(compare:)].reverseObjectEnumerator) { + [sections[indexPath.section].nodeModels removeObjectAtIndex:indexPath.item]; + } + + // Insertion paths, sorted ascending. + NSArray *insertionsSortedAcending = [insertedItems.allKeys sortedArrayUsingSelector:@selector(compare:)]; + for (NSIndexPath *indexPath in insertionsSortedAcending) { + [sections[indexPath.section].nodeModels insertObject:insertedItems[indexPath] atIndex:indexPath.item]; + } + + // Then update the collection node. + NSMutableIndexSet *reloadedSectionIndexes = [NSMutableIndexSet indexSet]; + for (NSNumber *i in reloadedSections) { + [reloadedSectionIndexes addIndex:i.integerValue]; + } + [collectionNode reloadSections:reloadedSectionIndexes]; + [collectionNode reloadItemsAtIndexPaths:reloadedItems.allKeys]; + [collectionNode deleteItemsAtIndexPaths:deletedItems]; + [collectionNode insertItemsAtIndexPaths:insertedItems.allKeys]; + + // Before the commit, lay out our expectations. + + // Expect it to load the new counts. + [self expectDataSourceCountMethods]; + + // Combine reloads + inserts and expect them to load content for all of them, in ascending order. + NSMutableDictionary *insertsPlusReloads = [[NSMutableDictionary alloc] initWithDictionary:insertedItems]; + + // Go through reloaded sections and add all their items into `insertsPlusReloads` + [reloadedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + [self expectContextMethodForSection:section]; + NSArray *nodeModels = sections[section].nodeModels; + for (NSInteger i = 0; i < nodeModels.count; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; + insertsPlusReloads[indexPath] = nodeModels[i]; + } + }]; + + [reloadedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + insertsPlusReloads[reloadMappings[key]] = obj; + }]; + + for (NSIndexPath *indexPath in [insertsPlusReloads.allKeys sortedArrayUsingSelector:@selector(compare:)]) { + [self expectNodeModelMethodForItemAtIndexPath:indexPath nodeModel:insertsPlusReloads[indexPath]]; + NSIndexPath *oldIndexPath = [reloadMappings allKeysForObject:indexPath].firstObject; + BOOL isSkippedReload = oldIndexPath && [skippedReloadIndexPaths containsObject:oldIndexPath]; + if (!isSkippedReload) { + [self expectNodeBlockMethodForItemAtIndexPath:indexPath]; + } + } + } completion:nil]; + + // Assert that the counts and node models are all correct now. + [self assertCollectionNodeContent]; +} + +@end + +#pragma mark - Other Objects + +@implementation ASTestCellNode + +- (BOOL)canUpdateToNodeModel:(id)nodeModel +{ + // Our tests default to NO for migrating node models. We use OCMExpect to return YES when we specifically want to. + return NO; +} + +@end + +@implementation ASTestSection +@synthesize collectionView; +@synthesize sectionName; + +- (instancetype)init +{ + if (self = [super init]) { + _nodeModels = [NSMutableArray array]; + } + return self; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCollectionViewFlowLayoutInspectorTests.mm b/submodules/AsyncDisplayKit/Tests/ASCollectionViewFlowLayoutInspectorTests.mm new file mode 100644 index 0000000000..555fe21134 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCollectionViewFlowLayoutInspectorTests.mm @@ -0,0 +1,428 @@ +// +// ASCollectionViewFlowLayoutInspectorTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import "ASXCTExtensions.h" + +#import +#import +#import +#import +#import + +@interface ASCollectionView (Private) + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout; + +@end + +/** + * Test Data Source + */ +@interface InspectorTestDataSource : NSObject +@end + +@implementation InspectorTestDataSource + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [[ASCellNode alloc] init]; +} + +- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ return [[ASCellNode alloc] init]; }; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return 0; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 2; +} + +@end + +@protocol InspectorTestDataSourceDelegateProtocol + +@end + +@interface InspectorTestDataSourceDelegateWithoutNodeConstrainedSize : NSObject +@end + +@implementation InspectorTestDataSourceDelegateWithoutNodeConstrainedSize + +- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ return [[ASCellNode alloc] init]; }; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return 0; +} + +@end + +@interface ASCollectionViewFlowLayoutInspectorTests : XCTestCase + +@end + +/** + * Test Delegate for Header Reference Size Implementation + */ +@interface HeaderReferenceSizeTestDelegate : NSObject + +@end + +@implementation HeaderReferenceSizeTestDelegate + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section +{ + return CGSizeMake(125.0, 125.0); +} + +@end + +/** + * Test Delegate for Footer Reference Size Implementation + */ +@interface FooterReferenceSizeTestDelegate : NSObject + +@end + +@implementation FooterReferenceSizeTestDelegate + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section +{ + return CGSizeMake(125.0, 125.0); +} + +@end + +@implementation ASCollectionViewFlowLayoutInspectorTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +#pragma mark - #collectionView:constrainedSizeForSupplementaryNodeOfKind:atIndexPath: + +// Vertical + +// Delegate implementation + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheHeaderDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(collectionView.bounds.size.width, 125.0)); + + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheFooterDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + FooterReferenceSizeTestDelegate *delegate = [[FooterReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(collectionView.bounds.size.width, 125.0)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Size implementation + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheHeaderProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.headerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(collectionView.bounds.size.width, 125.0)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheFooterProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(collectionView.bounds.size.width, 125.0)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Horizontal + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheHeaderDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(125.0, collectionView.bounds.size.height)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheFooterDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + FooterReferenceSizeTestDelegate *delegate = [[FooterReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(125.0, collectionView.bounds.size.height)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Size implementation + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheHeaderProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.headerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(125.0, collectionView.bounds.size.width)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheFooterProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeMake(125.0, collectionView.bounds.size.height)); + ASXCTAssertEqualSizeRanges(size, sizeCompare, @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsZeroSizeWhenNoReferenceSizeIsImplemented +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeZero); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a zero size"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +#pragma mark - #collectionView:supplementaryNodesOfKind:inSection: + +- (void)testThatItReturnsOneWhenAValidSizeIsImplementedOnTheDelegate +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionHeader inSection:0]; + XCTAssert(count == 1, @"should have a header supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsOneWhenAValidSizeIsImplementedOnTheLayout +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionFooter inSection:0]; + XCTAssert(count == 1, @"should have a footer supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsNoneWhenNoReferenceSizeIsImplemented +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = ASDynamicCast(collectionView.layoutInspector, ASCollectionViewFlowLayoutInspector); + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionFooter inSection:0]; + XCTAssert(count == 0, @"should not have a footer supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItThrowsIfNodeConstrainedSizeIsImplementedOnDataSourceButNotOnDelegateLayoutInspector +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionNode *node = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + ASCollectionView *collectionView = node.view; + + id dataSourceAndDelegate = [OCMockObject mockForProtocol:@protocol(InspectorTestDataSourceDelegateProtocol)]; + ASSizeRange constrainedSize = ASSizeRangeMake(CGSizeZero, CGSizeZero); + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + NSValue *value = [NSValue value:&constrainedSize withObjCType:@encode(ASSizeRange)]; + [[[dataSourceAndDelegate stub] andReturnValue:value] collectionNode:node constrainedSizeForItemAtIndexPath:indexPath]; + node.dataSource = dataSourceAndDelegate; + + id delegate = [InspectorTestDataSourceDelegateWithoutNodeConstrainedSize new]; + node.delegate = delegate; + + ASCollectionViewLayoutInspector *inspector = [[ASCollectionViewLayoutInspector alloc] init]; + + collectionView.layoutInspector = inspector; + XCTAssertThrows([inspector collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]); + + node.delegate = dataSourceAndDelegate; + XCTAssertNoThrow([inspector collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]); +} + +- (void)testThatItThrowsIfNodeConstrainedSizeIsImplementedOnDataSourceButNotOnDelegateFlowLayoutInspector +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + + ASCollectionNode *node = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + ASCollectionView *collectionView = node.view; + id dataSourceAndDelegate = [OCMockObject mockForProtocol:@protocol(InspectorTestDataSourceDelegateProtocol)]; + ASSizeRange constrainedSize = ASSizeRangeMake(CGSizeZero, CGSizeZero); + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + NSValue *value = [NSValue value:&constrainedSize withObjCType:@encode(ASSizeRange)]; + + [[[dataSourceAndDelegate stub] andReturnValue:value] collectionNode:node constrainedSizeForItemAtIndexPath:indexPath]; + node.dataSource = dataSourceAndDelegate; + id delegate = [InspectorTestDataSourceDelegateWithoutNodeConstrainedSize new]; + + node.delegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = collectionView.layoutInspector; + + XCTAssertThrows([inspector collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]); + + node.delegate = dataSourceAndDelegate; + XCTAssertNoThrow([inspector collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCollectionViewTests.mm b/submodules/AsyncDisplayKit/Tests/ASCollectionViewTests.mm new file mode 100644 index 0000000000..3ff5af4409 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCollectionViewTests.mm @@ -0,0 +1,1173 @@ +// +// ASCollectionViewTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "ASDisplayNodeTestsHelper.h" + +@interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode + +@property (nonatomic) NSUInteger setSelectedCounter; +@property (nonatomic) NSUInteger applyLayoutAttributesCount; + +@end + +@implementation ASTextCellNodeWithSetSelectedCounter + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + _setSelectedCounter++; +} + +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + _applyLayoutAttributesCount++; +} + +@end + +@interface ASTestSectionContext : NSObject + +@property (nonatomic) NSInteger sectionIndex; +@property (nonatomic) NSInteger sectionGeneration; + +@end + +@implementation ASTestSectionContext + +@synthesize sectionName = _sectionName, collectionView = _collectionView; + +@end + +@interface ASCollectionViewTestDelegate : NSObject + +@property (nonatomic) NSInteger sectionGeneration; +@property (nonatomic) void(^willBeginBatchFetch)(ASBatchContext *); + +@end + +@implementation ASCollectionViewTestDelegate { + @package + std::vector _itemCounts; +} + +- (id)initWithNumberOfSections:(NSInteger)numberOfSections numberOfItemsInSection:(NSInteger)numberOfItemsInSection { + if (self = [super init]) { + for (NSInteger i = 0; i < numberOfSections; i++) { + _itemCounts.push_back(numberOfItemsInSection); + } + _sectionGeneration = 1; + } + + return self; +} + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath { + ASTextCellNodeWithSetSelectedCounter *textCellNode = [ASTextCellNodeWithSetSelectedCounter new]; + textCellNode.text = indexPath.description; + + return textCellNode; +} + + +- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { + return ^{ + ASTextCellNodeWithSetSelectedCounter *textCellNode = [ASTextCellNodeWithSetSelectedCounter new]; + textCellNode.text = indexPath.description; + return textCellNode; + }; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return _itemCounts.size(); +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return _itemCounts[section]; +} + +- (id)collectionNode:(ASCollectionNode *)collectionNode contextForSection:(NSInteger)section +{ + ASTestSectionContext *context = [[ASTestSectionContext alloc] init]; + context.sectionGeneration = _sectionGeneration; + context.sectionIndex = section; + return context; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section +{ + return CGSizeMake(100, 100); +} + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return [[ASTextCellNodeWithSetSelectedCounter alloc] init]; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + if (_willBeginBatchFetch != nil) { + _willBeginBatchFetch(context); + } else { + [context cancelBatchFetching]; + } +} + +@end + +@interface ASCollectionViewTestController: UIViewController + +@property (nonatomic) ASCollectionViewTestDelegate *asyncDelegate; +@property (nonatomic) ASCollectionView *collectionView; +@property (nonatomic) ASCollectionNode *collectionNode; + +@end + +@implementation ASCollectionViewTestController + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Populate these immediately so that they're not unexpectedly nil during tests. + self.asyncDelegate = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:10 numberOfItemsInSection:10]; + id realLayout = [UICollectionViewFlowLayout new]; + id mockLayout = [OCMockObject partialMockForObject:realLayout]; + self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:mockLayout]; + self.collectionView = self.collectionNode.view; + self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.collectionNode.dataSource = self.asyncDelegate; + self.collectionNode.delegate = self.asyncDelegate; + + [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [self.view addSubview:self.collectionView]; + } + return self; +} + +@end + +@interface ASCollectionView (InternalTesting) + +- (NSArray *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections; + +@end + +@interface ASCollectionViewTests : XCTestCase + +@end + +@implementation ASCollectionViewTests + +- (void)tearDown +{ + // We can't prevent the system from retaining windows, but we can at least clear them out to avoid + // pollution between test cases. + for (UIWindow *window in [UIApplication sharedApplication].windows) { + for (UIView *subview in window.subviews) { + [subview removeFromSuperview]; + } + } + [super tearDown]; +} + +- (void)testDataSourceImplementsNecessaryMethods +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + + id dataSource = [NSObject new]; + XCTAssertThrows((collectionView.asyncDataSource = dataSource)); + + dataSource = [OCMockObject niceMockForProtocol:@protocol(ASCollectionDataSource)]; + XCTAssertNoThrow((collectionView.asyncDataSource = dataSource)); +} + +- (void)testThatItSetsALayoutInspectorForFlowLayouts +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + XCTAssert(collectionView.layoutInspector != nil, @"should automatically set a layout delegate for flow layouts"); + XCTAssert([collectionView.layoutInspector isKindOfClass:[ASCollectionViewFlowLayoutInspector class]], @"should have a flow layout inspector by default"); +} + +- (void)testThatADefaultLayoutInspectorIsProvidedForCustomLayouts +{ + UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + XCTAssert(collectionView.layoutInspector != nil, @"should automatically set a layout delegate for flow layouts"); + XCTAssert([collectionView.layoutInspector isKindOfClass:[ASCollectionViewLayoutInspector class]], @"should have a default layout inspector by default"); +} + +- (void)testThatRegisteringASupplementaryNodeStoresItForIntrospection +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + [collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssertEqualObjects([collectionView dataController:nil supplementaryNodeKindsInSections:[NSIndexSet indexSetWithIndex:0]], @[UICollectionElementKindSectionHeader]); +} + +- (void)testReloadIfNeeded +{ + __block ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + __block ASCollectionViewTestDelegate *del = testController.asyncDelegate; + __block ASCollectionNode *cn = testController.collectionNode; + + void (^reset)() = ^void() { + testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + del = testController.asyncDelegate; + cn = testController.collectionNode; + }; + + // Check if the number of sections matches the data source + XCTAssertEqual(cn.numberOfSections, del->_itemCounts.size(), @"Section count doesn't match the data source"); + + // Reset everything and then check if numberOfItemsInSection matches the data source + reset(); + XCTAssertEqual([cn numberOfItemsInSection:0], del->_itemCounts[0], @"Number of items in Section doesn't match the data source"); + + // Reset and check if we can get the node corresponding to a specific indexPath + reset(); + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + ASTextCellNodeWithSetSelectedCounter *node = (ASTextCellNodeWithSetSelectedCounter*)[cn nodeForItemAtIndexPath:indexPath]; + XCTAssertTrue([node.text isEqualToString:indexPath.description], @"Node's text should match the initial text it was created with"); +} + +- (void)testSelection +{ + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + [window setRootViewController:testController]; + [window makeKeyAndVisible]; + + [testController.collectionNode reloadData]; + [testController.collectionNode waitUntilAllUpdatesAreProcessed]; + [testController.collectionView layoutIfNeeded]; + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + ASCellNode *node = [testController.collectionView nodeForItemAtIndexPath:indexPath]; + + NSInteger setSelectedCount = 0; + // selecting node should select cell + node.selected = YES; + ++setSelectedCount; + XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath], @"Selecting node should update cell selection."); + + // deselecting node should deselect cell + node.selected = NO; + ++setSelectedCount; + XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] isEqualToArray:@[]], @"Deselecting node should update cell selection."); + + // selecting cell via collectionNode should select node + ++setSelectedCount; + [testController.collectionNode selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; + XCTAssertTrue(node.isSelected == YES, @"Selecting cell should update node selection."); + + // deselecting cell via collectionNode should deselect node + ++setSelectedCount; + [testController.collectionNode deselectItemAtIndexPath:indexPath animated:NO]; + XCTAssertTrue(node.isSelected == NO, @"Deselecting cell should update node selection."); + + // select the cell again, scroll down and back up, and check that the state persisted + [testController.collectionNode selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; + ++setSelectedCount; + XCTAssertTrue(node.isSelected == YES, @"Selecting cell should update node selection."); + + testController.collectionNode.allowsMultipleSelection = YES; + + NSIndexPath *indexPath2 = [NSIndexPath indexPathForItem:1 inSection:0]; + ASCellNode *node2 = [testController.collectionView nodeForItemAtIndexPath:indexPath2]; + + // selecting cell via collectionNode should select node + [testController.collectionNode selectItemAtIndexPath:indexPath2 animated:NO scrollPosition:UICollectionViewScrollPositionNone]; + XCTAssertTrue(node2.isSelected == YES, @"Selecting cell should update node selection."); + + XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath] && + [[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath2], + @"Selecting multiple cells should result in those cells being in the array of selectedItems."); + + // deselecting node should deselect cell + node.selected = NO; + ++setSelectedCount; + XCTAssertTrue(![[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath] && + [[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath2], @"Deselecting node should update array of selectedItems."); + + node.selected = YES; + ++setSelectedCount; + XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath], @"Selecting node should update cell selection."); + + node2.selected = NO; + XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath] && + ![[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath2], @"Deselecting node should update array of selectedItems."); + + // reload cell (-prepareForReuse is called) & check that selected state is preserved + [testController.collectionView setContentOffset:CGPointMake(0,testController.collectionView.bounds.size.height)]; + [testController.collectionView layoutIfNeeded]; + [testController.collectionView setContentOffset:CGPointMake(0,0)]; + [testController.collectionView layoutIfNeeded]; + XCTAssertTrue(node.isSelected == YES, @"Reloaded cell should preserve state."); + + // deselecting cell should deselect node + UICollectionViewCell *cell = [testController.collectionView cellForItemAtIndexPath:indexPath]; + cell.selected = NO; + XCTAssertTrue(node.isSelected == NO, @"Deselecting cell should update node selection."); + + // check setSelected not called extra times + XCTAssertTrue([(ASTextCellNodeWithSetSelectedCounter *)node setSelectedCounter] == (setSelectedCount + 1), @"setSelected: should not be called on node multiple times."); +} + +- (void)testTuningParametersWithExplicitRangeMode +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + + ASRangeTuningParameters minimumRenderParams = { .leadingBufferScreenfuls = 0.1, .trailingBufferScreenfuls = 0.1 }; + ASRangeTuningParameters minimumPreloadParams = { .leadingBufferScreenfuls = 0.1, .trailingBufferScreenfuls = 0.1 }; + ASRangeTuningParameters fullRenderParams = { .leadingBufferScreenfuls = 0.5, .trailingBufferScreenfuls = 0.5 }; + ASRangeTuningParameters fullPreloadParams = { .leadingBufferScreenfuls = 1, .trailingBufferScreenfuls = 0.5 }; + + [collectionNode setTuningParameters:minimumRenderParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay]; + [collectionNode setTuningParameters:minimumPreloadParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload]; + [collectionNode setTuningParameters:fullRenderParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay]; + [collectionNode setTuningParameters:fullPreloadParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload]; + + XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(minimumRenderParams, + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay])); + XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(minimumPreloadParams, + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload])); + XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(fullRenderParams, + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay])); + XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(fullPreloadParams, + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload])); +} + +- (void)testTuningParameters +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + + ASRangeTuningParameters renderParams = { .leadingBufferScreenfuls = 1.2, .trailingBufferScreenfuls = 3.2 }; + ASRangeTuningParameters preloadParams = { .leadingBufferScreenfuls = 4.3, .trailingBufferScreenfuls = 2.3 }; + + [collectionView setTuningParameters:renderParams forRangeType:ASLayoutRangeTypeDisplay]; + [collectionView setTuningParameters:preloadParams forRangeType:ASLayoutRangeTypePreload]; + + XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(renderParams, [collectionView tuningParametersForRangeType:ASLayoutRangeTypeDisplay])); + XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(preloadParams, [collectionView tuningParametersForRangeType:ASLayoutRangeTypePreload])); +} + +// Informations to test: https://github.com/TextureGroup/Texture/issues/1094 +- (void)testThatCollectionNodeCanHandleNilRangeController +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + [collectionNode recursivelySetInterfaceState:ASInterfaceStateDisplay]; + [collectionNode setHierarchyState:ASHierarchyStateRangeManaged]; + [collectionNode recursivelySetInterfaceState:ASInterfaceStateNone]; + ASCATransactionQueueWait(nil); +} + +/** + * This may seem silly, but we had issues where the runtime sometimes wouldn't correctly report + * conformances declared on categories. + */ +- (void)testThatCollectionNodeConformsToExpectedProtocols +{ + ASCollectionNode *node = [[ASCollectionNode alloc] initWithFrame:CGRectZero collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]]; + XCTAssert([node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]); +} + +#pragma mark - Update Validations + +#define updateValidationTestPrologue \ + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];\ + __unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\ + __unused ASCollectionView *cv = testController.collectionView;\ + ASCollectionNode *cn = testController.collectionNode;\ + UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\ + [window makeKeyAndVisible]; \ + window.rootViewController = testController;\ + \ + [cn reloadData];\ + [cn waitUntilAllUpdatesAreProcessed]; \ + [testController.collectionView layoutIfNeeded]; + +- (void)testThatSubmittingAValidInsertDoesNotThrowAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + del->_itemCounts[sectionCount - 1]++; + XCTAssertNoThrow([cv insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount - 1] ]]); +} + +- (void)testThatSubmittingAValidReloadDoesNotThrowAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + XCTAssertNoThrow([cv reloadItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount - 1] ]]); +} + +- (void)testThatSubmittingAnInvalidInsertThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + XCTAssertThrows([cv insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount + 1] ]]); +} + +- (void)testThatSubmittingAnInvalidDeleteThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + XCTAssertThrows([cv deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount + 1] ]]); +} + +- (void)testThatDeletingAndReloadingTheSameItemThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv performBatchUpdates:^{ + NSArray *indexPaths = @[ [NSIndexPath indexPathForItem:0 inSection:0] ]; + [cv deleteItemsAtIndexPaths:indexPaths]; + [cv reloadItemsAtIndexPaths:indexPaths]; + } completion:nil]); +} + +- (void)testThatHavingAnIncorrectSectionCountThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv deleteSections:[NSIndexSet indexSetWithIndex:0]]); +} + +- (void)testThatHavingAnIncorrectItemCountThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:0] ]]); +} + +- (void)testThatHavingAnIncorrectItemCountWithNoUpdatesThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv performBatchUpdates:^{ + del->_itemCounts[0]++; + } completion:nil]); +} + +- (void)testThatInsertingAnInvalidSectionThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + del->_itemCounts.push_back(10); + XCTAssertThrows([cv performBatchUpdates:^{ + [cv insertSections:[NSIndexSet indexSetWithIndex:sectionCount + 1]]; + } completion:nil]); +} + +- (void)testThatDeletingAndReloadingASectionThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + del->_itemCounts.pop_back(); + XCTAssertThrows([cv performBatchUpdates:^{ + NSIndexSet *sections = [NSIndexSet indexSetWithIndex:sectionCount - 1]; + [cv reloadSections:sections]; + [cv deleteSections:sections]; + } completion:nil]); +} + +- (void)testCellNodeLayoutAttributes +{ + updateValidationTestPrologue + NSSet *nodeBatch1 = [NSSet setWithArray:[cn visibleNodes]]; + XCTAssertGreaterThan(nodeBatch1.count, 0); + + NSArray *visibleLayoutAttributesBatch1 = [cv.collectionViewLayout layoutAttributesForElementsInRect:cv.bounds]; + XCTAssertGreaterThan(visibleLayoutAttributesBatch1.count, 0); + + // Expect all visible nodes get 1 applyLayoutAttributes and have a non-nil value. + for (ASTextCellNodeWithSetSelectedCounter *node in nodeBatch1) { + XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible nodes."); + XCTAssertNotNil(node.layoutAttributes, @"Expected layoutAttributes to be non-nil for visible cell node."); + } + + for (UICollectionViewLayoutAttributes *layoutAttributes in visibleLayoutAttributesBatch1) { + if (layoutAttributes.representedElementCategory != UICollectionElementCategorySupplementaryView) { + continue; + } + ASTextCellNodeWithSetSelectedCounter *node = (ASTextCellNodeWithSetSelectedCounter *)[cv supplementaryNodeForElementKind:layoutAttributes.representedElementKind atIndexPath:layoutAttributes.indexPath]; + XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible supplementary nodes."); + XCTAssertNotNil(node.layoutAttributes, @"Expected layoutAttributes to be non-nil for visible supplementary node."); + } + + // Scroll to next batch of items. + NSIndexPath *nextIP = [NSIndexPath indexPathForItem:nodeBatch1.count inSection:0]; + [cv scrollToItemAtIndexPath:nextIP atScrollPosition:UICollectionViewScrollPositionTop animated:NO]; + [cv layoutIfNeeded]; + + // Ensure we scrolled far enough that all the old ones are offscreen. + NSSet *nodeBatch2 = [NSSet setWithArray:[cn visibleNodes]]; + XCTAssertFalse([nodeBatch1 intersectsSet:nodeBatch2], @"Expected to scroll far away enough that all nodes are replaced."); + + // Now the nodes are no longer visible, expect their layout attributes are nil but not another applyLayoutAttributes call. + for (ASTextCellNodeWithSetSelectedCounter *node in nodeBatch1) { + XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible nodes, even after node is removed."); + XCTAssertNil(node.layoutAttributes, @"Expected layoutAttributes to be nil for removed cell node."); + } + + for (UICollectionViewLayoutAttributes *layoutAttributes in visibleLayoutAttributesBatch1) { + if (layoutAttributes.representedElementCategory != UICollectionElementCategorySupplementaryView) { + continue; + } + ASTextCellNodeWithSetSelectedCounter *node = (ASTextCellNodeWithSetSelectedCounter *)[cv supplementaryNodeForElementKind:layoutAttributes.representedElementKind atIndexPath:layoutAttributes.indexPath]; + XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible supplementary nodes, even after node is removed."); + XCTAssertNil(node.layoutAttributes, @"Expected layoutAttributes to be nil for removed supplementary node."); + } +} + +- (void)testCellNodeIndexPathConsistency +{ + updateValidationTestPrologue + + // Test with a visible cell + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:2 inSection:0]; + ASCellNode *cell = [cn nodeForItemAtIndexPath:indexPath]; + + // Check if cell's indexPath corresponds to the indexPath being tested + XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == indexPath.item, @"Expected the cell's indexPath to be the same as the indexPath being tested."); + + // Remove an item prior to the cell's indexPath from the same section and check for indexPath consistency + --del->_itemCounts[indexPath.section]; + [cn deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:indexPath.section]]]; + XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == (indexPath.item - 1), @"Expected the cell's indexPath to be updated once a cell with a lower index is deleted."); + + // Remove the section that includes the indexPath and check if the cell's indexPath is now nil + del->_itemCounts.erase(del->_itemCounts.begin()); + [cn deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section]]; + XCTAssertNil(cell.indexPath, @"Expected the cell's indexPath to be nil once the section that contains the node is deleted."); + + // Run the same tests but with a non-displayed cell + indexPath = [NSIndexPath indexPathForItem:2 inSection:(del->_itemCounts.size() - 1)]; + cell = [cn nodeForItemAtIndexPath:indexPath]; + + // Check if cell's indexPath corresponds to the indexPath being tested + XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == indexPath.item, @"Expected the cell's indexPath to be the same as the indexPath in question."); + + // Remove an item prior to the cell's indexPath from the same section and check for indexPath consistency + --del->_itemCounts[indexPath.section]; + [cn deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:indexPath.section]]]; + XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == (indexPath.item - 1), @"Expected the cell's indexPath to be updated once a cell with a lower index is deleted."); + + // Remove the section that includes the indexPath and check if the cell's indexPath is now nil + del->_itemCounts.pop_back(); + [cn deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section]]; + XCTAssertNil(cell.indexPath, @"Expected the cell's indexPath to be nil once the section that contains the node is deleted."); +} + +/** + * https://github.com/facebook/AsyncDisplayKit/issues/2011 + * + * If this ever becomes a pain to maintain, drop it. The underlying issue is tested by testThatLayerBackedSubnodesAreMarkedInvisibleBeforeDeallocWhenSupernodesViewIsRemovedFromHierarchyWhileBeingRetained + */ +- (void)testThatDisappearingSupplementariesWithLayerBackedNodesDontFailAssert +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UICollectionViewLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionNode *cn = [[ASCollectionNode alloc] initWithFrame:window.bounds collectionViewLayout:layout]; + ASCollectionView *cv = cn.view; + + + __unused NSMutableSet *keepaliveNodes = [NSMutableSet set]; + id dataSource = [OCMockObject niceMockForProtocol:@protocol(ASCollectionDataSource)]; + static int nodeIdx = 0; + [[[dataSource stub] andDo:^(NSInvocation *invocation) { + __autoreleasing ASCellNode *suppNode = [[ASCellNode alloc] init]; + int thisNodeIdx = nodeIdx++; + suppNode.debugName = [NSString stringWithFormat:@"Cell #%d", thisNodeIdx]; + [keepaliveNodes addObject:suppNode]; + + ASDisplayNode *layerBacked = [[ASDisplayNode alloc] init]; + layerBacked.layerBacked = YES; + layerBacked.debugName = [NSString stringWithFormat:@"Subnode #%d", thisNodeIdx]; + [suppNode addSubnode:layerBacked]; + [invocation setReturnValue:&suppNode]; + }] collectionNode:cn nodeForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:OCMOCK_ANY]; + [[[dataSource stub] andReturnValue:[NSNumber numberWithInteger:1]] numberOfSectionsInCollectionView:cv]; + cv.asyncDataSource = dataSource; + + id delegate = [OCMockObject niceMockForProtocol:@protocol(UICollectionViewDelegateFlowLayout)]; + [[[delegate stub] andReturnValue:[NSValue valueWithCGSize:CGSizeMake(100, 100)]] collectionView:cv layout:OCMOCK_ANY referenceSizeForHeaderInSection:0]; + cv.asyncDelegate = delegate; + + [cv registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [window addSubview:cv]; + + [window makeKeyAndVisible]; + + for (NSInteger i = 0; i < 2; i++) { + // NOTE: reloadData and waitUntilAllUpdatesAreProcessed are not sufficient here!! + XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]]; + [cn reloadDataWithCompletion:^{ + [done fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + } + +} + +- (void)testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation +{ + updateValidationTestPrologue + id layout = cv.collectionViewLayout; + CGSize initialItemSize = [cv nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].calculatedSize; + CGSize initialCVSize = cv.bounds.size; + + // Capture the node size before first call to prepareLayout after frame change. + __block CGSize itemSizeAtFirstLayout = CGSizeZero; + __block CGSize boundsSizeAtFirstLayout = CGSizeZero; + [[[[layout expect] andDo:^(NSInvocation *) { + itemSizeAtFirstLayout = [cv nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].calculatedSize; + boundsSizeAtFirstLayout = [cv bounds].size; + }] andForwardToRealObject] prepareLayout]; + + // Rotate the device + UIDeviceOrientation oldDeviceOrientation = [[UIDevice currentDevice] orientation]; + [[UIDevice currentDevice] setValue:@(UIDeviceOrientationLandscapeLeft) forKey:@"orientation"]; + + CGSize finalItemSize = [cv nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].calculatedSize; + CGSize finalCVSize = cv.bounds.size; + XCTAssertNotEqualObjects(NSStringFromCGSize(initialItemSize), NSStringFromCGSize(itemSizeAtFirstLayout)); + XCTAssertNotEqualObjects(NSStringFromCGSize(initialCVSize), NSStringFromCGSize(boundsSizeAtFirstLayout)); + XCTAssertEqualObjects(NSStringFromCGSize(itemSizeAtFirstLayout), NSStringFromCGSize(finalItemSize)); + XCTAssertEqualObjects(NSStringFromCGSize(boundsSizeAtFirstLayout), NSStringFromCGSize(finalCVSize)); + [layout verify]; + + // Teardown + [[UIDevice currentDevice] setValue:@(oldDeviceOrientation) forKey:@"orientation"]; +} + +/** + * See corresponding test in ASUICollectionViewTests + * + * @discussion Currently, we do not replicate UICollectionView's call order (outer, inner0, inner1, ...) + * and instead call (inner0, inner1, outer, ...). This is because we primarily provide a + * beginUpdates/endUpdatesWithCompletion: interface (like UITableView). With UICollectionView's + * performBatchUpdates:completion:, the completion block is enqueued at -beginUpdates time. + * With our tableView-like scheme, the completion block is provided at -endUpdates time + * and it is naturally enqueued at this time. It is assumed that this is an acceptable deviation, + * and that developers do not expect a particular completion order guarantee. + */ +- (void)testThatNestedBatchCompletionsAreCalledInOrder +{ + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + + ASCollectionView *cv = testController.collectionView; + + XCTestExpectation *inner0 = [self expectationWithDescription:@"Inner completion 0 is called"]; + XCTestExpectation *inner1 = [self expectationWithDescription:@"Inner completion 1 is called"]; + XCTestExpectation *outer = [self expectationWithDescription:@"Outer completion is called"]; + + NSMutableArray *completions = [NSMutableArray array]; + + [cv performBatchUpdates:^{ + [cv performBatchUpdates:^{ + + } completion:^(BOOL finished) { + [completions addObject:inner0]; + [inner0 fulfill]; + }]; + [cv performBatchUpdates:^{ + + } completion:^(BOOL finished) { + [completions addObject:inner1]; + [inner1 fulfill]; + }]; + } completion:^(BOOL finished) { + [completions addObject:outer]; + [outer fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + XCTAssertEqualObjects(completions, (@[ inner0, inner1, outer ]), @"Expected completion order to be correct"); +} + +#pragma mark - ASSectionContext tests + +- (void)testThatSectionContextsAreCorrectAfterTheInitialLayout +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + for (NSInteger section = 0; section < sectionCount; section++) { + ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section]; + XCTAssertNotNil(context); + XCTAssertEqual(context.sectionGeneration, 1); + XCTAssertEqual(context.sectionIndex, section); + } +} + +- (void)testThatSectionContextsAreCorrectAfterSectionMove +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + NSInteger originalSection = sectionCount - 1; + NSInteger toSection = 0; + + del.sectionGeneration++; + [cv moveSection:originalSection toSection:toSection]; + [cv waitUntilAllUpdatesAreCommitted]; + + // Only test left moving + XCTAssertTrue(toSection < originalSection); + ASTestSectionContext *movedSectionContext = (ASTestSectionContext *)[cn contextForSection:toSection]; + XCTAssertNotNil(movedSectionContext); + // ASCollectionView currently splits a move operation to a pair of delete and insert ones. + // So this movedSectionContext is newly loaded and thus is second generation. + XCTAssertEqual(movedSectionContext.sectionGeneration, 2); + XCTAssertEqual(movedSectionContext.sectionIndex, toSection); + + for (NSInteger section = toSection + 1; section <= originalSection && section < sectionCount; section++) { + ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section]; + XCTAssertNotNil(context); + XCTAssertEqual(context.sectionGeneration, 1); + // This section context was shifted to the right + XCTAssertEqual(context.sectionIndex, (section - 1)); + } +} + +- (void)testThatSectionContextsAreCorrectAfterReloadData +{ + updateValidationTestPrologue + + del.sectionGeneration++; + [cn reloadData]; + [cn waitUntilAllUpdatesAreProcessed]; + + NSInteger sectionCount = del->_itemCounts.size(); + for (NSInteger section = 0; section < sectionCount; section++) { + ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section]; + XCTAssertNotNil(context); + XCTAssertEqual(context.sectionGeneration, 2); + XCTAssertEqual(context.sectionIndex, section); + } +} + +- (void)testThatSectionContextsAreCorrectAfterReloadASection +{ + updateValidationTestPrologue + NSInteger sectionToReload = 0; + + del.sectionGeneration++; + [cv reloadSections:[NSIndexSet indexSetWithIndex:sectionToReload]]; + [cv waitUntilAllUpdatesAreCommitted]; + + NSInteger sectionCount = del->_itemCounts.size(); + for (NSInteger section = 0; section < sectionCount; section++) { + ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section]; + XCTAssertNotNil(context); + XCTAssertEqual(context.sectionGeneration, section != sectionToReload ? 1 : 2); + XCTAssertEqual(context.sectionIndex, section); + } +} + +/// See the same test in ASUICollectionViewTests for the reference behavior. +- (void)testThatIssuingAnUpdateBeforeInitialReloadIsAcceptable +{ + ASCollectionViewTestDelegate *del = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:0 numberOfItemsInSection:0]; + ASCollectionView *cv = [[ASCollectionView alloc] initWithCollectionViewLayout:[UICollectionViewFlowLayout new]]; + cv.asyncDataSource = del; + cv.asyncDelegate = del; + + // Add a section to the data source + del->_itemCounts.push_back(0); + // Attempt to insert section into collection view. We ignore it to workaround + // the bug demonstrated by + // ASUICollectionViewTests.testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable + XCTAssertNoThrow([cv insertSections:[NSIndexSet indexSetWithIndex:0]]); +} + +- (void)testThatNodeAtIndexPathIsCorrectImmediatelyAfterSubmittingUpdate +{ + updateValidationTestPrologue + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + // Insert an item and assert nodeForItemAtIndexPath: immediately returns new node + ASCellNode *oldNode = [cn nodeForItemAtIndexPath:indexPath]; + XCTAssertNotNil(oldNode); + del->_itemCounts[0] += 1; + [cv insertItemsAtIndexPaths:@[ indexPath ]]; + ASCellNode *newNode = [cn nodeForItemAtIndexPath:indexPath]; + XCTAssertNotNil(newNode); + XCTAssertNotEqualObjects(oldNode, newNode); + + // Delete all sections and assert nodeForItemAtIndexPath: immediately returns nil + NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, del->_itemCounts.size())]; + del->_itemCounts.clear(); + [cv deleteSections:sections]; + XCTAssertNil([cn nodeForItemAtIndexPath:indexPath]); +} + +- (void)DISABLED_testThatSupplementaryNodeAtIndexPathIsCorrectImmediatelyAfterSubmittingUpdate +{ + updateValidationTestPrologue + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + ASCellNode *oldHeader = [cv supplementaryNodeForElementKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; + XCTAssertNotNil(oldHeader); + + // Reload the section and ensure that the new header is loaded + [cv reloadSections:[NSIndexSet indexSetWithIndex:0]]; + ASCellNode *newHeader = [cv supplementaryNodeForElementKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; + XCTAssertNotNil(newHeader); + XCTAssertNotEqualObjects(oldHeader, newHeader); +} + +- (void)testThatNilBatchUpdatesCanBeSubmitted +{ + __block ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + __block ASCollectionNode *cn = testController.collectionNode; + + // Passing nil blocks should not crash + [cn performBatchUpdates:nil completion:nil]; + [cn performBatchAnimated:NO updates:nil completion:nil]; +} + +- (void)testThatDeletedItemsAreMarkedInvisible +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + window.rootViewController = testController; + + __block NSInteger itemCount = 1; + testController.asyncDelegate->_itemCounts = {itemCount}; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + ASCollectionNode *cn = testController.collectionNode; + [cn waitUntilAllUpdatesAreProcessed]; + [cn.view layoutIfNeeded]; + ASCellNode *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASCATransactionQueueWait(nil); + XCTAssertTrue(node.visible); + testController.asyncDelegate->_itemCounts = {0}; + [cn deleteItemsAtIndexPaths: @[[NSIndexPath indexPathForItem:0 inSection:0]]]; + [self expectationForPredicate:[NSPredicate predicateWithFormat:@"visible = NO"] evaluatedWithObject:node handler:nil]; + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)disabled_testThatMultipleBatchFetchesDontHappenUnnecessarily +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + window.rootViewController = testController; + + // Start with 1 item so that our content does not fill bounds. + __block NSInteger itemCount = 1; + testController.asyncDelegate->_itemCounts = {itemCount}; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + ASCollectionNode *cn = testController.collectionNode; + [cn waitUntilAllUpdatesAreProcessed]; + XCTAssertGreaterThan(cn.bounds.size.height, cn.view.contentSize.height, @"Expected initial data not to fill collection view area."); + + __block NSUInteger batchFetchCount = 0; + XCTestExpectation *expectation = [self expectationWithDescription:@"Batch fetching completed and then some"]; + __weak ASCollectionViewTestController *weakController = testController; + testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) { + + // Ensure only 1 batch fetch happens + batchFetchCount += 1; + if (batchFetchCount > 1) { + XCTFail(@"Too many batch fetches!"); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + // Up the item count to 1000 so that we're well beyond the + // edge of the collection view and not ready for another batch fetch. + NSMutableArray *indexPaths = [NSMutableArray array]; + for (; itemCount < 1000; itemCount++) { + [indexPaths addObject:[NSIndexPath indexPathForItem:itemCount inSection:0]]; + } + weakController.asyncDelegate->_itemCounts = {itemCount}; + [cn insertItemsAtIndexPaths:indexPaths]; + [context completeBatchFetching:YES]; + + // Let the run loop turn before we consider the test passed. + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + }); + }; + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testThatBatchFetchHappensForEmptyCollection +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + window.rootViewController = testController; + + testController.asyncDelegate->_itemCounts = {}; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + ASCollectionNode *cn = testController.collectionNode; + [cn waitUntilAllUpdatesAreProcessed]; + + __block NSUInteger batchFetchCount = 0; + XCTestExpectation *e = [self expectationWithDescription:@"Batch fetching completed"]; + testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) { + // Ensure only 1 batch fetch happens + batchFetchCount += 1; + if (batchFetchCount > 1) { + XCTFail(@"Too many batch fetches!"); + return; + } + [e fulfill]; + }; + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testThatWeBatchFetchUntilContentRequirementIsMet_Animated +{ + [self _primitiveBatchFetchingFillTestAnimated:YES visible:YES controller:nil]; +} + +- (void)testThatWeBatchFetchUntilContentRequirementIsMet_Nonanimated +{ + [self _primitiveBatchFetchingFillTestAnimated:NO visible:YES controller:nil]; +} + +- (void)testThatWeBatchFetchUntilContentRequirementIsMet_Invisible +{ + [self _primitiveBatchFetchingFillTestAnimated:NO visible:NO controller:nil]; +} + +- (void)testThatWhenWeBecomeVisibleWeWillFetchAdditionalContent +{ + ASCollectionViewTestController *ctrl = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + // Start with 1 empty section + ctrl.asyncDelegate->_itemCounts = {0}; + [self _primitiveBatchFetchingFillTestAnimated:NO visible:NO controller:ctrl]; + XCTAssertGreaterThan([ctrl.collectionNode numberOfItemsInSection:0], 0); + [self _primitiveBatchFetchingFillTestAnimated:NO visible:YES controller:ctrl]; +} + +- (void)_primitiveBatchFetchingFillTestAnimated:(BOOL)animated visible:(BOOL)visible controller:(nullable ASCollectionViewTestController *)testController +{ + if (testController == nil) { + testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + // Start with 1 empty section + testController.asyncDelegate->_itemCounts = {0}; + } + ASCollectionNode *cn = testController.collectionNode; + + UIWindow *window = nil; + UIView *view = nil; + if (visible) { + window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + view = window; + } else { + view = cn.view; + view.frame = [UIScreen mainScreen].bounds; + } + + XCTestExpectation *expectation = [self expectationWithDescription:@"Completed all batch fetches"]; + __weak ASCollectionViewTestController *weakController = testController; + __block NSInteger batchFetchCount = 0; + testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSInteger fetchIndex = batchFetchCount++; + + NSInteger itemCount = weakController.asyncDelegate->_itemCounts[0]; + weakController.asyncDelegate->_itemCounts[0] = (itemCount + 1); + if (animated) { + [cn insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:itemCount inSection:0] ]]; + } else { + [cn performBatchAnimated:NO updates:^{ + [cn insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:itemCount inSection:0] ]]; + } completion:nil]; + } + + [context completeBatchFetching:YES]; + + // If no more batch fetches have happened in 1 second, assume we're done. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (fetchIndex == batchFetchCount - 1) { + [expectation fulfill]; + } + }); + }); + }; + window.rootViewController = testController; + + [window makeKeyAndVisible]; + // Trigger the initial reload to start + [view layoutIfNeeded]; + + // Wait for ASDK reload to finish + [cn waitUntilAllUpdatesAreProcessed]; + // Force UIKit to read updated data & range controller to update and account for it + [cn.view layoutIfNeeded]; + [self waitForExpectationsWithTimeout:60 handler:nil]; + + CGFloat contentHeight = cn.view.contentSize.height; + CGFloat requiredContentHeight; + CGFloat itemHeight = [cn.view layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].size.height; + if (visible) { + requiredContentHeight = CGRectGetMaxY(cn.bounds) + CGRectGetHeight(cn.bounds) * cn.view.leadingScreensForBatching; + } else { + requiredContentHeight = CGRectGetMaxY(cn.bounds); + } + XCTAssertGreaterThan(batchFetchCount, 2); + XCTAssertGreaterThanOrEqual(contentHeight, requiredContentHeight, @"Loaded too little content."); + XCTAssertLessThanOrEqual(contentHeight, requiredContentHeight + 3 * itemHeight, @"Loaded too much content."); +} + +- (void)testInitialRangeBounds +{ + [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeNone + shouldWaitUntilAllUpdatesAreProcessed:YES]; +} + +- (void)testInitialRangeBoundsCellLayoutModeAlwaysAsync +{ + [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeAlwaysAsync + shouldWaitUntilAllUpdatesAreProcessed:YES]; +} + +- (void)testInitialRangeBoundsWithCellLayoutMode:(ASCellLayoutMode)cellLayoutMode + shouldWaitUntilAllUpdatesAreProcessed:(BOOL)shouldWait +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + ASCollectionNode *cn = testController.collectionNode; + cn.cellLayoutMode = cellLayoutMode; + [cn setTuningParameters:{ .leadingBufferScreenfuls = 2, .trailingBufferScreenfuls = 0 } forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload]; + window.rootViewController = testController; + + [testController.collectionNode.collectionViewLayout invalidateLayout]; + [testController.collectionNode.collectionViewLayout prepareLayout]; + + [window makeKeyAndVisible]; + // Trigger the initial reload to start + [window layoutIfNeeded]; + + if (shouldWait) { + XCTAssertTrue(cn.isProcessingUpdates, @"ASCollectionNode should still be processing updates after initial layoutIfNeeded call (reloadData)"); + + [cn onDidFinishProcessingUpdates:^{ + XCTAssertTrue(!cn.isProcessingUpdates, @"ASCollectionNode should no longer be processing updates inside -onDidFinishProcessingUpdates: block"); + }]; + + // Wait for ASDK reload to finish + [cn waitUntilAllUpdatesAreProcessed]; + } + + XCTAssertTrue(!cn.isProcessingUpdates, @"ASCollectionNode should no longer be processing updates after -wait call"); + + // Force UIKit to read updated data & range controller to update and account for it + [cn.view layoutIfNeeded]; + + CGRect preloadBounds = ({ + CGRect r = CGRectNull; + for (NSInteger s = 0; s < cn.numberOfSections; s++) { + NSInteger c = [cn numberOfItemsInSection:s]; + for (NSInteger i = 0; i < c; i++) { + NSIndexPath *ip = [NSIndexPath indexPathForItem:i inSection:s]; + ASCellNode *node = [cn nodeForItemAtIndexPath:ip]; + ASCATransactionQueueWait(nil); + if (node.inPreloadState) { + CGRect frame = [cn.view layoutAttributesForItemAtIndexPath:ip].frame; + r = CGRectUnion(r, frame); + } + } + } + r; + }); + CGFloat expectedHeight = cn.bounds.size.height * 3; + XCTAssertEqualWithAccuracy(CGRectGetHeight(preloadBounds), expectedHeight, expectedHeight * 0.1); + XCTAssertEqual([[cn valueForKeyPath:@"rangeController.currentRangeMode"] integerValue], ASLayoutRangeModeMinimum, @"Expected range mode to be minimum before scrolling begins."); +} + +- (void)testTraitCollectionChangesMidUpdate +{ + CGRect screenBounds = [UIScreen mainScreen].bounds; + UIWindow *window = [[UIWindow alloc] initWithFrame:screenBounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + ASCollectionNode *cn = testController.collectionNode; + window.rootViewController = testController; + + [window makeKeyAndVisible]; + // Trigger the initial reload to start + [window layoutIfNeeded]; + + // The initial reload is async, changing the trait collection here should be "mid-update" + ASPrimitiveTraitCollection traitCollection = ASPrimitiveTraitCollectionMakeDefault(); + traitCollection.displayScale = cn.primitiveTraitCollection.displayScale + 1; // Just a dummy change + traitCollection.containerSize = screenBounds.size; + cn.primitiveTraitCollection = traitCollection; + + [cn waitUntilAllUpdatesAreProcessed]; + [cn.view layoutIfNeeded]; + + // Assert that the new trait collection is picked up by all cell nodes, including ones that were not allocated but are forced to allocate now + for (NSInteger s = 0; s < cn.numberOfSections; s++) { + NSInteger c = [cn numberOfItemsInSection:s]; + for (NSInteger i = 0; i < c; i++) { + NSIndexPath *ip = [NSIndexPath indexPathForItem:i inSection:s]; + ASCellNode *node = [cn.view nodeForItemAtIndexPath:ip]; + XCTAssertTrue(ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, node.primitiveTraitCollection)); + } + } +} + +/** + * This tests an issue where, since subnode insertions aren't applied until the UIKit layout pass, + * which we trigger during the display phase, subnodes like network image nodes are not preloading + * until this layout pass happens which is too late. + */ +- (void)DISABLED_testThatAutomaticallyManagedSubnodesGetPreloadCallBeforeDisplay +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + window.rootViewController = testController; + ASCollectionNode *cn = testController.collectionNode; + + __block NSInteger itemCount = 100; + testController.asyncDelegate->_itemCounts = {itemCount}; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + [cn waitUntilAllUpdatesAreProcessed]; + for (NSInteger i = 0; i < itemCount; i++) { + ASTextCellNodeWithSetSelectedCounter *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; + XCTAssert(node.automaticallyManagesSubnodes, @"Expected test cell node to use automatic subnode management. Can modify the test with a different class if needed."); + ASDisplayNode *subnode = node.textNode; + XCTAssertEqualObjects(NSStringFromASInterfaceState(subnode.interfaceState), NSStringFromASInterfaceState(node.interfaceState), @"Subtree interface state should match cell node interface state for ASM nodes."); + XCTAssert(node.inDisplayState || !node.nodeLoaded, @"Only nodes in the display range should be loaded."); + } + +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCollectionViewThrashTests.mm b/submodules/AsyncDisplayKit/Tests/ASCollectionViewThrashTests.mm new file mode 100644 index 0000000000..4650639d1f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCollectionViewThrashTests.mm @@ -0,0 +1,217 @@ +// +// ASCollectionViewThrashTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +#import "ASThrashUtility.h" + +@interface ASCollectionViewThrashTests : XCTestCase + +@end + +@implementation ASCollectionViewThrashTests +{ + // The current update, which will be logged in case of a failure. + ASThrashUpdate *_update; + BOOL _failed; +} + +- (void)tearDown +{ + if (_failed && _update != nil) { + NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); + } + _failed = NO; + _update = nil; +} + +// NOTE: Despite the documentation, this is not always called if an exception is caught. +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected +{ + _failed = YES; + [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; +} + +- (void)verifyDataSource:(ASThrashDataSource *)ds +{ + CollectionView *collectionView = ds.collectionView; + NSArray *data = [ds data]; + for (NSInteger i = 0; i < collectionView.numberOfSections; i++) { + XCTAssertEqual([collectionView numberOfItemsInSection:i], data[i].items.count); + + for (NSInteger j = 0; j < [collectionView numberOfItemsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASThrashTestItem *item = data[i].items[j]; + ASThrashTestNode *node = (ASThrashTestNode *)[collectionView nodeForItemAtIndexPath:indexPath]; + XCTAssertEqualObjects(node.item, item, @"Wrong node at index path %@", indexPath); + } + } +} + +#pragma mark Test Methods + +- (void)testInitialDataRead +{ + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; + [self verifyDataSource:ds]; +} + +/// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file +- (void)testRecordedThrashCase +{ + NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; + + _update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (_update == nil) { + return; + } + + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:_update.oldData]; + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:YES]; + [self verifyDataSource:ds]; +} + +- (void)testThrashingWildly +{ + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { + [self setUp]; + @autoreleasepool { + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:sections]; + + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:NO]; + [self verifyDataSource:ds]; + [self expectationForPredicate:[ds predicateForDeallocatedHierarchy] evaluatedWithObject:(id)kCFNull handler:nil]; + } + [self waitForExpectationsWithTimeout:3 handler:nil]; + + [self tearDown]; + } +} + +- (void)testThrashingWildlyOnSameCollectionView +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:nil]; + for (NSInteger i = 0; i < 1000; i++) { + [self setUp]; + @autoreleasepool { + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + [ds setData:sections]; + [ds.collectionView reloadData]; + + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:NO]; + [self verifyDataSource:ds]; + if (i == 999) { + [expectation fulfill]; + } + } + + [self tearDown]; + } + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testThrashingWildlyDispatchWildly +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { + [self setUp]; + @autoreleasepool { + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:sections]; + + [self applyUpdateUsingBatchUpdates:_update + toDataSource:ds + animated:NO + useXCTestWait:NO]; + [self verifyDataSource:ds]; + if (i == kThrashingIterationCount-1) { + [expectation fulfill]; + } + }); + } + + [self tearDown]; + } + + [self waitForExpectationsWithTimeout:100 handler:nil]; +} + +#pragma mark Helpers + +- (void)applyUpdateUsingBatchUpdates:(ASThrashUpdate *)update + toDataSource:(ASThrashDataSource *)dataSource animated:(BOOL)animated + useXCTestWait:(BOOL)wait +{ + CollectionView *collectionView = dataSource.collectionView; + + XCTestExpectation *expectation; + if (wait) { + expectation = [self expectationWithDescription:@"Wait for collection view to update"]; + } + + void (^updateBlock)() = ^ void (){ + dataSource.data = update.data; + + [collectionView insertSections:update.insertedSectionIndexes]; + [collectionView deleteSections:update.deletedSectionIndexes]; + [collectionView reloadSections:update.replacedSectionIndexes]; + + [update.insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [collectionView insertItemsAtIndexPaths:indexPaths]; + }]; + + [update.deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [collectionView deleteItemsAtIndexPaths:indexPaths]; + }]; + + [update.replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [collectionView reloadItemsAtIndexPaths:indexPaths]; + }]; + }; + + @try { + [collectionView performBatchAnimated:animated + updates:updateBlock + completion:^(BOOL finished) { + [expectation fulfill]; + }]; + } @catch (NSException *exception) { + _failed = YES; + XCTFail("TEST FAILED"); + @throw exception; + } + + if (wait) { + [self waitForExpectationsWithTimeout:1 handler:nil]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCollectionsTests.mm b/submodules/AsyncDisplayKit/Tests/ASCollectionsTests.mm new file mode 100644 index 0000000000..ae0313d78c --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCollectionsTests.mm @@ -0,0 +1,55 @@ +// +// ASCollectionsTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASCollectionsTests : XCTestCase + +@end + +@implementation ASCollectionsTests + +- (void)testTransferArray { + id objs[2]; + objs[0] = [NSObject new]; + id o0 = objs[0]; + objs[1] = [NSObject new]; + __weak id w0 = objs[0]; + __weak id w1 = objs[1]; + CFTypeRef cf0 = (__bridge CFTypeRef)objs[0]; + CFTypeRef cf1 = (__bridge CFTypeRef)objs[1]; + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + NSArray *arr = [NSArray arrayByTransferring:objs count:2]; + XCTAssertNil(objs[0]); + XCTAssertNil(objs[1]); + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + NSArray *immutableCopy = [arr copy]; + XCTAssertEqual(immutableCopy, arr); + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + NSMutableArray *mc = [arr mutableCopy]; + XCTAssertEqual(CFGetRetainCount(cf0), 3); + XCTAssertEqual(CFGetRetainCount(cf1), 2); + arr = nil; + immutableCopy = nil; + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + [mc removeObjectAtIndex:0]; + XCTAssertEqual(CFGetRetainCount(cf0), 1); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + [mc removeObjectAtIndex:0]; + XCTAssertEqual(CFGetRetainCount(cf0), 1); + XCTAssertNil(w1); + o0 = nil; + XCTAssertNil(w0); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASConfigurationTests.mm b/submodules/AsyncDisplayKit/Tests/ASConfigurationTests.mm new file mode 100644 index 0000000000..6952924851 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASConfigurationTests.mm @@ -0,0 +1,139 @@ +// +// ASConfigurationTests.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import + +#import "ASTestCase.h" + +static ASExperimentalFeatures features[] = { + ASExperimentalGraphicsContexts, +#if AS_ENABLE_TEXTNODE + ASExperimentalTextNode, +#endif + ASExperimentalInterfaceStateCoalescing, + ASExperimentalUnfairLock, + ASExperimentalLayerDefaults, + ASExperimentalCollectionTeardown, + ASExperimentalFramesetterCache, + ASExperimentalSkipClearData, + ASExperimentalDidEnterPreloadSkipASMLayout, + ASExperimentalDisableAccessibilityCache, + ASExperimentalDispatchApply, + ASExperimentalImageDownloaderPriority, + ASExperimentalTextDrawing +}; + +@interface ASConfigurationTests : ASTestCase + +@end + +@implementation ASConfigurationTests { + void (^onActivate)(ASConfigurationTests *self, ASExperimentalFeatures feature); +} + ++ (NSArray *)names { + return @[ + @"exp_graphics_contexts", + @"exp_text_node", + @"exp_interface_state_coalesce", + @"exp_unfair_lock", + @"exp_infer_layer_defaults", + @"exp_collection_teardown", + @"exp_framesetter_cache", + @"exp_skip_clear_data", + @"exp_did_enter_preload_skip_asm_layout", + @"exp_disable_a11y_cache", + @"exp_dispatch_apply", + @"exp_image_downloader_priority", + @"exp_text_drawing" + ]; +} + +- (ASExperimentalFeatures)allFeatures { + ASExperimentalFeatures allFeatures = 0; + for (int i = 0; i < sizeof(features)/sizeof(ASExperimentalFeatures); i++) { + allFeatures |= features[i]; + } + return allFeatures; +} + +#if AS_ENABLE_TEXTNODE + +- (void)testExperimentalFeatureConfig +{ + // Set the config + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalGraphicsContexts; + config.delegate = self; + [ASConfigurationManager test_resetWithConfiguration:config]; + + // Set an expectation for a callback, and assert we only get one. + XCTestExpectation *e = [self expectationWithDescription:@"Callbacks done."]; + e.expectedFulfillmentCount = 2; + e.assertForOverFulfill = YES; + onActivate = ^(ASConfigurationTests *self, ASExperimentalFeatures feature) { + [e fulfill]; + }; + + // Now activate the graphics experiment and expect it works. + XCTAssertTrue(ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)); + // We should get a callback here + // Now activate text node and expect it fails. + XCTAssertFalse(ASActivateExperimentalFeature(ASExperimentalTextNode)); + // But we should get another callback. + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +#endif + +- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)feature +{ + if (onActivate) { + onActivate(self, feature); + } +} + +- (void)testMappingNamesToFlags +{ + // Throw in a bad bit. + ASExperimentalFeatures allFeatures = [self allFeatures]; + ASExperimentalFeatures featuresWithBadBit = allFeatures | (1 << 22); + NSArray *expectedNames = [ASConfigurationTests names]; + XCTAssertEqualObjects(expectedNames, ASExperimentalFeaturesGetNames(featuresWithBadBit)); +} + +- (void)testMappingFlagsFromNames +{ + // Throw in a bad name. + NSMutableArray *allNames = [[NSMutableArray alloc] initWithArray:[ASConfigurationTests names]]; + [allNames addObject:@"__invalid_name"]; + ASExperimentalFeatures expected = [self allFeatures]; + XCTAssertEqual(expected, ASExperimentalFeaturesFromArray(allNames)); +} + +- (void)testFlagMatchName +{ + NSArray *names = [ASConfigurationTests names]; + for (NSInteger i = 0; i < names.count; i++) { + XCTAssertEqual(features[i], ASExperimentalFeaturesFromArray(@[names[i]])); + } +} + +- (void)testNameMatchFlag { + NSArray *names = [ASConfigurationTests names]; + for (NSInteger i = 0; i < names.count; i++) { + XCTAssertEqualObjects(@[names[i]], ASExperimentalFeaturesGetNames(features[i])); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASControlNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASControlNodeTests.mm new file mode 100644 index 0000000000..e1c0150a0d --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASControlNodeTests.mm @@ -0,0 +1,225 @@ +// +// ASControlNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#define ACTION @selector(action) +#define ACTION_SENDER @selector(action:) +#define ACTION_SENDER_EVENT @selector(action:event:) +#define EVENT ASControlNodeEventTouchUpInside + +@interface ReceiverController : UIViewController +@property (nonatomic) NSInteger hits; +@end +@implementation ReceiverController +@end + +@interface ASActionController : ReceiverController +@end +@implementation ASActionController +- (void)action { self.hits++; } +- (void)firstAction { } +- (void)secondAction { } +- (void)thirdAction { } +@end + +@interface ASActionSenderController : ReceiverController +@end +@implementation ASActionSenderController +- (void)action:(id)sender { self.hits++; } +@end + +@interface ASActionSenderEventController : ReceiverController +@end +@implementation ASActionSenderEventController +- (void)action:(id)sender event:(UIEvent *)event { self.hits++; } +@end + +@interface ASGestureController : ReceiverController +@end +@implementation ASGestureController +- (void)onGesture:(UIGestureRecognizer *)recognizer { self.hits++; } +- (void)action:(id)sender { self.hits++; } +@end + +@interface ASControlNodeTests : XCTestCase + +@end + +@implementation ASControlNodeTests + +- (void)testActionWithoutParameters { + ASActionController *controller = [[ASActionController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testActionAndSender { + ASActionSenderController *controller = [[ASActionSenderController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testActionAndSenderAndEvent { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testActionWithoutTarget { + ASActionController *controller = [[ASActionController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:nil action:ACTION forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testActionAndSenderWithoutTarget { + ASActionSenderController *controller = [[ASActionSenderController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:nil action:ACTION_SENDER forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testActionAndSenderAndEventWithoutTarget { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testRemoveWithoutTargetRemovesTargetlessAction { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node removeTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 0, @"Controller did not receive exactly zero action events"); +} + +- (void)testRemoveWithTarget { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node removeTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 0, @"Controller did not receive exactly zero action events"); +} + +- (void)testRemoveWithTargetRemovesAction { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node removeTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 0, @"Controller did not receive exactly zero action events"); +} + +- (void)testRemoveWithoutTargetRemovesTargetedAction { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node removeTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 0, @"Controller did not receive exactly zero action events"); +} + +- (void)testDuplicateEntriesWithoutTarget { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node addTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 1, @"Controller did not receive exactly one action event"); +} + +- (void)testDuplicateEntriesWithTarget { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 1, @"Controller did not receive exactly one action event"); +} + +- (void)testDuplicateEntriesWithAndWithoutTarget { + ASActionSenderEventController *controller = [[ASActionSenderEventController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [node addTarget:nil action:ACTION_SENDER_EVENT forControlEvents:EVENT]; + [controller.view addSubview:node.view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssertEqual(controller.hits, 2, @"Controller did not receive exactly two action events"); +} + +- (void)testDeeperHierarchyWithoutTarget { + ASActionController *controller = [[ASActionController alloc] init]; + UIView *view = [[UIView alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:nil action:ACTION forControlEvents:EVENT]; + [view addSubview:node.view]; + [controller.view addSubview:view]; + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the action event"); +} + +- (void)testTouchesWorkWithGestures { + ASGestureController *controller = [[ASGestureController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:@selector(action:) forControlEvents:ASControlNodeEventTouchUpInside]; + [node.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:controller action:@selector(onGesture:)]]; + [controller.view addSubnode:node]; + + [node sendActionsForControlEvents:EVENT withEvent:nil]; + XCTAssert(controller.hits == 1, @"Controller did not receive the tap event"); +} + +- (void)testActionsAreCalledInTheSameOrderAsTheyWereAdded { + ASActionController *controller = [[ASActionController alloc] init]; + ASControlNode *node = [[ASControlNode alloc] init]; + [node addTarget:controller action:@selector(firstAction) forControlEvents:ASControlNodeEventTouchUpInside]; + [node addTarget:controller action:@selector(secondAction) forControlEvents:ASControlNodeEventTouchUpInside]; + [node addTarget:controller action:@selector(thirdAction) forControlEvents:ASControlNodeEventTouchUpInside]; + [controller.view addSubnode:node]; + + id controllerMock = [OCMockObject partialMockForObject:controller]; + [controllerMock setExpectationOrderMatters:YES]; + [[controllerMock expect] firstAction]; + [[controllerMock expect] secondAction]; + [[controllerMock expect] thirdAction]; + + [node sendActionsForControlEvents:ASControlNodeEventTouchUpInside withEvent:nil]; + + [controllerMock verify]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASCornerLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASCornerLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..2c496cd595 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASCornerLayoutSpecSnapshotTests.mm @@ -0,0 +1,215 @@ +// +// ASCornerLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" +#import +#import + +typedef NS_ENUM(NSInteger, ASCornerLayoutSpecSnapshotTestsOffsetOption) { + ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter, + ASCornerLayoutSpecSnapshotTestsOffsetOptionInner, + ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter, +}; + + +@interface ASCornerLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase + +@property (nonatomic, copy) UIColor *boxColor; +@property (nonatomic, copy) UIColor *baseColor; +@property (nonatomic, copy) UIColor *cornerColor; +@property (nonatomic, copy) UIColor *contextColor; + +@property (nonatomic) CGSize baseSize; +@property (nonatomic) CGSize cornerSize; +@property (nonatomic) CGSize contextSize; + +@property (nonatomic) ASSizeRange contextSizeRange; + +@end + + +@implementation ASCornerLayoutSpecSnapshotTests + +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; + + _boxColor = [UIColor greenColor]; + _baseColor = [UIColor blueColor]; + _cornerColor = [UIColor orangeColor]; + _contextColor = [UIColor lightGrayColor]; + + _baseSize = CGSizeMake(60, 60); + _cornerSize = CGSizeMake(20, 20); + _contextSize = CGSizeMake(120, 120); + + _contextSizeRange = ASSizeRangeMake(CGSizeZero, _contextSize); +} + +- (void)testCornerSpecForAllLocations +{ + ASCornerLayoutSpecSnapshotTestsOffsetOption center = ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:center wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:center wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:center wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:center wrapsCorner:YES]; +} + +- (void)testCornerSpecForAllLocationsWithInnerOffset +{ + ASCornerLayoutSpecSnapshotTestsOffsetOption inner = ASCornerLayoutSpecSnapshotTestsOffsetOptionInner; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:inner wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:inner wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:inner wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:inner wrapsCorner:YES]; +} + +- (void)testCornerSpecForAllLocationsWithOuterOffset +{ + ASCornerLayoutSpecSnapshotTestsOffsetOption outer = ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:outer wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:outer wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:outer wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:outer wrapsCorner:YES]; +} + +- (void)testCornerSpecWithLocation:(ASCornerLayoutLocation)location + offsetOption:(ASCornerLayoutSpecSnapshotTestsOffsetOption)offsetOption + wrapsCorner:(BOOL)wrapsCorner +{ + ASDisplayNode *baseNode = ASDisplayNodeWithBackgroundColor(_baseColor, _baseSize); + ASDisplayNode *cornerNode = ASDisplayNodeWithBackgroundColor(_cornerColor, _cornerSize); + ASDisplayNode *debugBoxNode = ASDisplayNodeWithBackgroundColor(_boxColor); + + baseNode.style.layoutPosition = CGPointMake((_contextSize.width - _baseSize.width) / 2, + (_contextSize.height - _baseSize.height) / 2); + + ASCornerLayoutSpec *cornerSpec = [ASCornerLayoutSpec cornerLayoutSpecWithChild:baseNode + corner:cornerNode + location:location]; + + CGPoint delta = (CGPoint){ _cornerSize.width / 2, _cornerSize.height / 2 }; + cornerSpec.offset = [self offsetForOption:offsetOption location:location delta:delta]; + cornerSpec.wrapsCorner = wrapsCorner; + + ASBackgroundLayoutSpec *backgroundSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:cornerSpec + background:debugBoxNode]; + + [self testLayoutSpec:backgroundSpec + sizeRange:_contextSizeRange + subnodes:@[debugBoxNode, baseNode, cornerNode] + identifier:[self suffixWithLocation:location option:offsetOption wrapsCorner:wrapsCorner]]; +} + +- (CGPoint)offsetForOption:(ASCornerLayoutSpecSnapshotTestsOffsetOption)option + location:(ASCornerLayoutLocation)location + delta:(CGPoint)delta +{ + CGFloat x = delta.x; + CGFloat y = delta.y; + + switch (option) { + + case ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter: + return CGPointZero; + + case ASCornerLayoutSpecSnapshotTestsOffsetOptionInner: + + switch (location) { + case ASCornerLayoutLocationTopLeft: return (CGPoint){ x, y }; + case ASCornerLayoutLocationTopRight: return (CGPoint){ -x, y }; + case ASCornerLayoutLocationBottomLeft: return (CGPoint){ x, -y }; + case ASCornerLayoutLocationBottomRight: return (CGPoint){ -x, -y }; + } + + case ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter: + + switch (location) { + case ASCornerLayoutLocationTopLeft: return (CGPoint){ -x, -y }; + case ASCornerLayoutLocationTopRight: return (CGPoint){ x, -y }; + case ASCornerLayoutLocationBottomLeft: return (CGPoint){ -x, y }; + case ASCornerLayoutLocationBottomRight: return (CGPoint){ x, y }; + } + + } + +} + +- (NSString *)suffixWithLocation:(ASCornerLayoutLocation)location + option:(ASCornerLayoutSpecSnapshotTestsOffsetOption)option + wrapsCorner:(BOOL)wrapsCorner +{ + NSMutableString *desc = [NSMutableString string]; + + switch (location) { + case ASCornerLayoutLocationTopLeft: + [desc appendString:@"topLeft"]; + break; + case ASCornerLayoutLocationTopRight: + [desc appendString:@"topRight"]; + break; + case ASCornerLayoutLocationBottomLeft: + [desc appendString:@"bottomLeft"]; + break; + case ASCornerLayoutLocationBottomRight: + [desc appendString:@"bottomRight"]; + break; + } + + [desc appendString:@"_"]; + + switch (option) { + case ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter: + [desc appendString:@"center"]; + break; + case ASCornerLayoutSpecSnapshotTestsOffsetOptionInner: + [desc appendString:@"inner"]; + break; + case ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter: + [desc appendString:@"outer"]; + break; + } + + [desc appendString:@"_"]; + + if (wrapsCorner) { + [desc appendString:@"fullSize"]; + } else { + [desc appendString:@"childSize"]; + } + + return desc.copy; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDimensionTests.mm b/submodules/AsyncDisplayKit/Tests/ASDimensionTests.mm new file mode 100644 index 0000000000..c3e30ae63c --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDimensionTests.mm @@ -0,0 +1,106 @@ +// +// ASDimensionTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import "ASXCTExtensions.h" + + +@interface ASDimensionTests : XCTestCase +@end + +@implementation ASDimensionTests + +- (void)testCreatingDimensionUnitAutos +{ + XCTAssertNoThrow(ASDimensionMake(ASDimensionUnitAuto, 0)); + XCTAssertThrows(ASDimensionMake(ASDimensionUnitAuto, 100)); + ASXCTAssertEqualDimensions(ASDimensionAuto, ASDimensionMake(@"")); + ASXCTAssertEqualDimensions(ASDimensionAuto, ASDimensionMake(@"auto")); +} + +- (void)testCreatingDimensionUnitFraction +{ + XCTAssertNoThrow(ASDimensionMake(ASDimensionUnitFraction, 0.5)); + ASXCTAssertEqualDimensions(ASDimensionMake(ASDimensionUnitFraction, 0.5), ASDimensionMake(@"50%")); +} + +- (void)testCreatingDimensionUnitPoints +{ + XCTAssertNoThrow(ASDimensionMake(ASDimensionUnitPoints, 100)); + ASXCTAssertEqualDimensions(ASDimensionMake(ASDimensionUnitPoints, 100), ASDimensionMake(@"100pt")); +} + +- (void)testIntersectingOverlappingSizeRangesReturnsTheirIntersection +{ + // range: |---------| + // other: |----------| + // result: |----| + + ASSizeRange range = {{0,0}, {10,10}}; + ASSizeRange other = {{7,7}, {15,15}}; + ASSizeRange result = ASSizeRangeIntersect(range, other); + ASSizeRange expected = {{7,7}, {10,10}}; + XCTAssertTrue(ASSizeRangeEqualToSizeRange(result, expected), @"Expected %@ but got %@", NSStringFromASSizeRange(expected), NSStringFromASSizeRange(result)); +} + +- (void)testIntersectingSizeRangeWithRangeThatContainsItReturnsSameRange +{ + // range: |-----| + // other: |---------| + // result: |-----| + + ASSizeRange range = {{2,2}, {8,8}}; + ASSizeRange other = {{0,0}, {10,10}}; + ASSizeRange result = ASSizeRangeIntersect(range, other); + ASSizeRange expected = {{2,2}, {8,8}}; + XCTAssertTrue(ASSizeRangeEqualToSizeRange(result, expected), @"Expected %@ but got %@", NSStringFromASSizeRange(expected), NSStringFromASSizeRange(result)); +} + +- (void)testIntersectingSizeRangeWithRangeContainedWithinItReturnsContainedRange +{ + // range: |---------| + // other: |-----| + // result: |-----| + + ASSizeRange range = {{0,0}, {10,10}}; + ASSizeRange other = {{2,2}, {8,8}}; + ASSizeRange result = ASSizeRangeIntersect(range, other); + ASSizeRange expected = {{2,2}, {8,8}}; + XCTAssertTrue(ASSizeRangeEqualToSizeRange(result, expected), @"Expected %@ but got %@", NSStringFromASSizeRange(expected), NSStringFromASSizeRange(result)); +} + +- (void)testIntersectingSizeRangeWithNonOverlappingRangeToRightReturnsSinglePointNearestOtherRange +{ + // range: |-----| + // other: |---| + // result: * + + ASSizeRange range = {{0,0}, {5,5}}; + ASSizeRange other = {{10,10}, {15,15}}; + ASSizeRange result = ASSizeRangeIntersect(range, other); + ASSizeRange expected = {{5,5}, {5,5}}; + XCTAssertTrue(ASSizeRangeEqualToSizeRange(result, expected), @"Expected %@ but got %@", NSStringFromASSizeRange(expected), NSStringFromASSizeRange(result)); +} + +- (void)testIntersectingSizeRangeWithNonOverlappingRangeToLeftReturnsSinglePointNearestOtherRange +{ + // range: |---| + // other: |-----| + // result: * + + ASSizeRange range = {{10,10}, {15,15}}; + ASSizeRange other = {{0,0}, {5,5}}; + ASSizeRange result = ASSizeRangeIntersect(range, other); + ASSizeRange expected = {{10,10}, {10,10}}; + XCTAssertTrue(ASSizeRangeEqualToSizeRange(result, expected), @"Expected %@ but got %@", NSStringFromASSizeRange(expected), NSStringFromASSizeRange(result)); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDispatchTests.mm b/submodules/AsyncDisplayKit/Tests/ASDispatchTests.mm new file mode 100644 index 0000000000..a90bce7ed8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDispatchTests.mm @@ -0,0 +1,64 @@ +// +// ASDispatchTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASDispatchTests : XCTestCase + +@end + +@implementation ASDispatchTests + +- (void)testDispatchApply +{ + dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSInteger expectedThreadCount = [NSProcessInfo processInfo].activeProcessorCount * 2; + NSLock *lock = [NSLock new]; + NSMutableSet *threads = [NSMutableSet set]; + NSMutableIndexSet *indices = [NSMutableIndexSet indexSet]; + + size_t const iterations = 1E5; + ASDispatchApply(iterations, q, 0, ^(size_t i) { + [lock lock]; + [threads addObject:[NSThread currentThread]]; + XCTAssertFalse([indices containsIndex:i]); + [indices addIndex:i]; + [lock unlock]; + }); + XCTAssertLessThanOrEqual(threads.count, expectedThreadCount); + XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]); +} + +- (void)testDispatchAsync +{ + dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSInteger expectedThreadCount = [NSProcessInfo processInfo].activeProcessorCount * 2; + NSLock *lock = [NSLock new]; + NSMutableSet *threads = [NSMutableSet set]; + NSMutableIndexSet *indices = [NSMutableIndexSet indexSet]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Executed all blocks"]; + + size_t const iterations = 1E5; + ASDispatchAsync(iterations, q, 0, ^(size_t i) { + [lock lock]; + [threads addObject:[NSThread currentThread]]; + XCTAssertFalse([indices containsIndex:i]); + [indices addIndex:i]; + if (indices.count == iterations) { + [expectation fulfill]; + } + [lock unlock]; + }); + [self waitForExpectationsWithTimeout:10 handler:nil]; + XCTAssertLessThanOrEqual(threads.count, expectedThreadCount); + XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayLayerTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayLayerTests.mm new file mode 100644 index 0000000000..2e03eb6da2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayLayerTests.mm @@ -0,0 +1,598 @@ +// +// ASDisplayLayerTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +#import + +#import "ASDisplayNodeTestsHelper.h" + +static UIImage *bogusImage() { + static UIImage *bogusImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + UIGraphicsBeginImageContext(CGSizeMake(10, 10)); + + bogusImage = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); + + }); + + return bogusImage; +} + +@interface _ASDisplayLayerTestContainerLayer : CALayer +@property (nonatomic, readonly) NSUInteger didCompleteTransactionCount; +@end + +@implementation _ASDisplayLayerTestContainerLayer + +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction +{ + _didCompleteTransactionCount++; +} + +@end + + +@interface ASDisplayNode (HackForTests) +- (id)initWithViewClass:(Class)viewClass; +- (id)initWithLayerClass:(Class)layerClass; +@end + + +@interface _ASDisplayLayerTestLayer : _ASDisplayLayer +{ + BOOL _isInCancelAsyncDisplay; + BOOL _isInDisplay; +} +@property (nonatomic, readonly) NSUInteger displayCount; +@property (nonatomic, readonly) NSUInteger drawInContextCount; +@property (nonatomic, readonly) NSUInteger setContentsAsyncCount; +@property (nonatomic, readonly) NSUInteger setContentsSyncCount; +@property (nonatomic, copy, readonly) NSString *setContentsCounts; +- (BOOL)checkSetContentsCountsWithSyncCount:(NSUInteger)syncCount asyncCount:(NSUInteger)asyncCount; +@end + +@implementation _ASDisplayLayerTestLayer + +- (NSString *)setContentsCounts +{ + return [NSString stringWithFormat:@"syncCount:%tu, asyncCount:%tu", _setContentsSyncCount, _setContentsAsyncCount]; +} + +- (BOOL)checkSetContentsCountsWithSyncCount:(NSUInteger)syncCount asyncCount:(NSUInteger)asyncCount +{ + return ((syncCount == _setContentsSyncCount) && + (asyncCount == _setContentsAsyncCount)); +} + +- (void)setContents:(id)contents +{ + [super setContents:contents]; + + if (self.displaysAsynchronously) { + if (_isInDisplay) { + [NSException raise:NSInvalidArgumentException format:@"There is no placeholder logic in _ASDisplayLayer, unknown caller for setContents:"]; + } else if (!_isInCancelAsyncDisplay) { + _setContentsAsyncCount++; + } + } else { + _setContentsSyncCount++; + } +} + +- (void)display +{ + _isInDisplay = YES; + [super display]; + _isInDisplay = NO; + _displayCount++; +} + +- (void)cancelAsyncDisplay +{ + _isInCancelAsyncDisplay = YES; + [super cancelAsyncDisplay]; + _isInCancelAsyncDisplay = NO; +} + +// This should never get called. This just records if it is. +- (void)drawInContext:(CGContextRef)context +{ + [super drawInContext:context]; + _drawInContextCount++; +} + +@end + +typedef NS_ENUM(NSUInteger, _ASDisplayLayerTestDelegateMode) +{ + _ASDisplayLayerTestDelegateModeNone = 0, + _ASDisplayLayerTestDelegateModeDrawParameters = 1 << 0, + _ASDisplayLayerTestDelegateModeWillDisplay = 1 << 1, + _ASDisplayLayerTestDelegateModeDidDisplay = 1 << 2, +}; + +typedef NS_ENUM(NSUInteger, _ASDisplayLayerTestDelegateClassModes) { + _ASDisplayLayerTestDelegateClassModeNone = 0, + _ASDisplayLayerTestDelegateClassModeDisplay = 1 << 0, + _ASDisplayLayerTestDelegateClassModeDrawInContext = 1 << 1, +}; + +@interface _ASDisplayLayerTestDelegate : ASDisplayNode <_ASDisplayLayerDelegate> + +@property (nonatomic) NSUInteger didDisplayCount; +@property (nonatomic) NSUInteger drawParametersCount; +@property (nonatomic) NSUInteger willDisplayCount; + +// for _ASDisplayLayerTestDelegateModeClassDisplay +@property (nonatomic) NSUInteger displayCount; +@property (nonatomic) UIImage *(^displayLayerBlock)(void); + +// for _ASDisplayLayerTestDelegateModeClassDrawInContext +@property (nonatomic) NSUInteger drawRectCount; + +@end + +@implementation _ASDisplayLayerTestDelegate { + _ASDisplayLayerTestDelegateMode _modes; +} + +static _ASDisplayLayerTestDelegateClassModes _class_modes; + ++ (void)setClassModes:(_ASDisplayLayerTestDelegateClassModes)classModes +{ + _class_modes = classModes; +} + +- (id)initWithModes:(_ASDisplayLayerTestDelegateMode)modes +{ + _modes = modes; + + if (!(self = [super initWithLayerClass:[_ASDisplayLayerTestLayer class]])) + return nil; + + return self; +} + +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + _didDisplayCount++; +} + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + _drawParametersCount++; + return self; +} + +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + _willDisplayCount++; +} + +- (BOOL)respondsToSelector:(SEL)selector +{ + if (sel_isEqual(selector, @selector(didDisplayAsyncLayer:))) { + return (_modes & _ASDisplayLayerTestDelegateModeDidDisplay); + } else if (sel_isEqual(selector, @selector(drawParametersForAsyncLayer:))) { + return (_modes & _ASDisplayLayerTestDelegateModeDrawParameters); + } else if (sel_isEqual(selector, @selector(willDisplayAsyncLayer:))) { + return (_modes & _ASDisplayLayerTestDelegateModeWillDisplay); + } else { + return [super respondsToSelector:selector]; + } +} + ++ (BOOL)respondsToSelector:(SEL)selector +{ + if (sel_isEqual(selector, @selector(displayWithParameters:isCancelled:))) { + return _class_modes & _ASDisplayLayerTestDelegateClassModeDisplay; + } else if (sel_isEqual(selector, @selector(drawRect:withParameters:isCancelled:isRasterizing:))) { + return _class_modes & _ASDisplayLayerTestDelegateClassModeDrawInContext; + } else { + return [super respondsToSelector:selector]; + } +} + +// DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! ++ (UIImage *)displayWithParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)sentinelBlock +{ + UIImage *contents = bogusImage(); + if (delegate->_displayLayerBlock != NULL) { + contents = delegate->_displayLayerBlock(); + } + delegate->_displayCount++; + return contents; +} + +// DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! ++ (void)drawRect:(CGRect)bounds withParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)sentinelBlock isRasterizing:(BOOL)isRasterizing +{ + __atomic_add_fetch(&delegate->_drawRectCount, 1, __ATOMIC_SEQ_CST); +} + +- (NSUInteger)drawRectCount +{ + return(__atomic_load_n(&_drawRectCount, __ATOMIC_SEQ_CST)); +} + +@end + +@interface _ASDisplayLayerTests : XCTestCase +@end + +@implementation _ASDisplayLayerTests + +- (void)setUp { + [super setUp]; + // Force bogusImage() to create+cache its image. This impacts any time-sensitive tests which call the method from + // within the timed portion of the test. It seems that, in rare cases, this image creation can take a bit too long, + // causing a test failure. + bogusImage(); +} + +// since we're not running in an application, we need to force this display on layer the hierarchy +- (void)displayLayerRecursively:(CALayer *)layer +{ + if (layer.needsDisplay) { + [layer displayIfNeeded]; + } + for (CALayer *sublayer in layer.sublayers) { + [self displayLayerRecursively:sublayer]; + } +} + +- (void)waitForDisplayQueue +{ + // make sure we don't lock up the tests indefinitely; fail after 1 sec by using an async barrier + __block BOOL didHitBarrier = NO; + dispatch_barrier_async([_ASDisplayLayer displayQueue], ^{ + __atomic_store_n(&didHitBarrier, YES, __ATOMIC_SEQ_CST); + }); + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return __atomic_load_n(&didHitBarrier, __ATOMIC_SEQ_CST); })); +} + +- (void)waitForLayer:(_ASDisplayLayerTestLayer *)layer asyncDisplayCount:(NSUInteger)count +{ + // make sure we don't lock up the tests indefinitely; fail after 1 sec of waiting for the setContents async count to increment + // NOTE: the layer sets its contents async back on the main queue, so we need to wait for main + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (layer.setContentsAsyncCount == count); + })); +} + +- (void)waitForAsyncDelegate:(_ASDisplayLayerTestDelegate *)asyncDelegate +{ + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (asyncDelegate.didDisplayCount == 1); + })); +} + +- (void)checkDelegateDisplay:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateMode(_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters)]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + [self waitForAsyncDelegate:asyncDelegate]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 1u); + XCTAssertEqual(asyncDelegate.displayCount, 1u); +} + +- (void)testDelegateDisplaySync +{ + [self checkDelegateDisplay:NO]; +} + +- (void)testDelegateDisplayAsync +{ + [self checkDelegateDisplay:YES]; +} + +- (void)checkDelegateDrawInContext:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDrawInContext]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateMode(_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters)]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForLayer:layer asyncDisplayCount:1]; + [self waitForAsyncDelegate:asyncDelegate]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 1u); + XCTAssertEqual(asyncDelegate.displayCount, 0u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 1u); +} + +- (void)testDelegateDrawInContextSync +{ + [self checkDelegateDrawInContext:NO]; +} + +- (void)testDelegateDrawInContextAsync +{ + [self checkDelegateDrawInContext:YES]; +} + +- (void)checkDelegateDisplayAndDrawInContext:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModes(_ASDisplayLayerTestDelegateClassModeDisplay | _ASDisplayLayerTestDelegateClassModeDrawInContext)]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateMode(_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters)]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + [self waitForAsyncDelegate:asyncDelegate]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 1u); + XCTAssertEqual(asyncDelegate.displayCount, 1u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); +} + +- (void)testDelegateDisplayAndDrawInContextSync +{ + [self checkDelegateDisplayAndDrawInContext:NO]; +} + +- (void)testDelegateDisplayAndDrawInContextAsync +{ + [self checkDelegateDisplayAndDrawInContext:YES]; +} + +- (void)testCancelAsyncDisplay +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateModeDidDisplay]; + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + + dispatch_suspend([_ASDisplayLayer displayQueue]); + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + XCTAssertTrue(layer.needsDisplay); + [layer displayIfNeeded]; + + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + + [layer cancelAsyncDisplay]; + + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 0u); + XCTAssertEqual(asyncDelegate.displayCount, 0u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 0u); +} + +- (void)DISABLED_testTransaction +{ + auto delegateModes = _ASDisplayLayerTestDelegateMode(_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters); + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay]; + + // Setup + _ASDisplayLayerTestContainerLayer *containerLayer = [[_ASDisplayLayerTestContainerLayer alloc] init]; + containerLayer.asyncdisplaykit_asyncTransactionContainer = YES; + containerLayer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + + _ASDisplayLayerTestDelegate *layer1Delegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:delegateModes]; + _ASDisplayLayerTestLayer *layer1 = (_ASDisplayLayerTestLayer *)layer1Delegate.layer; + layer1.displaysAsynchronously = YES; + + dispatch_semaphore_t displayAsyncLayer1Sema = dispatch_semaphore_create(0); + layer1Delegate.displayLayerBlock = ^UIImage *{ + dispatch_semaphore_wait(displayAsyncLayer1Sema, DISPATCH_TIME_FOREVER); + return bogusImage(); + }; + layer1.backgroundColor = [UIColor blackColor].CGColor; + layer1.frame = CGRectMake(0.0, 0.0, 333.0, 123.0); + [containerLayer addSublayer:layer1]; + + _ASDisplayLayerTestDelegate *layer2Delegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:delegateModes]; + _ASDisplayLayerTestLayer *layer2 = (_ASDisplayLayerTestLayer *)layer2Delegate.layer; + layer2.displaysAsynchronously = YES; + layer2.backgroundColor = [UIColor blackColor].CGColor; + layer2.frame = CGRectMake(0.0, 50.0, 97.0, 50.0); + [containerLayer addSublayer:layer2]; + + dispatch_suspend([_ASDisplayLayer displayQueue]); + + // display below if needed + [layer1 setNeedsDisplay]; + [layer2 setNeedsDisplay]; + [containerLayer setNeedsDisplay]; + [self displayLayerRecursively:containerLayer]; + + // check state before running displayQueue + XCTAssertTrue([layer1 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer1.setContentsCounts); + XCTAssertEqual(layer1.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 0u); + XCTAssertTrue([layer2 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer2.setContentsCounts); + XCTAssertEqual(layer2.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 0u); + XCTAssertEqual(containerLayer.didCompleteTransactionCount, 0u); + + // run displayQueue until async display for layer2 has been run + dispatch_resume([_ASDisplayLayer displayQueue]); + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (layer2Delegate.displayCount == 1); + })); + + // check layer1 has not had async display run + XCTAssertTrue([layer1 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer1.setContentsCounts); + XCTAssertEqual(layer1.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 0u); + // check layer2 has had async display run + XCTAssertTrue([layer2 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer2.setContentsCounts); + XCTAssertEqual(layer2.displayCount, 1u); + XCTAssertEqual(layer2Delegate.displayCount, 1u); + XCTAssertEqual(containerLayer.didCompleteTransactionCount, 0u); + + + // allow layer1 to complete display + dispatch_semaphore_signal(displayAsyncLayer1Sema); + [self waitForLayer:layer1 asyncDisplayCount:1]; + + // check that both layers have completed display + XCTAssertTrue([layer1 checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer1.setContentsCounts); + XCTAssertEqual(layer1.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 1u); + XCTAssertTrue([layer2 checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer2.setContentsCounts); + XCTAssertEqual(layer2.displayCount, 1u); + XCTAssertEqual(layer2Delegate.displayCount, 1u); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (containerLayer.didCompleteTransactionCount == 1); + })); +} + +- (void)checkSuspendResume:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDrawInContext]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateMode(_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters)]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + + // Layer shouldn't display because display is suspended + layer.displaySuspended = YES; + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + XCTAssertEqual(layer.displayCount, 0u, @"Should not have displayed because display is suspended, thus -setNeedsDisplay is a no-op"); + XCTAssertFalse(layer.needsDisplay, @"Should not need display"); + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + } + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + + // Layer should display because display is resumed + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.displaySuspended = NO; + XCTAssertTrue(layer.needsDisplay); + [layer displayIfNeeded]; + XCTAssertEqual(layer.displayCount, 1u); + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForLayer:layer asyncDisplayCount:1]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 1u); + XCTAssertFalse(layer.needsDisplay); +} + +- (void)testSuspendResumeAsync +{ + [self checkSuspendResume:YES]; +} + +- (void)testSuspendResumeSync +{ + [self checkSuspendResume:NO]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeAppearanceTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeAppearanceTests.mm new file mode 100644 index 0000000000..515bcf7cfc --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeAppearanceTests.mm @@ -0,0 +1,417 @@ +// +// ASDisplayNodeAppearanceTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +#import +#import +#import +#import + +// helper functions +IMP class_replaceMethodWithBlock(Class theClass, SEL originalSelector, id block); +IMP class_replaceMethodWithBlock(Class theClass, SEL originalSelector, id block) +{ + IMP newImplementation = imp_implementationWithBlock(block); + Method method = class_getInstanceMethod(theClass, originalSelector); + return class_replaceMethod(theClass, originalSelector, newImplementation, method_getTypeEncoding(method)); +} + +static dispatch_block_t modifyMethodByAddingPrologueBlockAndReturnCleanupBlock(Class theClass, SEL originalSelector, void (^block)(id)) +{ + __block IMP originalImp = NULL; + void (^blockActualSwizzle)(id) = ^(id swizzedSelf){ + block(swizzedSelf); + ((void(*)(id, SEL))originalImp)(swizzedSelf, originalSelector); + }; + originalImp = class_replaceMethodWithBlock(theClass, originalSelector, blockActualSwizzle); + void (^cleanupBlock)(void) = ^{ + // restore original method + Method method = class_getInstanceMethod(theClass, originalSelector); + class_replaceMethod(theClass, originalSelector, originalImp, method_getTypeEncoding(method)); + }; + return cleanupBlock; +}; + +@interface ASDisplayNode (PrivateStuffSoWeDontPullInCPPInternalH) +- (BOOL)__visibilityNotificationsDisabled; +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled; +- (id)initWithViewClass:(Class)viewClass; +- (id)initWithLayerClass:(Class)layerClass; +@end + +@interface ASDisplayNodeAppearanceTests : XCTestCase +@end + +// Conveniences for making nodes named a certain way +#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.debugName = @#n +#define DeclareViewNamed(v) \ + ASDisplayNode *node_##v = [[ASDisplayNode alloc] init]; \ + node_##v.debugName = @#v; \ + UIView *v = node_##v.view; + +@implementation ASDisplayNodeAppearanceTests +{ + _ASDisplayView *_view; + + NSMutableArray *_swizzleCleanupBlocks; + + NSCountedSet *_willEnterHierarchyCounts; + NSCountedSet *_didExitHierarchyCounts; + +} + +- (void)setUp +{ + [super setUp]; + + _swizzleCleanupBlocks = [[NSMutableArray alloc] init]; + + // Using this instead of mocks. Count # of times method called + _willEnterHierarchyCounts = [[NSCountedSet alloc] init]; + _didExitHierarchyCounts = [[NSCountedSet alloc] init]; + + dispatch_block_t cleanupBlock = modifyMethodByAddingPrologueBlockAndReturnCleanupBlock([ASDisplayNode class], @selector(willEnterHierarchy), ^(id blockSelf){ + [_willEnterHierarchyCounts addObject:blockSelf]; + }); + [_swizzleCleanupBlocks addObject:cleanupBlock]; + cleanupBlock = modifyMethodByAddingPrologueBlockAndReturnCleanupBlock([ASDisplayNode class], @selector(didExitHierarchy), ^(id blockSelf){ + [_didExitHierarchyCounts addObject:blockSelf]; + }); + [_swizzleCleanupBlocks addObject:cleanupBlock]; +} + +- (void)tearDown +{ + [super tearDown]; + + for(dispatch_block_t cleanupBlock in _swizzleCleanupBlocks) { + cleanupBlock(); + } + _swizzleCleanupBlocks = nil; + _willEnterHierarchyCounts = nil; + _didExitHierarchyCounts = nil; +} + +- (void)testAppearanceMethodsCalledWithRootNodeInWindowLayer +{ + [self checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:YES]; +} + +- (void)testAppearanceMethodsCalledWithRootNodeInWindowView +{ + [self checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:NO]; +} + +- (void)checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:(BOOL)isLayerBacked +{ + // ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it. + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + + DeclareNodeNamed(n); + DeclareViewNamed(superview); + + n.layerBacked = isLayerBacked; + + if (isLayerBacked) { + [superview.layer addSublayer:n.layer]; + } else { + [superview addSubview:n.view]; + } + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:n], 0u, @"willEnterHierarchy erroneously called"); + XCTAssertEqual([_didExitHierarchyCounts countForObject:n], 0u, @"didExitHierarchy erroneously called"); + + [window addSubview:superview]; + XCTAssertEqual([_willEnterHierarchyCounts countForObject:n], 1u, @"willEnterHierarchy not called when node's view added to hierarchy"); + XCTAssertEqual([_didExitHierarchyCounts countForObject:n], 0u, @"didExitHierarchy erroneously called"); + + XCTAssertTrue(n.inHierarchy, @"Node should be visible"); + + if (isLayerBacked) { + [n.layer removeFromSuperlayer]; + } else { + [n.view removeFromSuperview]; + } + + XCTAssertFalse(n.inHierarchy, @"Node should be not visible"); + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:n], 1u, @"willEnterHierarchy not called when node's view added to hierarchy"); + XCTAssertEqual([_didExitHierarchyCounts countForObject:n], 1u, @"didExitHierarchy erroneously called"); +} + +- (void)checkManualAppearanceViewLoaded:(BOOL)isViewLoaded layerBacked:(BOOL)isLayerBacked +{ + // ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it. + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(aa); + DeclareNodeNamed(ab); + + for (ASDisplayNode *n in @[parent, a, b, aa, ab]) { + n.layerBacked = isLayerBacked; + if (isViewLoaded) + [n layer]; + } + + [parent addSubnode:a]; + + XCTAssertFalse(parent.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(a.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(b.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(aa.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(ab.inHierarchy, @"Nothing should be visible"); + + if (isLayerBacked) { + [window.layer addSublayer:parent.layer]; + } else { + [window addSubview:parent.view]; + } + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:parent], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:a], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:b], 0u, @"Should not have appeared yet"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:aa], 0u, @"Should not have appeared yet"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:ab], 0u, @"Should not have appeared yet"); + + XCTAssertTrue(parent.inHierarchy, @"Should be visible"); + XCTAssertTrue(a.inHierarchy, @"Should be visible"); + XCTAssertFalse(b.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(aa.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(ab.inHierarchy, @"Nothing should be visible"); + + // Add to an already-visible node should make the node visible + [parent addSubnode:b]; + [a insertSubnode:aa atIndex:0]; + [a insertSubnode:ab aboveSubnode:aa]; + + XCTAssertTrue(parent.inHierarchy, @"Should be visible"); + XCTAssertTrue(a.inHierarchy, @"Should be visible"); + XCTAssertTrue(b.inHierarchy, @"Should be visible after adding to visible parent"); + XCTAssertTrue(aa.inHierarchy, @"Nothing should be visible"); + XCTAssertTrue(ab.inHierarchy, @"Nothing should be visible"); + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:parent], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:a], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:b], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:aa], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:ab], 1u, @"Should have -willEnterHierarchy called once"); + + if (isLayerBacked) { + [parent.layer removeFromSuperlayer]; + } else { + [parent.view removeFromSuperview]; + } + + XCTAssertFalse(parent.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(a.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(b.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(aa.inHierarchy, @"Nothing should be visible"); + XCTAssertFalse(ab.inHierarchy, @"Nothing should be visible"); +} + +- (void)testAppearanceMethodsNoLayer +{ + [self checkManualAppearanceViewLoaded:NO layerBacked:YES]; +} + +- (void)testAppearanceMethodsNoView +{ + [self checkManualAppearanceViewLoaded:NO layerBacked:NO]; +} + +- (void)testAppearanceMethodsLayer +{ + [self checkManualAppearanceViewLoaded:YES layerBacked:YES]; +} + +- (void)testAppearanceMethodsView +{ + [self checkManualAppearanceViewLoaded:YES layerBacked:NO]; +} + +- (void)testSynchronousIntermediaryView +{ + // Parent is a wrapper node for a scrollview + ASDisplayNode *parentSynchronousNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]]; + DeclareNodeNamed(layerBackedNode); + DeclareNodeNamed(viewBackedNode); + + layerBackedNode.layerBacked = YES; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + [parentSynchronousNode addSubnode:layerBackedNode]; + [parentSynchronousNode addSubnode:viewBackedNode]; + + XCTAssertFalse(parentSynchronousNode.inHierarchy, @"Should not yet be visible"); + XCTAssertFalse(layerBackedNode.inHierarchy, @"Should not yet be visible"); + XCTAssertFalse(viewBackedNode.inHierarchy, @"Should not yet be visible"); + + [window addSubview:parentSynchronousNode.view]; + + // This is a known case that isn't supported + XCTAssertFalse(parentSynchronousNode.inHierarchy, @"Synchronous views are not currently marked visible"); + + XCTAssertTrue(layerBackedNode.inHierarchy, @"Synchronous views' subviews should get marked visible"); + XCTAssertTrue(viewBackedNode.inHierarchy, @"Synchronous views' subviews should get marked visible"); + + // Try moving a node to/from a synchronous node in the window with the node API + // Setup + [layerBackedNode removeFromSupernode]; + [viewBackedNode removeFromSupernode]; + XCTAssertFalse(layerBackedNode.inHierarchy, @"aoeu"); + XCTAssertFalse(viewBackedNode.inHierarchy, @"aoeu"); + + // now move to synchronous node + [parentSynchronousNode addSubnode:layerBackedNode]; + [parentSynchronousNode insertSubnode:viewBackedNode aboveSubnode:layerBackedNode]; + XCTAssertTrue(layerBackedNode.inHierarchy, @"Synchronous views' subviews should get marked visible"); + XCTAssertTrue(viewBackedNode.inHierarchy, @"Synchronous views' subviews should get marked visible"); + + [parentSynchronousNode.view removeFromSuperview]; + + XCTAssertFalse(parentSynchronousNode.inHierarchy, @"Should not have changed"); + XCTAssertFalse(layerBackedNode.inHierarchy, @"Should have been marked invisible when synchronous superview was removed from the window"); + XCTAssertFalse(viewBackedNode.inHierarchy, @"Should have been marked invisible when synchronous superview was removed from the window"); +} + +- (void)checkMoveAcrossHierarchyLayerBacked:(BOOL)isLayerBacked useManualCalls:(BOOL)useManualDisable useNodeAPI:(BOOL)useNodeAPI +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + + DeclareNodeNamed(parentA); + DeclareNodeNamed(parentB); + DeclareNodeNamed(child); + DeclareNodeNamed(childSubnode); + + for (ASDisplayNode *n in @[parentA, parentB, child, childSubnode]) { + n.layerBacked = isLayerBacked; + } + + [parentA addSubnode:child]; + [child addSubnode:childSubnode]; + + XCTAssertFalse(parentA.inHierarchy, @"Should not yet be visible"); + XCTAssertFalse(parentB.inHierarchy, @"Should not yet be visible"); + XCTAssertFalse(child.inHierarchy, @"Should not yet be visible"); + XCTAssertFalse(childSubnode.inHierarchy, @"Should not yet be visible"); + XCTAssertFalse(childSubnode.inHierarchy, @"Should not yet be visible"); + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:child], 0u, @"Should not have -willEnterHierarchy called"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:childSubnode], 0u, @"Should not have -willEnterHierarchy called"); + + if (isLayerBacked) { + [window.layer addSublayer:parentA.layer]; + [window.layer addSublayer:parentB.layer]; + } else { + [window addSubview:parentA.view]; + [window addSubview:parentB.view]; + } + + XCTAssertTrue(parentA.inHierarchy, @"Should be visible after added to window"); + XCTAssertTrue(parentB.inHierarchy, @"Should be visible after added to window"); + XCTAssertTrue(child.inHierarchy, @"Should be visible after parent added to window"); + XCTAssertTrue(childSubnode.inHierarchy, @"Should be visible after parent added to window"); + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:child], 1u, @"Should have -willEnterHierarchy called once"); + XCTAssertEqual([_willEnterHierarchyCounts countForObject:childSubnode], 1u, @"Should have -willEnterHierarchy called once"); + + // Move subnode from A to B + if (useManualDisable) { + ASDisplayNodeDisableHierarchyNotifications(child); + } + if (!useNodeAPI) { + [child removeFromSupernode]; + [parentB addSubnode:child]; + } else { + [parentB addSubnode:child]; + } + if (useManualDisable) { + XCTAssertTrue([child __visibilityNotificationsDisabled], @"Should not have re-enabled yet"); + XCTAssertTrue([child __selfOrParentHasVisibilityNotificationsDisabled], @"Should not have re-enabled yet"); + ASDisplayNodeEnableHierarchyNotifications(child); + } + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:child], 1u, @"Should not have -willEnterHierarchy called when moving child around in hierarchy"); + + // Move subnode back to A + if (useManualDisable) { + ASDisplayNodeDisableHierarchyNotifications(child); + } + if (!useNodeAPI) { + [child removeFromSupernode]; + [parentA insertSubnode:child atIndex:0]; + } else { + [parentA insertSubnode:child atIndex:0]; + } + if (useManualDisable) { + XCTAssertTrue([child __visibilityNotificationsDisabled], @"Should not have re-enabled yet"); + XCTAssertTrue([child __selfOrParentHasVisibilityNotificationsDisabled], @"Should not have re-enabled yet"); + ASDisplayNodeEnableHierarchyNotifications(child); + } + + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:child], 1u, @"Should not have -willEnterHierarchy called when moving child around in hierarchy"); + + // Finally, remove subnode + [child removeFromSupernode]; + + XCTAssertEqual([_willEnterHierarchyCounts countForObject:child], 1u, @"Should appear and disappear just once"); + + // Make sure that we don't leave these unbalanced + XCTAssertFalse([child __visibilityNotificationsDisabled], @"Unbalanced visibility notifications calls"); + XCTAssertFalse([child __selfOrParentHasVisibilityNotificationsDisabled], @"Should not have re-enabled yet"); +} + +- (void)testMoveAcrossHierarchyLayer +{ + [self checkMoveAcrossHierarchyLayerBacked:YES useManualCalls:NO useNodeAPI:YES]; +} + +- (void)testMoveAcrossHierarchyView +{ + [self checkMoveAcrossHierarchyLayerBacked:NO useManualCalls:NO useNodeAPI:YES]; +} + +- (void)testMoveAcrossHierarchyManualLayer +{ + [self checkMoveAcrossHierarchyLayerBacked:YES useManualCalls:YES useNodeAPI:NO]; +} + +- (void)testMoveAcrossHierarchyManualView +{ + [self checkMoveAcrossHierarchyLayerBacked:NO useManualCalls:YES useNodeAPI:NO]; +} + +- (void)testDisableWithNodeAPILayer +{ + [self checkMoveAcrossHierarchyLayerBacked:YES useManualCalls:YES useNodeAPI:YES]; +} + +- (void)testDisableWithNodeAPIView +{ + [self checkMoveAcrossHierarchyLayerBacked:NO useManualCalls:YES useNodeAPI:YES]; +} + +- (void)testPreventManualAppearanceMethods +{ + DeclareNodeNamed(n); + + XCTAssertThrows([n willEnterHierarchy], @"Should not allow manually calling appearance methods."); + XCTAssertThrows([n didExitHierarchy], @"Should not allow manually calling appearance methods."); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeExtrasTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeExtrasTests.mm new file mode 100644 index 0000000000..77d904c2e6 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeExtrasTests.mm @@ -0,0 +1,76 @@ +// +// ASDisplayNodeExtrasTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@interface ASDisplayNodeExtrasTests : XCTestCase + +@end + +@interface TestDisplayNode : ASDisplayNode +@end + +@implementation TestDisplayNode +@end + +@implementation ASDisplayNodeExtrasTests + +- (void)testShallowFindSubnodesOfSubclass { + ASDisplayNode *supernode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer * _Nonnull{ + return [CALayer layer]; + }]; + NSUInteger count = 10; + NSMutableArray *expected = [[NSMutableArray alloc] initWithCapacity:count]; + for (NSUInteger nodeIndex = 0; nodeIndex < count; nodeIndex++) { + TestDisplayNode *node = [[TestDisplayNode alloc] initWithLayerBlock:^CALayer * _Nonnull{ + return [CALayer layer]; + }]; + [supernode addSubnode:node]; + [expected addObject:node]; + } + NSArray *found = ASDisplayNodeFindAllSubnodesOfClass(supernode, [TestDisplayNode class]); + XCTAssertEqualObjects(found, expected, @"Expecting %lu %@ nodes, found %lu", (unsigned long)count, [TestDisplayNode class], (unsigned long)found.count); +} + +- (void)testDeepFindSubnodesOfSubclass { + ASDisplayNode *supernode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer * _Nonnull{ + return [CALayer layer]; + }]; + + const NSUInteger count = 2; + const NSUInteger levels = 2; + const NSUInteger capacity = [[self class] capacityForCount:count levels:levels]; + NSMutableArray *expected = [[NSMutableArray alloc] initWithCapacity:capacity]; + + [[self class] addSubnodesToNode:supernode number:count remainingLevels:levels accumulated:expected]; + + NSArray *found = ASDisplayNodeFindAllSubnodesOfClass(supernode, [TestDisplayNode class]); + XCTAssertEqualObjects(found, expected, @"Expecting %lu %@ nodes, found %lu", (unsigned long)count, [TestDisplayNode class], (unsigned long)found.count); +} + ++ (void)addSubnodesToNode:(ASDisplayNode *)supernode number:(NSUInteger)number remainingLevels:(NSUInteger)level accumulated:(inout NSMutableArray *)expected { + if (level == 0) return; + for (NSUInteger nodeIndex = 0; nodeIndex < number; nodeIndex++) { + TestDisplayNode *node = [[TestDisplayNode alloc] initWithLayerBlock:^CALayer * _Nonnull{ + return [CALayer layer]; + }]; + [supernode addSubnode:node]; + [expected addObject:node]; + [self addSubnodesToNode:node number:number remainingLevels:(level - 1) accumulated:expected]; + } +} + +// Graph theory is failing me atm. ++ (NSUInteger)capacityForCount:(NSUInteger)count levels:(NSUInteger)level { + if (level == 0) return 0; + return pow(count, level) + [self capacityForCount:count levels:(level - 1)]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeImplicitHierarchyTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeImplicitHierarchyTests.mm new file mode 100644 index 0000000000..ea41a322b3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeImplicitHierarchyTests.mm @@ -0,0 +1,329 @@ +// +// ASDisplayNodeImplicitHierarchyTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import "ASDisplayNodeTestsHelper.h" + +@interface ASSpecTestDisplayNode : ASDisplayNode + +/** + Simple state identifier to allow control of current spec inside of the layoutSpecBlock + */ +@property (nonatomic) NSNumber *layoutState; + +@end + +@implementation ASSpecTestDisplayNode + +- (instancetype)init +{ + self = [super init]; + if (self) { + _layoutState = @1; + } + return self; +} + +@end + +@interface ASDisplayNodeImplicitHierarchyTests : XCTestCase + +@end + +@implementation ASDisplayNodeImplicitHierarchyTests + +- (void)testFeatureFlag +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + XCTAssertFalse(node.automaticallyManagesSubnodes); + + node.automaticallyManagesSubnodes = YES; + XCTAssertTrue(node.automaticallyManagesSubnodes); +} + +- (void)testInitialNodeInsertionWithOrdering +{ + static CGSize kSize = {100, 100}; + + ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node3 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node4 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node5 = [[ASDisplayNode alloc] init]; + + + // As we will involve a stack spec we have to give the nodes an intrinsic content size + node1.style.preferredSize = kSize; + node2.style.preferredSize = kSize; + node3.style.preferredSize = kSize; + node4.style.preferredSize = kSize; + node5.style.preferredSize = kSize; + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { + ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node4]]; + + ASStackLayoutSpec *stack1 = [[ASStackLayoutSpec alloc] init]; + [stack1 setChildren:@[node1, node2]]; + + ASStackLayoutSpec *stack2 = [[ASStackLayoutSpec alloc] init]; + [stack2 setChildren:@[node3, absoluteLayout]]; + + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[stack1, stack2, node5]]; + }; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node.view layoutIfNeeded]; + + XCTAssertEqual(node.subnodes[0], node1); + XCTAssertEqual(node.subnodes[1], node2); + XCTAssertEqual(node.subnodes[2], node3); + XCTAssertEqual(node.subnodes[3], node4); + XCTAssertEqual(node.subnodes[4], node5); +} + +- (void)testInitialNodeInsertionWhenEnterPreloadState +{ + static CGSize kSize = {100, 100}; + + static NSInteger subnodeCount = 5; + NSMutableArray *subnodes = [NSMutableArray arrayWithCapacity:subnodeCount]; + for (NSInteger i = 0; i < subnodeCount; i++) { + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + // As we will involve a stack spec we have to give the nodes an intrinsic content size + subnode.style.preferredSize = kSize; + [subnodes addObject:subnode]; + } + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + [node setHierarchyState:ASHierarchyStateRangeManaged]; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { + ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[subnodes[3]]]; + + ASStackLayoutSpec *stack1 = [[ASStackLayoutSpec alloc] init]; + [stack1 setChildren:@[subnodes[0], subnodes[1]]]; + + ASStackLayoutSpec *stack2 = [[ASStackLayoutSpec alloc] init]; + [stack2 setChildren:@[subnodes[2], absoluteLayout]]; + + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[stack1, stack2, subnodes[4]]]; + }; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node recursivelySetInterfaceState:ASInterfaceStatePreload]; + + ASCATransactionQueueWait(nil); + // No premature view allocation + XCTAssertFalse(node.isNodeLoaded); + // Subnodes should be inserted, laid out and entered preload state + XCTAssertTrue([subnodes isEqualToArray:node.subnodes]); + for (NSInteger i = 0; i < subnodeCount; i++) { + ASDisplayNode *subnode = subnodes[i]; + XCTAssertTrue(CGSizeEqualToSize(kSize, subnode.bounds.size)); + XCTAssertTrue(ASInterfaceStateIncludesPreload(subnode.interfaceState)); + } +} + +- (void)testCalculatedLayoutHierarchyTransitions +{ + static CGSize kSize = {100, 100}; + + ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node3 = [[ASDisplayNode alloc] init]; + + node1.debugName = @"a"; + node2.debugName = @"b"; + node3.debugName = @"c"; + + // As we will involve a stack spec we have to give the nodes an intrinsic content size + node1.style.preferredSize = kSize; + node2.style.preferredSize = kSize; + node3.style.preferredSize = kSize; + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize){ + ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; + if ([strongNode.layoutState isEqualToNumber:@1]) { + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node1, node2]]; + } else { + ASStackLayoutSpec *stackLayout = [[ASStackLayoutSpec alloc] init]; + [stackLayout setChildren:@[node3, node2]]; + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node1, stackLayout]]; + } + }; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node1); + XCTAssertEqual(node.subnodes[1], node2); + + node.layoutState = @2; + [node setNeedsLayout]; // After a state change the layout needs to be invalidated + [node.view layoutIfNeeded]; // A new layout pass will trigger the hiearchy transition + + XCTAssertEqual(node.subnodes[0], node1); + XCTAssertEqual(node.subnodes[1], node3); + XCTAssertEqual(node.subnodes[2], node2); +} + +// Disable test for now as we disabled the assertion +//- (void)testLayoutTransitionWillThrowForManualSubnodeManagement +//{ +// ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; +// node1.name = @"node1"; +// +// ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; +// node.automaticallyManagesSubnodes = YES; +// node.layoutSpecBlock = ^ASLayoutSpec *(ASDisplayNode *weakNode, ASSizeRange constrainedSize){ +// return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node1]]; +// }; +// +// XCTAssertNoThrow([node layoutThatFits:ASSizeRangeMake(CGSizeZero)]); +// XCTAssertThrows([node1 removeFromSupernode]); +//} + +- (void)testLayoutTransitionMeasurementCompletionBlockIsCalledOnMainThread +{ + const CGSize kSize = CGSizeMake(100, 100); + + ASDisplayNode *displayNode = [[ASDisplayNode alloc] init]; + displayNode.style.preferredSize = kSize; + + // Trigger explicit view creation to be able to use the Transition API + [displayNode view]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call measurement completion block on main"]; + + [displayNode transitionLayoutWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)) animated:YES shouldMeasureAsync:YES measurementCompletion:^{ + XCTAssertTrue(ASDisplayNodeThreadIsMain(), @"Measurement completion block should be called on main thread"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2.0 handler:nil]; +} + +- (void)testMeasurementInBackgroundThreadWithLoadedNode +{ + const CGSize kNodeSize = CGSizeMake(100, 100); + ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.style.preferredSize = kNodeSize; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { + ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; + if ([strongNode.layoutState isEqualToNumber:@1]) { + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node1]]; + } else { + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node2]]; + } + }; + + // Intentionally trigger view creation + [node view]; + [node2 view]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout also if one node is already loaded"]; + + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Measurement happens in the background + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + + // Dispatch back to the main thread to let the insertion / deletion of subnodes happening + dispatch_async(dispatch_get_main_queue(), ^{ + + // Layout on main + [node setNeedsLayout]; + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node1); + + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Change state and measure in the background + node.layoutState = @2; + [node setNeedsLayout]; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + + // Dispatch back to the main thread to let the insertion / deletion of subnodes happening + dispatch_async(dispatch_get_main_queue(), ^{ + + // Layout on main again + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node2); + + [expectation fulfill]; + }); + }); + }); + }); + + [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { + if (error) { + NSLog(@"Timeout Error: %@", error); + } + }]; +} + +- (void)testTransitionLayoutWithAnimationWithLoadedNodes +{ + const CGSize kNodeSize = CGSizeMake(100, 100); + ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + node.style.preferredSize = kNodeSize; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { + ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; + if ([strongNode.layoutState isEqualToNumber:@1]) { + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node1]]; + } else { + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[node2]]; + } + }; + + // Intentionally trigger view creation + [node1 view]; + [node2 view]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout transition also if one node is already loaded"]; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node1); + + node.layoutState = @2; + [node invalidateCalculatedLayout]; + [node transitionLayoutWithAnimation:YES shouldMeasureAsync:YES measurementCompletion:^{ + // Push this to the next runloop to let async insertion / removing of nodes finished before checking + dispatch_async(dispatch_get_main_queue(), ^{ + XCTAssertEqual(node.subnodes[0], node2); + [expectation fulfill]; + }); + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { + if (error) { + NSLog(@"Timeout Error: %@", error); + } + }]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeLayoutTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeLayoutTests.mm new file mode 100644 index 0000000000..582e141634 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeLayoutTests.mm @@ -0,0 +1,175 @@ +// +// ASDisplayNodeLayoutTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASXCTExtensions.h" +#import +#import +#import + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +@interface ASDisplayNodeLayoutTests : XCTestCase +@end + +@implementation ASDisplayNodeLayoutTests + +- (void)testMeasureOnLayoutIfNotHappenedBefore +{ + CGSize nodeSize = CGSizeMake(100, 100); + + ASDisplayNode *displayNode = [[ASDisplayNode alloc] init]; + displayNode.style.width = ASDimensionMake(100); + displayNode.style.height = ASDimensionMake(100); + + // Use a button node in here as ASButtonNode uses layoutSpecThatFits: + ASButtonNode *buttonNode = [ASButtonNode new]; + [displayNode addSubnode:buttonNode]; + + displayNode.frame = {.size = nodeSize}; + buttonNode.frame = {.size = nodeSize}; + + ASXCTAssertEqualSizes(displayNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); + ASXCTAssertEqualSizes(buttonNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); + + // Trigger view creation and layout pass without a manual -layoutThatFits: call before so the automatic measurement + // pass will trigger in the layout pass + [displayNode.view layoutIfNeeded]; + + ASXCTAssertEqualSizes(displayNode.calculatedSize, nodeSize, @"Automatic measurement pass should have happened in layout pass"); + ASXCTAssertEqualSizes(buttonNode.calculatedSize, nodeSize, @"Automatic measurement pass should have happened in layout pass"); +} + +#if DEBUG +- (void)testNotAllowAddingSubnodesInLayoutSpecThatFits +{ + ASDisplayNode *displayNode = [ASDisplayNode new]; + ASDisplayNode *someOtherNode = [ASDisplayNode new]; + + displayNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + [node addSubnode:someOtherNode]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:someOtherNode]; + }; + + XCTAssertThrows([displayNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))], @"Should throw if subnode was added in layoutSpecThatFits:"); +} + +- (void)testNotAllowModifyingSubnodesInLayoutSpecThatFits +{ + ASDisplayNode *displayNode = [ASDisplayNode new]; + ASDisplayNode *someOtherNode = [ASDisplayNode new]; + + [displayNode addSubnode:someOtherNode]; + + displayNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + [someOtherNode removeFromSupernode]; + [node addSubnode:[ASDisplayNode new]]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:someOtherNode]; + }; + + XCTAssertThrows([displayNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))], @"Should throw if subnodes where modified in layoutSpecThatFits:"); +} +#endif + +- (void)testMeasureOnLayoutIfNotHappenedBeforeNoRemeasureForSameBounds +{ + CGSize nodeSize = CGSizeMake(100, 100); + + ASDisplayNode *displayNode = [ASDisplayNode new]; + displayNode.style.width = ASDimensionMake(nodeSize.width); + displayNode.style.height = ASDimensionMake(nodeSize.height); + + ASButtonNode *buttonNode = [ASButtonNode new]; + [displayNode addSubnode:buttonNode]; + + __block atomic_int numberOfLayoutSpecThatFitsCalls = ATOMIC_VAR_INIT(0); + displayNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + atomic_fetch_add(&numberOfLayoutSpecThatFitsCalls, 1); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:buttonNode]; + }; + + displayNode.frame = {.size = nodeSize}; + + // Trigger initial layout pass without a measurement pass before + [displayNode.view layoutIfNeeded]; + XCTAssertEqual(numberOfLayoutSpecThatFitsCalls, 1, @"Should measure during layout if not measured"); + + [displayNode layoutThatFits:ASSizeRangeMake(nodeSize, nodeSize)]; + XCTAssertEqual(numberOfLayoutSpecThatFitsCalls, 1, @"Should not remeasure with same bounds"); +} + +- (void)testThatLayoutWithInvalidSizeCausesException +{ + ASDisplayNode *displayNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.layoutSpecBlock = ^ASLayoutSpec *(ASDisplayNode *node, ASSizeRange constrainedSize) { + return [ASWrapperLayoutSpec wrapperWithLayoutElement:displayNode]; + }; + + XCTAssertThrows([node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, FLT_MAX))]); +} + +- (void)testThatLayoutCreatedWithInvalidSizeCausesException +{ + ASDisplayNode *displayNode = [[ASDisplayNode alloc] init]; + XCTAssertThrows([ASLayout layoutWithLayoutElement:displayNode size:CGSizeMake(FLT_MAX, FLT_MAX)]); + XCTAssertThrows([ASLayout layoutWithLayoutElement:displayNode size:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]); + XCTAssertThrows([ASLayout layoutWithLayoutElement:displayNode size:CGSizeMake(INFINITY, INFINITY)]); +} + +- (void)testThatLayoutElementCreatedInLayoutSpecThatFitsDoNotGetDeallocated +{ + const CGSize kSize = CGSizeMake(300, 300); + + ASDisplayNode *subNode = [[ASDisplayNode alloc] init]; + subNode.automaticallyManagesSubnodes = YES; + subNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test Test Test Test"]; + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:insetSpec]; + }; + + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + rootNode.automaticallyManagesSubnodes = YES; + rootNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test"]; + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode]; + + return [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[insetSpec, subNode]]; + }; + + rootNode.frame = CGRectMake(0, 0, kSize.width, kSize.height); + [rootNode view]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Execute measure and layout pass"]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + [rootNode layoutThatFits:ASSizeRangeMake(kSize)]; + + dispatch_async(dispatch_get_main_queue(), ^{ + XCTAssertNoThrow([rootNode.view layoutIfNeeded]); + [expectation fulfill]; + }); + }); + + [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeSnapshotTests.mm new file mode 100644 index 0000000000..489727e3ba --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeSnapshotTests.mm @@ -0,0 +1,36 @@ +// +// ASDisplayNodeSnapshotTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASSnapshotTestCase.h" +#import + +@interface ASDisplayNodeSnapshotTests : ASSnapshotTestCase + +@end + +@implementation ASDisplayNodeSnapshotTests + +- (void)testBasicHierarchySnapshotTesting +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.backgroundColor = [UIColor blueColor]; + + ASTextNode *subnode = [[ASTextNode alloc] init]; + subnode.backgroundColor = [UIColor whiteColor]; + + subnode.attributedText = [[NSAttributedString alloc] initWithString:@"Hello"]; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(5, 5, 5, 5) child:subnode]; + }; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(node, nil); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTests.mm new file mode 100644 index 0000000000..d1e1698413 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTests.mm @@ -0,0 +1,2705 @@ +// +// ASDisplayNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "ASXCTExtensions.h" +#import "ASDisplayNodeTestsHelper.h" + +// Conveniences for making nodes named a certain way +#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.debugName = @#n +#define DeclareViewNamed(v) \ + ASDisplayNode *node_##v = [[ASDisplayNode alloc] init]; \ + node_##v.debugName = @#v; \ + UIView *v = node_##v.view; +#define DeclareLayerNamed(l) \ + ASDisplayNode *node_##l = [[ASDisplayNode alloc] init]; \ + node_##l.debugName = @#l; \ + node_##l.layerBacked = YES; \ + CALayer *l = node_##l.layer; + +static NSString *orderStringFromSublayers(CALayer *l) { + return [[[l.sublayers valueForKey:@"asyncdisplaykit_node"] valueForKey:@"debugName"] componentsJoinedByString:@","]; +} + +static NSString *orderStringFromSubviews(UIView *v) { + return [[[v.subviews valueForKey:@"asyncdisplaykit_node"] valueForKey:@"debugName"] componentsJoinedByString:@","]; +} + +static NSString *orderStringFromSubnodes(ASDisplayNode *n) { + return [[n.subnodes valueForKey:@"debugName"] componentsJoinedByString:@","]; +} + +// Asserts subnode, subview, sublayer order match what you provide here +#define XCTAssertNodeSubnodeSubviewSublayerOrder(n, loaded, isLayerBacked, order, description) \ +XCTAssertEqualObjects(orderStringFromSubnodes(n), order, @"Incorrect node order for " description );\ +if (loaded) {\ + if (!isLayerBacked) {\ + XCTAssertEqualObjects(orderStringFromSubviews(n.view), order, @"Incorrect subviews for " description);\ + }\ + XCTAssertEqualObjects(orderStringFromSublayers(n.layer), order, @"Incorrect sublayers for " description);\ +} + +#define XCTAssertNodesHaveParent(parent, nodes ...) \ +for (ASDisplayNode *n in @[ nodes ]) {\ + XCTAssertEqualObjects(parent, n.supernode, @"%@ has the wrong parent", n.debugName);\ +} + +#define XCTAssertNodesLoaded(nodes ...) \ +for (ASDisplayNode *n in @[ nodes ]) {\ + XCTAssertTrue(n.nodeLoaded, @"%@ should be loaded", n.debugName);\ +} + +#define XCTAssertNodesNotLoaded(nodes ...) \ +for (ASDisplayNode *n in @[ nodes ]) {\ + XCTAssertFalse(n.nodeLoaded, @"%@ should not be loaded", n.debugName);\ +} + +@interface UIWindow (Testing) +// UIWindow has this handy method that is not public but great for testing +- (UIResponder *)firstResponder; +@end + +@interface ASDisplayNode (HackForTests) +- (id)initWithViewClass:(Class)viewClass; +- (id)initWithLayerClass:(Class)layerClass; +- (void)setInterfaceState:(ASInterfaceState)state; +// FIXME: Importing ASDisplayNodeInternal.h causes a heap of problems. +- (void)enterInterfaceState:(ASInterfaceState)interfaceState; +@end + +@interface ASTestDisplayNode : ASDisplayNode +@property (nonatomic) void (^willDeallocBlock)(__unsafe_unretained ASTestDisplayNode *node); +@property (nonatomic) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); + +@property (nonatomic, nullable) UIGestureRecognizer *gestureRecognizer; +@property (nonatomic, nullable) id idGestureRecognizer; +@property (nonatomic, nullable) UIImage *bigImage; +@property (nonatomic, nullable) NSArray *randomProperty; + +@property (nonatomic, nullable) UIGestureRecognizer *gestureRecognizer; +@property (nonatomic, nullable) id idGestureRecognizer; +@property (nonatomic, nullable) UIImage *bigImage; +@property (nonatomic, nullable) NSArray *randomProperty; + +@property (nonatomic) BOOL displayRangeStateChangedToYES; +@property (nonatomic) BOOL displayRangeStateChangedToNO; + +@property (nonatomic) BOOL hasPreloaded; +@property (nonatomic) BOOL preloadStateChangedToYES; +@property (nonatomic) BOOL preloadStateChangedToNO; + +@property (nonatomic) NSUInteger displayWillStartCount; +@property (nonatomic) NSUInteger didDisplayCount; + +@end + +@interface ASTestResponderNode : ASTestDisplayNode +@end + +@implementation ASTestDisplayNode + +- (void)setInterfaceState:(ASInterfaceState)state +{ + [super setInterfaceState:state]; + ASCATransactionQueueWait(nil); +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + return _calculateSizeBlock ? _calculateSizeBlock(self, constrainedSize) : CGSizeZero; +} + +- (void)didEnterDisplayState +{ + [super didEnterDisplayState]; + self.displayRangeStateChangedToYES = YES; +} + +- (void)didExitDisplayState +{ + [super didExitDisplayState]; + self.displayRangeStateChangedToNO = YES; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + self.preloadStateChangedToYES = YES; + self.hasPreloaded = YES; +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + self.preloadStateChangedToNO = YES; +} + +- (void)dealloc +{ + if (_willDeallocBlock) { + _willDeallocBlock(self); + } +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + _didDisplayCount++; +} + +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + [super displayWillStartAsynchronously:asynchronously]; + _displayWillStartCount++; +} + +- (CALayer *__strong (*)[NUM_CLIP_CORNER_LAYERS])clipCornerLayers +{ + return &self->_clipCornerLayers; +} + +@end + +@interface ASSynchronousTestDisplayNodeViaViewClass : ASDisplayNode +@end + +@implementation ASSynchronousTestDisplayNodeViaViewClass + ++ (Class)viewClass { + return [UIView class]; +} + +@end + +@interface ASSynchronousTestDisplayNodeViaLayerClass : ASDisplayNode +@end + +@implementation ASSynchronousTestDisplayNodeViaLayerClass + ++ (Class)layerClass { + return [CALayer class]; +} + +@end + +@interface UIDisplayNodeTestView : UIView +@end + +@interface UIResponderNodeTestView : _ASDisplayView +@property(nonatomic) BOOL testIsFirstResponder; +@end + +@implementation UIDisplayNodeTestView +@end + +@interface ASTestWindow : UIWindow +@end + +@implementation ASTestWindow + +- (id)firstResponder { + return self.subviews.firstObject; +} + +@end + +@implementation ASTestResponderNode + ++ (Class)viewClass { + return [UIResponderNodeTestView class]; +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +@end + +@implementation UIResponderNodeTestView + +- (BOOL)becomeFirstResponder { + self.testIsFirstResponder = YES; + return YES; +} + +- (BOOL)canResignFirstResponder { + return YES; +} + +- (BOOL)resignFirstResponder { + [super resignFirstResponder]; + if (self.testIsFirstResponder) { + self.testIsFirstResponder = NO; + return YES; + } + return NO; +} + +@end + +@interface ASTestResponderNodeWithOverride : ASDisplayNode +@end +@implementation ASTestResponderNodeWithOverride +- (BOOL)canBecomeFirstResponder { + return YES; +} +@end + +@interface ASTestViewController: ASViewController +@end +@implementation ASTestViewController +- (BOOL)prefersStatusBarHidden { return YES; } +@end + +@interface UIResponderNodeTestDisplayViewCallingSuper : _ASDisplayView +@end +@implementation UIResponderNodeTestDisplayViewCallingSuper +- (BOOL)canBecomeFirstResponder { return YES; } +- (BOOL)becomeFirstResponder { return [super becomeFirstResponder]; } +@end + +@interface UIResponderNodeTestViewCallingSuper : UIView +@end +@implementation UIResponderNodeTestViewCallingSuper +- (BOOL)canBecomeFirstResponder { return YES; } +- (BOOL)becomeFirstResponder { return [super becomeFirstResponder]; } +@end + +@interface ASDisplayNodeTests : XCTestCase +@end + +@implementation ASDisplayNodeTests +{ + dispatch_queue_t queue; +} + +- (void)testOverriddenNodeFirstResponderBehavior +{ + ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testOverriddenDisplayViewFirstResponderBehavior +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewClass:[UIResponderNodeTestDisplayViewCallingSuper class]]; + + // We have to add the node to a window otherwise the super responder methods call responses are undefined + // This will also create the backing view of the node + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testOverriddenViewFirstResponderBehavior +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewClass:[UIResponderNodeTestViewCallingSuper class]]; + + // We have to add the node to a window otherwise the super responder methods call responses are undefined + // This will also create the backing view of the node + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testDefaultFirstResponderBehavior +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + XCTAssertFalse([node canBecomeFirstResponder]); + XCTAssertFalse([node becomeFirstResponder]); +} + +- (void)testResponderMethodsBehavior +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASEditableTextNode *textNode = [[ASEditableTextNode alloc] init]; + + // We have to add the text node to a window otherwise the responder methods responses are undefined + // This will also create the backing view of the node + [window addSubnode:textNode]; + [window makeKeyAndVisible]; + + XCTAssertTrue([textNode canBecomeFirstResponder]); + XCTAssertTrue([textNode becomeFirstResponder]); + XCTAssertTrue([window firstResponder] == textNode.textView); + XCTAssertTrue([textNode resignFirstResponder]); + + // If the textNode resigns it's first responder the view should not be the first responder + XCTAssertTrue([window firstResponder] == nil); + XCTAssertFalse([textNode.view isFirstResponder]); +} + +- (void)testResponderOverrrideCanBecomeFirstResponder +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASTestResponderNodeWithOverride *node = [[ASTestResponderNodeWithOverride alloc] init]; + + // We have to add the text node to a window otherwise the responder methods responses are undefined + // This will also create the backing view of the node + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); + XCTAssertTrue([window firstResponder] == node.view); +} + +- (void)testUnsupportedResponderSetupWillThrow +{ + ASTestResponderNode *node = [[ASTestResponderNode alloc] init]; + [node setViewBlock:^UIView * _Nonnull{ + return [[UIView alloc] init]; + }]; + XCTAssertThrows([node view], @"Externally provided views should be synchronous"); +} + +- (void)setUp +{ + [super setUp]; + queue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDisplayNodeTestsQueue", NULL); +} + +- (void)testViewCreatedOffThreadCanBeRealizedOnThread +{ + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + }]; + + UIView *view = node.view; + XCTAssertNotNil(view, @"Getting node's view on-thread should succeed."); +} + +- (void)testNodeCreatedOffThreadWithExistingView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + return view; + }]; + }]; + + XCTAssertFalse(node.layerBacked, @"Can't be layer backed"); + XCTAssertTrue(node.synchronous, @"Node with plain view should be synchronous"); + XCTAssertFalse(node.nodeLoaded, @"Shouldn't have a view yet"); + XCTAssertEqual(view, node.view, @"Getting node's view on-thread should succeed."); +} + +- (void)testNodeCreatedOffThreadWithLazyView +{ + __block UIView *view = nil; + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + XCTAssertTrue([NSThread isMainThread], @"View block must run on the main queue"); + view = [[UIDisplayNodeTestView alloc] init]; + return view; + }]; + }]; + + XCTAssertNil(view, @"View block should not be invoked yet"); + [node view]; + XCTAssertNotNil(view, @"View block should have been invoked"); + XCTAssertEqual(view, node.view, @"Getting node's view on-thread should succeed."); + XCTAssertTrue(node.synchronous, @"Node with plain view should be synchronous"); +} + +- (void)testNodeCreatedWithLazyAsyncView +{ + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + XCTAssertTrue([NSThread isMainThread], @"View block must run on the main queue"); + return [[_ASDisplayView alloc] init]; + }]; + + XCTAssertThrows([node view], @"Externally provided views should be synchronous"); + XCTAssertTrue(node.synchronous, @"Node with externally provided view should be synchronous"); +} + +- (void)checkValuesMatchDefaults:(ASDisplayNode *)node isLayerBacked:(BOOL)isLayerBacked +{ + NSString *targetName = isLayerBacked ? @"layer" : @"view"; + NSString *hasLoadedView = node.nodeLoaded ? @"with view" : [NSString stringWithFormat:@"after loading %@", targetName]; + +// id rgbBlackCGColorIdPtr = (id)[UIColor blackColor].CGColor; + + XCTAssertEqual((id)nil, node.contents, @"default contents broken %@", hasLoadedView); + XCTAssertEqual(NO, node.clipsToBounds, @"default clipsToBounds broken %@", hasLoadedView); + XCTAssertEqual(YES, node.opaque, @"default opaque broken %@", hasLoadedView); + XCTAssertEqual(NO, node.needsDisplayOnBoundsChange, @"default needsDisplayOnBoundsChange broken %@", hasLoadedView); + XCTAssertEqual(YES, node.allowsGroupOpacity, @"default allowsGroupOpacity broken %@", hasLoadedView); + XCTAssertEqual(NO, node.allowsEdgeAntialiasing, @"default allowsEdgeAntialiasing broken %@", hasLoadedView); + XCTAssertEqual((unsigned int)(kCALayerLeftEdge | kCALayerRightEdge | kCALayerBottomEdge | kCALayerTopEdge), node.edgeAntialiasingMask, @"default edgeAntialisingMask broken %@", hasLoadedView); + XCTAssertEqual(NO, node.hidden, @"default hidden broken %@", hasLoadedView); + XCTAssertEqual(1.0f, node.alpha, @"default alpha broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.bounds), @"default bounds broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.frame), @"default frame broken %@", hasLoadedView); + XCTAssertTrue(CGPointEqualToPoint(CGPointZero, node.position), @"default position broken %@", hasLoadedView); + XCTAssertEqual((CGFloat)0.0, node.zPosition, @"default zPosition broken %@", hasLoadedView); + XCTAssertEqual(1.0f, node.contentsScale, @"default contentsScale broken %@", hasLoadedView); + XCTAssertEqual([UIScreen mainScreen].scale, node.contentsScaleForDisplay, @"default contentsScaleForDisplay broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.transform), @"default transform broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.subnodeTransform), @"default subnodeTransform broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.backgroundColor, @"default backgroundColor broken %@", hasLoadedView); + XCTAssertEqual(UIViewContentModeScaleToFill, node.contentMode, @"default contentMode broken %@", hasLoadedView); +// XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.shadowColor, @"default shadowColor broken %@", hasLoadedView); + XCTAssertEqual(0.0f, node.shadowOpacity, @"default shadowOpacity broken %@", hasLoadedView); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(0, -3), node.shadowOffset), @"default shadowOffset broken %@", hasLoadedView); + XCTAssertEqual(3.f, node.shadowRadius, @"default shadowRadius broken %@", hasLoadedView); + XCTAssertEqual(0.0f, node.borderWidth, @"default borderWidth broken %@", hasLoadedView); +// XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.borderColor, @"default borderColor broken %@", hasLoadedView); + XCTAssertEqual(NO, node.displaySuspended, @"default displaySuspended broken %@", hasLoadedView); + XCTAssertEqual(YES, node.displaysAsynchronously, @"default displaysAsynchronously broken %@", hasLoadedView); + XCTAssertEqual(NO, node.asyncdisplaykit_asyncTransactionContainer, @"default asyncdisplaykit_asyncTransactionContainer broken %@", hasLoadedView); + XCTAssertEqualObjects(nil, node.debugName, @"default name broken %@", hasLoadedView); + + XCTAssertEqual(NO, node.isAccessibilityElement, @"default isAccessibilityElement is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityLabel, @"default accessibilityLabel is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityHint, @"default accessibilityHint is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityValue, @"default accessibilityValue is broken %@", hasLoadedView); +// if (AS_AT_LEAST_IOS11) { +// XCTAssertEqual((id)nil, node.accessibilityAttributedLabel, @"default accessibilityAttributedLabel is broken %@", hasLoadedView); +// XCTAssertEqual((id)nil, node.accessibilityAttributedHint, @"default accessibilityAttributedHint is broken %@", hasLoadedView); +// XCTAssertEqual((id)nil, node.accessibilityAttributedValue, @"default accessibilityAttributedValue is broken %@", hasLoadedView); +// } + XCTAssertEqual(UIAccessibilityTraitNone, node.accessibilityTraits, @"default accessibilityTraits is broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.accessibilityFrame), @"default accessibilityFrame is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityLanguage, @"default accessibilityLanguage is broken %@", hasLoadedView); + XCTAssertEqual(NO, node.accessibilityElementsHidden, @"default accessibilityElementsHidden is broken %@", hasLoadedView); + XCTAssertEqual(NO, node.accessibilityViewIsModal, @"default accessibilityViewIsModal is broken %@", hasLoadedView); + XCTAssertEqual(NO, node.shouldGroupAccessibilityChildren, @"default shouldGroupAccessibilityChildren is broken %@", hasLoadedView); + + if (!isLayerBacked) { + XCTAssertEqual(YES, node.userInteractionEnabled, @"default userInteractionEnabled broken %@", hasLoadedView); + XCTAssertEqual(NO, node.exclusiveTouch, @"default exclusiveTouch broken %@", hasLoadedView); + XCTAssertEqual(YES, node.autoresizesSubviews, @"default autoresizesSubviews broken %@", hasLoadedView); + XCTAssertEqual(UIViewAutoresizingNone, node.autoresizingMask, @"default autoresizingMask broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsMake(8, 8, 8, 8), node.layoutMargins), @"default layoutMargins broken %@", hasLoadedView); + XCTAssertEqual(NO, node.preservesSuperviewLayoutMargins, @"default preservesSuperviewLayoutMargins broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, node.safeAreaInsets), @"default safeAreaInsets broken %@", hasLoadedView); + XCTAssertEqual(YES, node.insetsLayoutMarginsFromSafeArea, @"default insetsLayoutMarginsFromSafeArea broken %@", hasLoadedView); + } else { + XCTAssertEqual(NO, node.userInteractionEnabled, @"layer-backed nodes do not support userInteractionEnabled %@", hasLoadedView); + XCTAssertEqual(NO, node.exclusiveTouch, @"layer-backed nodes do not support exclusiveTouch %@", hasLoadedView); + } +} + +- (void)checkDefaultPropertyValuesWithLayerBacking:(BOOL)isLayerBacked +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + XCTAssertEqual(NO, node.isLayerBacked, @"default isLayerBacked broken without view"); + node.layerBacked = isLayerBacked; + XCTAssertEqual(isLayerBacked, node.isLayerBacked, @"setIsLayerBacked: broken"); + + // Assert that the values can be fetched from the node before the view is realized. + [self checkValuesMatchDefaults:node isLayerBacked:isLayerBacked]; + + [node layer]; // Force either view or layer loading + XCTAssertTrue(node.nodeLoaded, @"Didn't load view"); + + // Assert that the values can be fetched from the node after the view is realized. + [self checkValuesMatchDefaults:node isLayerBacked:isLayerBacked]; +} + +- (void)testDefaultPropertyValuesLayer +{ + [self checkDefaultPropertyValuesWithLayerBacking:YES]; +} + +- (void)testDefaultPropertyValuesView +{ + [self checkDefaultPropertyValuesWithLayerBacking:NO]; +} + +- (UIImage *)bogusImage +{ + static UIImage *bogusImage; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContext(CGSizeMake(1, 1)); + bogusImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + return bogusImage; +} + +- (void)checkValuesMatchSetValues:(ASDisplayNode *)node isLayerBacked:(BOOL)isLayerBacked +{ + NSString *targetName = isLayerBacked ? @"layer" : @"view"; + NSString *hasLoadedView = node.nodeLoaded ? @"with view" : [NSString stringWithFormat:@"after loading %@", targetName]; + + XCTAssertEqual(isLayerBacked, node.isLayerBacked, @"isLayerBacked broken %@", hasLoadedView); + XCTAssertEqualObjects((id)[self bogusImage].CGImage, (id)node.contents, @"contents broken %@", hasLoadedView); + XCTAssertEqual(YES, node.clipsToBounds, @"clipsToBounds broken %@", hasLoadedView); + XCTAssertEqual(NO, node.opaque, @"opaque broken %@", hasLoadedView); + XCTAssertEqual(YES, node.needsDisplayOnBoundsChange, @"needsDisplayOnBoundsChange broken %@", hasLoadedView); + XCTAssertEqual(NO, node.allowsGroupOpacity, @"allowsGroupOpacity broken %@", hasLoadedView); + XCTAssertEqual(YES, node.allowsEdgeAntialiasing, @"allowsEdgeAntialiasing broken %@", hasLoadedView); + XCTAssertTrue((unsigned int)(kCALayerLeftEdge | kCALayerTopEdge) == node.edgeAntialiasingMask, @"edgeAntialiasingMask broken: %@", hasLoadedView); + XCTAssertEqual(YES, node.hidden, @"hidden broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.alpha, @"alpha broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(10, 15, 42, 115.2), node.bounds), @"bounds broken %@", hasLoadedView); + XCTAssertTrue(CGPointEqualToPoint(CGPointMake(10, 65), node.position), @"position broken %@", hasLoadedView); + XCTAssertEqual((CGFloat)5.6, node.zPosition, @"zPosition broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.contentsScale, @"contentsScale broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DMakeScale(0.5, 0.5, 1.0), node.transform), @"transform broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DMakeTranslation(1337, 7357, 7007), node.subnodeTransform), @"subnodeTransform broken %@", hasLoadedView); + XCTAssertEqualObjects([UIColor clearColor], node.backgroundColor, @"backgroundColor broken %@", hasLoadedView); + XCTAssertEqual(UIViewContentModeBottom, node.contentMode, @"contentMode broken %@", hasLoadedView); + XCTAssertEqual([[UIColor cyanColor] CGColor], node.shadowColor, @"shadowColor broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.shadowOpacity, @"shadowOpacity broken %@", hasLoadedView); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(1.0f, 1.0f), node.shadowOffset), @"shadowOffset broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.shadowRadius, @"shadowRadius broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.borderWidth, @"borderWidth broken %@", hasLoadedView); + XCTAssertEqual([[UIColor orangeColor] CGColor], node.borderColor, @"borderColor broken %@", hasLoadedView); + XCTAssertEqual(YES, node.displaySuspended, @"displaySuspended broken %@", hasLoadedView); + XCTAssertEqual(NO, node.displaysAsynchronously, @"displaySuspended broken %@", hasLoadedView); + XCTAssertEqual(YES, node.asyncdisplaykit_asyncTransactionContainer, @"asyncTransactionContainer broken %@", hasLoadedView); + XCTAssertEqual(NO, node.userInteractionEnabled, @"userInteractionEnabled broken %@", hasLoadedView); + XCTAssertEqual((BOOL)!isLayerBacked, node.exclusiveTouch, @"exclusiveTouch broken %@", hasLoadedView); + XCTAssertEqualObjects(@"quack like a duck", node.debugName, @"debugName broken %@", hasLoadedView); + + XCTAssertEqual(YES, node.isAccessibilityElement, @"accessibilityElement broken %@", hasLoadedView); + XCTAssertEqualObjects(@"Ship love", node.accessibilityLabel, @"accessibilityLabel broken %@", hasLoadedView); + XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityHint, @"accessibilityHint broken %@", hasLoadedView); + XCTAssertEqualObjects(@"1 of 2", node.accessibilityValue, @"accessibilityValue broken %@", hasLoadedView); + + // setting the accessibilityLabel, accessibilityHint and accessibilityValue is supposed to be bridged to the attributed versions +// if (AS_AT_LEAST_IOS11) { +// XCTAssertEqualObjects(@"Ship love", node.accessibilityAttributedLabel.string, @"accessibilityAttributedLabel is broken %@", hasLoadedView); +// XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityAttributedHint.string, @"accessibilityAttributedHint is broken %@", hasLoadedView); +// XCTAssertEqualObjects(@"1 of 2", node.accessibilityAttributedValue.string, @"accessibilityAttributedValue is broken %@", hasLoadedView); +// } + XCTAssertEqual(UIAccessibilityTraitSelected | UIAccessibilityTraitButton, node.accessibilityTraits, @"accessibilityTraits broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(1, 2, 3, 4), node.accessibilityFrame), @"accessibilityFrame broken %@", hasLoadedView); + XCTAssertEqualObjects(@"mas", node.accessibilityLanguage, @"accessibilityLanguage broken %@", hasLoadedView); + XCTAssertEqual(YES, node.accessibilityElementsHidden, @"accessibilityElementsHidden broken %@", hasLoadedView); + XCTAssertEqual(YES, node.accessibilityViewIsModal, @"accessibilityViewIsModal broken %@", hasLoadedView); + XCTAssertEqual(YES, node.shouldGroupAccessibilityChildren, @"shouldGroupAccessibilityChildren broken %@", hasLoadedView); + XCTAssertEqual(UIAccessibilityNavigationStyleSeparate, node.accessibilityNavigationStyle, @"accessibilityNavigationStyle broken %@", hasLoadedView); + XCTAssertTrue(CGPointEqualToPoint(CGPointMake(1.0, 1.0), node.accessibilityActivationPoint), @"accessibilityActivationPoint broken %@", hasLoadedView); + XCTAssertNotNil(node.accessibilityPath, @"accessibilityPath broken %@", hasLoadedView); + + + if (!isLayerBacked) { + XCTAssertEqual(UIViewAutoresizingFlexibleLeftMargin, node.autoresizingMask, @"autoresizingMask %@", hasLoadedView); + XCTAssertEqual(NO, node.autoresizesSubviews, @"autoresizesSubviews broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsMake(3, 5, 8, 11), node.layoutMargins), @"layoutMargins broken %@", hasLoadedView); + XCTAssertEqual(YES, node.preservesSuperviewLayoutMargins, @"preservesSuperviewLayoutMargins broken %@", hasLoadedView); + XCTAssertEqual(NO, node.insetsLayoutMarginsFromSafeArea, @"insetsLayoutMarginsFromSafeArea broken %@", hasLoadedView); + } +} + +- (void)checkSimpleBridgePropertiesSetPropagate:(BOOL)isLayerBacked +{ + __block ASDisplayNode *node = nil; + + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + node.layerBacked = isLayerBacked; + + node.contents = (id)[self bogusImage].CGImage; + node.clipsToBounds = YES; + node.opaque = NO; + node.needsDisplayOnBoundsChange = YES; + node.allowsGroupOpacity = NO; + node.allowsEdgeAntialiasing = YES; + node.edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerTopEdge); + node.hidden = YES; + node.alpha = .5f; + node.position = CGPointMake(10, 65); + node.zPosition = 5.6; + node.bounds = CGRectMake(10, 15, 42, 115.2); + node.contentsScale = .5f; + node.transform = CATransform3DMakeScale(0.5, 0.5, 1.0); + node.subnodeTransform = CATransform3DMakeTranslation(1337, 7357, 7007); + node.backgroundColor = [UIColor clearColor]; + node.contentMode = UIViewContentModeBottom; + node.shadowColor = [[UIColor cyanColor] CGColor]; + node.shadowOpacity = .5f; + node.shadowOffset = CGSizeMake(1.0f, 1.0f); + node.shadowRadius = .5f; + node.borderWidth = .5f; + node.borderColor = [[UIColor orangeColor] CGColor]; + node.displaySuspended = YES; + node.displaysAsynchronously = NO; + node.asyncdisplaykit_asyncTransactionContainer = YES; + node.userInteractionEnabled = NO; + node.debugName = @"quack like a duck"; + + node.isAccessibilityElement = YES; + + for (int i = 0; i < 4; i++) { + if (i % 2 == 0) { + XCTAssertNoThrow(node.accessibilityLabel = nil); + XCTAssertNoThrow(node.accessibilityHint = nil); + XCTAssertNoThrow(node.accessibilityValue = nil); + } else { + node.accessibilityLabel = @"Ship love"; + node.accessibilityHint = @"Awesome things will happen"; + node.accessibilityValue = @"1 of 2"; + } + } + + node.accessibilityTraits = UIAccessibilityTraitSelected | UIAccessibilityTraitButton; + node.accessibilityFrame = CGRectMake(1, 2, 3, 4); + node.accessibilityLanguage = @"mas"; + node.accessibilityElementsHidden = YES; + node.accessibilityViewIsModal = YES; + node.shouldGroupAccessibilityChildren = YES; + node.accessibilityNavigationStyle = UIAccessibilityNavigationStyleSeparate; + node.accessibilityActivationPoint = CGPointMake(1.0, 1.0); + node.accessibilityPath = [UIBezierPath bezierPath]; + + if (!isLayerBacked) { + node.exclusiveTouch = YES; + node.autoresizesSubviews = NO; + node.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + node.insetsLayoutMarginsFromSafeArea = NO; + node.layoutMargins = UIEdgeInsetsMake(3, 5, 8, 11); + node.preservesSuperviewLayoutMargins = YES; + } + }]; + + // Assert that the values can be fetched from the node before the view is realized. + [self checkValuesMatchSetValues:node isLayerBacked:isLayerBacked]; + + // Assert that the realized view/layer have the correct values. + [node layer]; + + [self checkValuesMatchSetValues:node isLayerBacked:isLayerBacked]; + + // As a final sanity check, change a value on the realized view and ensure it is fetched through the node. + if (isLayerBacked) { + node.layer.hidden = NO; + } else { + node.view.hidden = NO; + } + XCTAssertEqual(NO, node.hidden, @"After the view is realized, the node should delegate properties to the view."); +} + +// Set each of the simple bridged UIView properties to a non-default value off-thread, then +// assert that they are correct on the node and propagated to the UIView realized on-thread. +- (void)testSimpleUIViewBridgePropertiesSetOffThreadPropagate +{ + [self checkSimpleBridgePropertiesSetPropagate:NO]; +} + +- (void)testSimpleCALayerBridgePropertiesSetOffThreadPropagate +{ + [self checkSimpleBridgePropertiesSetPropagate:YES]; +} + +- (void)testPropertiesSetOffThreadBeforeLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + node.autoresizingMask = UIViewAutoresizingFlexibleWidth; + node.userInteractionEnabled = YES; + }]; + + [self checkExternalViewAppliedPropertiesMatch:node]; +} + +- (void)testPropertiesSetOnThreadAfterLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + + // Load the backing view first + [node view]; + + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + node.autoresizingMask = UIViewAutoresizingFlexibleWidth; + node.userInteractionEnabled = YES; + + [self checkExternalViewAppliedPropertiesMatch:node]; +} + +- (void)checkExternalViewAppliedPropertiesMatch:(ASDisplayNode *)node +{ + UIView *view = node.view; + + XCTAssertEqualObjects([UIColor blueColor], view.backgroundColor, @"backgroundColor not propagated to view"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(10, 20, 30, 40), view.frame), @"frame not propagated to view"); + XCTAssertEqual(UIViewAutoresizingFlexibleWidth, view.autoresizingMask, @"autoresizingMask not propagated to view"); + XCTAssertEqual(YES, view.userInteractionEnabled, @"userInteractionEnabled not propagated to view"); +} + +- (void)testPropertiesSetOffThreadBeforeLoadingExternalLayer +{ + CALayer *layer = [[CAShapeLayer alloc] init]; + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithLayerBlock:^{ + return layer; + }]; + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + }]; + + [self checkExternalLayerAppliedPropertiesMatch:node]; +} + +- (void)testPropertiesSetOnThreadAfterLoadingExternalLayer +{ + CALayer *layer = [[CAShapeLayer alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithLayerBlock:^{ + return layer; + }]; + + // Load the backing layer first + [node layer]; + + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + + [self checkExternalLayerAppliedPropertiesMatch:node]; +} + +- (void)checkExternalLayerAppliedPropertiesMatch:(ASDisplayNode *)node +{ + CALayer *layer = node.layer; + + XCTAssertTrue(CGColorEqualToColor([UIColor blueColor].CGColor, layer.backgroundColor), @"backgroundColor not propagated to layer"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(10, 20, 30, 40), layer.frame), @"frame not propagated to layer"); +} + + +// Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. +- (void)testDeriveFrameFromBoundsPositionAnchorPoint +{ + UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero]; + plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f); + plainView.layer.position = CGPointMake(10, 20); + plainView.layer.bounds = CGRectMake(0, 0, 60, 80); + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + node.anchorPoint = CGPointMake(0.25f, 0.75f); + node.bounds = CGRectMake(0, 0, 60, 80); + node.position = CGPointMake(10, 20); + }]; + + XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.frame), @"Node frame should match UIView frame before realization."); + XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.view.frame), @"Realized view frame should match UIView frame."); +} + +// Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. +- (void)testSetFrameSetsBoundsPosition +{ + UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero]; + plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f); + plainView.layer.frame = CGRectMake(10, 20, 60, 80); + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + node.anchorPoint = CGPointMake(0.25f, 0.75f); + node.frame = CGRectMake(10, 20, 60, 80); + }]; + + XCTAssertTrue(CGPointEqualToPoint(plainView.layer.position, node.position), @"Node position should match UIView position before realization."); + XCTAssertTrue(CGRectEqualToRect(plainView.layer.bounds, node.bounds), @"Node bounds should match UIView bounds before realization."); + XCTAssertTrue(CGPointEqualToPoint(plainView.layer.position, node.view.layer.position), @"Realized view position should match UIView position before realization."); + XCTAssertTrue(CGRectEqualToRect(plainView.layer.bounds, node.view.layer.bounds), @"Realized view bounds should match UIView bounds before realization."); +} + +- (void)testDisplayNodePointConversionWithFrames +{ + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + + // Setup + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point *FROM* outer node's coordinate space to inner node's coordinate space + node.frame = CGRectMake(100, 100, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(105, 105); + correctPoint = CGPointMake(95, 95); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point *FROM* inner node's coordinate space to outer node's coordinate space + node.frame = CGRectMake(100, 100, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(15, 15); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point in inner node's coordinate space *TO* outer node's coordinate space + node.frame = CGRectMake(100, 100, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(95, 95); + correctPoint = CGPointMake(105, 105); + convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point in outer node's coordinate space *TO* inner node's coordinate space + node.frame = CGRectMake(0, 0, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(-5, -5); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +// Test conversions when bounds is not null. +// NOTE: Esoteric values were picked to facilitate visual inspection by demonstrating the relevance of certain numbers and lack of relevance of others +- (void)testDisplayNodePointConversionWithNonZeroBounds +{ + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + + // Setup + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point *FROM* outer node's coordinate space to inner node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(42, 42); + correctPoint = CGPointMake(36, 36); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point *FROM* inner node's coordinate space to outer node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(11, 11); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point in inner node's coordinate space *TO* outer node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(36, 36); + correctPoint = CGPointMake(42, 42); + convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point in outer node's coordinate space *TO* inner node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(11, 11); + correctPoint = CGPointMake(5, 5); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +// Test conversions when the anchorPoint is not {0.0, 0.0}. +- (void)testDisplayNodePointConversionWithNonZeroAnchorPoint +{ + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + + // Setup + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point *FROM* outer node's coordinate space to inner node's coordinate space + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.anchorPoint = CGPointMake(0.75, 1); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(42, 42); + correctPoint = CGPointMake(51, 56); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point *FROM* inner node's coordinate space to outer node's coordinate space + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.anchorPoint = CGPointMake(0.3, 0.3); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(55, 55); + correctPoint = CGPointMake(1, 1); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point in inner node's coordinate space *TO* outer node's coordinate space + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.anchorPoint = CGPointMake(0.75, 1); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(51, 56); + correctPoint = CGPointMake(42, 42); + convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + [node addSubnode:innerNode]; + + // Convert point in outer node's coordinate space *TO* inner node's coordinate space + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.anchorPoint = CGPointMake(0.3, 0.3); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(1, 1); + correctPoint = CGPointMake(55, 55); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +- (void)testDisplayNodePointConversionAgainstSelf { + ASDisplayNode *innerNode = nil; + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero; + + innerNode = [[ASDisplayNode alloc] init]; + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(105, 105); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:innerNode]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, originalPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[ASDisplayNode alloc] init]; + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(42, 42); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[ASDisplayNode alloc] init]; + innerNode.anchorPoint = CGPointMake(0.3, 0.3); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(55, 55); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[ASDisplayNode alloc] init]; + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(95, 95); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[ASDisplayNode alloc] init]; + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(36, 36); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[ASDisplayNode alloc] init]; + innerNode.anchorPoint = CGPointMake(0.75, 1); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(51, 56); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); +} + +- (void)testDisplayNodePointConversionFailureFromDisjointHierarchies +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASDisplayNode *childNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *otherNode = [[ASDisplayNode alloc] init]; + [node addSubnode:childNode]; + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero fromNode:node selfNode:childNode], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:node selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:childNode selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero fromNode:childNode selfNode:node], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:otherNode selfNode:node], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:otherNode selfNode:childNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero toNode:node selfNode:childNode], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:node selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:childNode selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero toNode:childNode selfNode:node], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:otherNode selfNode:node], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:otherNode selfNode:childNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); +} + +- (void)testDisplayNodePointConversionOnDeepHierarchies +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + // 7 deep (six below root); each one positioned at position = (1, 1) + _addTonsOfSubnodes(node, 2, 6, ^(ASDisplayNode *createdNode) { + createdNode.position = CGPointMake(1, 1); + }); + + ASDisplayNode *deepSubNode = [self _getDeepSubnodeForRoot:node withIndices:@[@1, @1, @1, @1, @1, @1]]; + + CGPoint originalPoint = CGPointMake(55, 55); + CGPoint correctPoint = CGPointMake(61, 61); + CGPoint convertedPoint = [deepSubNode convertPoint:originalPoint toNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +// Adds nodes (breadth-first rather than depth-first addition) +static void _addTonsOfSubnodes(ASDisplayNode *parent, NSUInteger fanout, NSUInteger depth, void (^onCreate)(ASDisplayNode *createdNode)) { + if (depth == 0) { + return; + } + + for (NSUInteger i = 0; i < fanout; i++) { + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + [parent addSubnode:subnode]; + onCreate(subnode); + } + for (NSUInteger i = 0; i < fanout; i++) { + _addTonsOfSubnodes(parent.subnodes[i], fanout, depth - 1, onCreate); + } +} + +// Convenience function for getting a node deep within a node hierarchy +- (ASDisplayNode *)_getDeepSubnodeForRoot:(ASDisplayNode *)root withIndices:(NSArray *)indexArray { + if ([indexArray count] == 0) { + return root; + } + + NSArray *subnodes = root.subnodes; + if ([subnodes count] == 0) { + XCTFail(@"Node hierarchy isn't deep enough for given index array"); + } + + NSUInteger index = [indexArray[0] unsignedIntegerValue]; + NSArray *otherIndices = [indexArray subarrayWithRange:NSMakeRange(1, [indexArray count] -1)]; + + return [self _getDeepSubnodeForRoot:subnodes[index] withIndices:otherIndices]; +} + +static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point2, CGFloat epsilon) { + CGFloat absEpsilon = fabs(epsilon); + BOOL xOK = fabs(point1.x - point2.x) < absEpsilon; + BOOL yOK = fabs(point1.y - point2.y) < absEpsilon; + return xOK && yOK; +} + +- (CGPoint)checkConvertPoint:(CGPoint)point fromNode:(ASDisplayNode *)fromNode selfNode:(ASDisplayNode *)toNode +{ + CGPoint nodeConversion = [toNode convertPoint:point fromNode:fromNode]; + + UIView *fromView = fromNode.view; + UIView *toView = toNode.view; + CGPoint viewConversion = [toView convertPoint:point fromView:fromView]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(nodeConversion, viewConversion, 0.001), @"Conversion mismatch: node: %@ view: %@", NSStringFromCGPoint(nodeConversion), NSStringFromCGPoint(viewConversion)); + return nodeConversion; +} + +- (CGPoint)checkConvertPoint:(CGPoint)point toNode:(ASDisplayNode *)toNode selfNode:(ASDisplayNode *)fromNode +{ + CGPoint nodeConversion = [fromNode convertPoint:point toNode:toNode]; + + UIView *fromView = fromNode.view; + UIView *toView = toNode.view; + CGPoint viewConversion = [fromView convertPoint:point toView:toView]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(nodeConversion, viewConversion, 0.001), @"Conversion mismatch: node: %@ view: %@", NSStringFromCGPoint(nodeConversion), NSStringFromCGPoint(viewConversion)); + return nodeConversion; +} + +- (void)executeOffThread:(void (^)(void))block +{ + __block BOOL blockExecuted = NO; + dispatch_group_t g = dispatch_group_create(); + dispatch_group_async(g, queue, ^{ + block(); + blockExecuted = YES; + }); + dispatch_group_wait(g, DISPATCH_TIME_FOREVER); + XCTAssertTrue(blockExecuted, @"Block did not finish executing. Timeout or exception?"); +} + +- (void)testReferenceCounting +{ + __weak ASTestDisplayNode *weakNode = nil; + { + NS_VALID_UNTIL_END_OF_SCOPE ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + weakNode = node; + } + XCTAssertNil(weakNode); +} + +- (void)testAddingNodeToHierarchyRetainsNode +{ + UIView *v = [[UIView alloc] initWithFrame:CGRectZero]; + __weak ASTestDisplayNode *weakNode = nil; + { + NS_VALID_UNTIL_END_OF_SCOPE ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + [v addSubview:node.view]; + weakNode = node; + } + XCTAssertNotNil(weakNode); +} + +- (void)testAddingSubnodeDoesNotCreateRetainCycle +{ + __weak ASTestDisplayNode *weakNode = nil; + __weak ASTestDisplayNode *weakSubnode = nil; + { + NS_VALID_UNTIL_END_OF_SCOPE ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + NS_VALID_UNTIL_END_OF_SCOPE ASTestDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + [node addSubnode:subnode]; + weakNode = node; + weakSubnode = subnode; + + XCTAssertNotNil(weakNode); + XCTAssertNotNil(weakSubnode); + } + XCTAssertNil(weakNode); + XCTAssertNil(weakSubnode); +} + +- (void)testThatUIKitDeallocationTrampoliningWorks +{ + NS_VALID_UNTIL_END_OF_SCOPE __weak UIGestureRecognizer *weakRecognizer = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIGestureRecognizer *weakIdRecognizer = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIView *weakView = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak CALayer *weakLayer = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak UIImage *weakImage = nil; + NS_VALID_UNTIL_END_OF_SCOPE __weak NSArray *weakArray = nil; + __block NS_VALID_UNTIL_END_OF_SCOPE ASTestDisplayNode *node = nil; + @autoreleasepool { + node = [[ASTestDisplayNode alloc] init]; + node.gestureRecognizer = [[UIGestureRecognizer alloc] init]; + node.idGestureRecognizer = [[UIGestureRecognizer alloc] init]; + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1000, 1000), YES, 1); + node.bigImage = UIGraphicsGetImageFromCurrentImageContext(); + node.randomProperty = @[ @"Hello, world!" ]; + UIGraphicsEndImageContext(); + weakImage = node.bigImage; + weakView = node.view; + weakLayer = node.layer; + weakArray = node.randomProperty; + weakIdRecognizer = node.idGestureRecognizer; + weakRecognizer = node.gestureRecognizer; + } + + [self executeOffThread:^{ + node = nil; + }]; + + XCTAssertNotNil(weakRecognizer, @"UIGestureRecognizer ivars should be deallocated on main."); + XCTAssertNotNil(weakIdRecognizer, @"UIGestureRecognizer-backed 'id' ivars should be deallocated on main."); + XCTAssertNotNil(weakView, @"UIView ivars should be deallocated on main."); + XCTAssertNotNil(weakLayer, @"CALayer ivars should be deallocated on main."); + XCTAssertNil(weakImage, @"UIImage ivars should be deallocated normally."); + XCTAssertNil(weakArray, @"NSArray ivars should be deallocated normally."); + XCTAssertNil(node); + + [self expectationForPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nonnull evaluatedObject, NSDictionary * _Nullable bindings) { + return (weakRecognizer == nil && weakIdRecognizer == nil && weakView == nil); + }] evaluatedWithObject:(id)kCFNull handler:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testSubnodes +{ + ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + ASDisplayNode *nilNode = nil; + XCTAssertThrows([parent addSubnode:nilNode], @"Don't try to add nil, but we'll deal with it in production, but throw in development."); + XCTAssertNoThrow([parent addSubnode:parent], @"Not good, test that we recover"); + XCTAssertEqual(0u, parent.subnodes.count, @"We shouldn't have any subnodes"); +} + +- (void)testReplaceSubnodeNoView +{ + [self checkReplaceSubnodeLoaded:NO layerBacked:NO]; +} + +- (void)testReplaceSubnodeNoLayer +{ + [self checkReplaceSubnodeLoaded:NO layerBacked:YES]; +} + +- (void)testReplaceSubnodeView +{ + [self checkReplaceSubnodeLoaded:YES layerBacked:NO]; +} + +- (void)testReplaceSubnodeLayer +{ + [self checkReplaceSubnodeLoaded:YES layerBacked:YES]; +} + + +- (void)checkReplaceSubnodeLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareNodeNamed(d); + + for (ASDisplayNode *n in @[parent, a, b, c, d]) { + n.layerBacked = isLayerBacked; + } + + [parent addSubnode:a]; + [parent addSubnode:b]; + [parent addSubnode:c]; + + if (loaded) { + [parent layer]; + } + + if (loaded) { + XCTAssertFalse(d.nodeLoaded, @"Should not yet be loaded"); + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // Check initial state + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"initial state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Check replace 0th + [parent replaceSubnode:a withSubnode:d]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"d,b,c", @"after replace 0th"); + XCTAssertNodesHaveParent(parent, d, b, c); + XCTAssertNodesHaveParent(nilParent, a); + if (loaded) { + XCTAssertNodesLoaded(d); + } + + [parent replaceSubnode:d withSubnode:a]; + + // Check replace 1st + [parent replaceSubnode:b withSubnode:d]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,d,c", @"Replace"); + XCTAssertNodesHaveParent(parent, a, c, d); + XCTAssertNodesHaveParent(nilParent, b); + + [parent replaceSubnode:d withSubnode:b]; + + // Check replace 2nd + [parent replaceSubnode:c withSubnode:d]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,d", @"Replace"); + XCTAssertNodesHaveParent(parent, a, b, d); + XCTAssertNodesHaveParent(nilParent, c); + + [parent replaceSubnode:d withSubnode:c]; + + //Check initial again + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"check should back to initial"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Check replace 0th with 2nd + [parent replaceSubnode:a withSubnode:c]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,b", @"After replace 0th"); + XCTAssertNodesHaveParent(parent, c, b); + XCTAssertNodesHaveParent(nilParent, a,d); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here +} + +- (void)testInsertSubnodeAtIndexView +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:YES layerBacked:NO]; +} + +- (void)testInsertSubnodeAtIndexLayer +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:YES layerBacked:YES]; +} + +- (void)testInsertSubnodeAtIndexNoView +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:NO layerBacked:NO]; +} + +- (void)testInsertSubnodeAtIndexNoLayer +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:NO layerBacked:YES]; +} + +- (void)checkInsertSubnodeAtIndexWithViewLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *v in @[parent, a, b, c]) { + v.layerBacked = isLayerBacked; + } + + // Load parent + if (loaded) { + (void)[parent layer]; + } + + // Add another subnode to test creation after parent is loaded + DeclareNodeNamed(d); + d.layerBacked = isLayerBacked; + if (loaded) { + XCTAssertFalse(d.nodeLoaded, @"Should not yet be loaded"); + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // Check initial state + XCTAssertEqual(0u, parent.subnodes.count, @"Should have the right subnode count"); + + // Check insert at 0th () => (a,b,c) + [parent insertSubnode:c atIndex:0]; + [parent insertSubnode:b atIndex:0]; + [parent insertSubnode:a atIndex:0]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"initial state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + if (loaded) { + XCTAssertNodesLoaded(a, b, c); + } else { + XCTAssertNodesNotLoaded(a, b, c); + } + + // Check insert at 1st (a,b,c) => (a,d,b,c) + [parent insertSubnode:d atIndex:1]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,d,b,c", @"initial state"); + XCTAssertNodesHaveParent(parent, a, b, c, d); + if (loaded) { + XCTAssertNodesLoaded(d); + } + + // Reset + [d removeFromSupernode]; + XCTAssertEqual(3u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"Bad removal of d"); + XCTAssertNodesHaveParent(nilParent, d); + + // Check insert at last position + [parent insertSubnode:d atIndex:3]; + + XCTAssertEqual(4u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c,d", @"insert at last position."); + XCTAssertNodesHaveParent(parent, a, b, c, d); + + // Reset + [d removeFromSupernode]; + XCTAssertEqual(3u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertEqualObjects(nilParent, d.supernode, @"d's parent is messed up"); + + // Check insert a nil node + ASDisplayNode *nilNode = nil; + XCTAssertThrows([parent insertSubnode:nilNode atIndex:0], @"Should not allow insertion of nil node. We will throw in development and deal with it in production"); + + // Check insert at invalid index + XCTAssertThrows([parent insertSubnode:d atIndex:NSNotFound], @"Should not allow insertion at invalid index"); + XCTAssertThrows([parent insertSubnode:d atIndex:-1], @"Should not allow insertion at invalid index"); + + // Should have same state as before + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"Funny business should not corrupt state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Check reordering existing subnodes with the insert API + // Move c to front + [parent insertSubnode:c atIndex:0]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,a,b", @"Move to front when already a subnode"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Move c to middle + [parent insertSubnode:c atIndex:1]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Move c to middle"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Insert c at the index it's already at + [parent insertSubnode:c atIndex:1]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Funny business should not corrupt state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Insert c at 0th when it's already in the array + [parent insertSubnode:c atIndex:2]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"Funny business should not corrupt state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here +} + +// This tests our resiliancy to having other views and layers inserted into our view or layer +- (void)testInsertSubviewAtIndexWithMeddlingViewsAndLayersViewBacked +{ + ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareViewNamed(d); + DeclareLayerNamed(e); + + [parent layer]; + + // (a,b) + [parent addSubnode:a]; + [parent addSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,b", @"Didn't match"); + + // (a,b) => (a,d,b) + [parent.view insertSubview:d aboveSubview:a.view]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,d,b", @"Didn't match"); + + // (a,d,b) => (a,e,d,b) + [parent.layer insertSublayer:e above:a.layer]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,e,d,b", @"Didn't match"); + + // (a,e,d,b) => (a,e,d,c,b) + [parent insertSubnode:c belowSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,e,d,c,b", @"Didn't match"); + + XCTAssertEqual(4u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertEqual(4u, parent.view.subviews.count, @"Should have the right subview count"); + XCTAssertEqual(5u, parent.layer.sublayers.count, @"Should have the right sublayer count"); + + [e removeFromSuperlayer]; + XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count"); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here +} + +- (void)testAppleBugInsertSubview +{ + DeclareViewNamed(parent); + + DeclareLayerNamed(aa); + DeclareLayerNamed(ab); + DeclareViewNamed(a); + DeclareLayerNamed(ba); + DeclareLayerNamed(bb); + DeclareLayerNamed(bc); + DeclareLayerNamed(bd); + DeclareViewNamed(c); + DeclareViewNamed(d); + DeclareLayerNamed(ea); + DeclareLayerNamed(eb); + DeclareLayerNamed(ec); + + [parent.layer addSublayer:aa]; + [parent.layer addSublayer:ab]; + [parent addSubview:a]; + [parent.layer addSublayer:ba]; + [parent.layer addSublayer:bb]; + [parent.layer addSublayer:bc]; + [parent.layer addSublayer:bd]; + [parent addSubview:d]; + [parent.layer addSublayer:ea]; + [parent.layer addSublayer:eb]; + [parent.layer addSublayer:ec]; + + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"aa,ab,a,ba,bb,bc,bd,d,ea,eb,ec", @"Should be in order"); + + // Should insert at SUBVIEW index 1, right?? + [parent insertSubview:c atIndex:1]; + + // You would think that this would be true, but instead it inserts it at the SUBLAYER index 1 +// XCTAssertEquals([parent.subviews indexOfObjectIdenticalTo:c], 1u, @"Should have index 1 after insert"); +// XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"aa,ab,a,ba,bb,bc,bd,c,d,ea,eb,ec", @"Should be in order"); + + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"aa,c,ab,a,ba,bb,bc,bd,d,ea,eb,ec", @"Apple has fixed insertSubview:atIndex:. You must update insertSubnode: etc. APIS to accomidate this."); +} + +// This tests our resiliancy to having other views and layers inserted into our view or layer +- (void)testInsertSubviewAtIndexWithMeddlingView +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareViewNamed(d); + + [parent layer]; + + // (a,b) + [parent addSubnode:a]; + [parent addSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,b", @"Didn't match"); + + // (a,b) => (a,d,b) + [parent.view insertSubview:d aboveSubview:a.view]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,d,b", @"Didn't match"); + + // (a,d,b) => (a,d,>c<,b) + [parent insertSubnode:c belowSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,d,c,b", @"Didn't match"); + + XCTAssertEqual(4u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertEqual(4u, parent.view.subviews.count, @"Should have the right subview count"); + XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count"); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here +} + + +- (void)testInsertSubnodeBelowWithView +{ + [self checkInsertSubnodeBelowWithView:YES layerBacked:NO]; +} + +- (void)testInsertSubnodeBelowWithNoView +{ + [self checkInsertSubnodeBelowWithView:NO layerBacked:NO]; +} + +- (void)testInsertSubnodeBelowWithNoLayer +{ + [self checkInsertSubnodeBelowWithView:NO layerBacked:YES]; +} + +- (void)testInsertSubnodeBelowWithLayer +{ + [self checkInsertSubnodeBelowWithView:YES layerBacked:YES]; +} + + +- (void)checkInsertSubnodeBelowWithView:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *v in @[parent, a, b, c]) { + v.layerBacked = isLayerBacked; + } + + [parent addSubnode:b]; + + if (loaded) { + [parent layer]; + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // (b) => (a, b) + [parent insertSubnode:a belowSubnode:b]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, b); + XCTAssertNodesHaveParent(nilParent, c); + + // (a,b) => (c,a,b) + [parent insertSubnode:c belowSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,a,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, b, c); + + // Check insertSubnode with no below + ASDisplayNode *nilNode = nil; + XCTAssertThrows([parent insertSubnode:b belowSubnode:nilNode], @"Can't insert below a nil"); + // Check nothing was inserted + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,a,b", @"Incorrect insertion below"); + + + XCTAssertThrows([parent insertSubnode:nilNode belowSubnode:nilNode], @"Can't insert a nil subnode"); + XCTAssertThrows([parent insertSubnode:nilNode belowSubnode:a], @"Can't insert a nil subnode"); + + // Check inserting below when you're already in the array + // (c,a,b) => (a,c,b) + [parent insertSubnode:c belowSubnode:b]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, c, b); + + // Check what happens when you try to insert a node below itself (should do nothing) + // (a,c,b) => (a,c,b) + [parent insertSubnode:c belowSubnode:c]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, c, b); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here +} + +- (void)testInsertSubnodeAboveWithView +{ + [self checkInsertSubnodeAboveLoaded:YES layerBacked:NO]; +} + +- (void)testInsertSubnodeAboveWithNoView +{ + [self checkInsertSubnodeAboveLoaded:NO layerBacked:NO]; +} + +- (void)testInsertSubnodeAboveWithLayer +{ + [self checkInsertSubnodeAboveLoaded:YES layerBacked:YES]; +} + +- (void)testInsertSubnodeAboveWithNoLayer +{ + [self checkInsertSubnodeAboveLoaded:NO layerBacked:YES]; +} + + +- (void)checkInsertSubnodeAboveLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *n in @[parent, a, b, c]) { + n.layerBacked = isLayerBacked; + } + + [parent addSubnode:a]; + + if (loaded) { + [parent layer]; + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // (a) => (a,b) + [parent insertSubnode:b aboveSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b", @"Insert subnode above"); + XCTAssertNodesHaveParent(parent, a,b); + XCTAssertNodesHaveParent(nilParent, c); + + // (a,b) => (a,c,b) + [parent insertSubnode:c aboveSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"After insert c above a"); + + // Check insertSubnode with invalid parameters throws and doesn't change anything + // (a,c,b) => (a,c,b) + ASDisplayNode *nilNode = nil; + XCTAssertThrows([parent insertSubnode:b aboveSubnode:nilNode], @"Can't insert below a nil"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Check no monkey business"); + + XCTAssertThrows([parent insertSubnode:nilNode aboveSubnode:nilNode], @"Can't insert a nil subnode"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Check no monkey business"); + + XCTAssertThrows([parent insertSubnode:nilNode aboveSubnode:a], @"Can't insert a nil subnode"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Check no monkey business"); + + // Check inserting above when you're already in the array + // (a,c,b) => (c,b,a) + [parent insertSubnode:a aboveSubnode:b]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,b,a", @"Check inserting above when you're already in the array"); + XCTAssertNodesHaveParent(parent, a, c, b); + + // Check what happens when you try to insert a node above itself (should do nothing) + // (c,b,a) => (c,b,a) + [parent insertSubnode:a aboveSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,b,a", @"Insert above self should not change anything"); + XCTAssertNodesHaveParent(parent, a, c, b); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here +} + +- (void)testRemoveFromViewBackedLoadedSupernode +{ + DeclareNodeNamed(a); + DeclareNodeNamed(b); + [b addSubnode:a]; + [a view]; + [b view]; + XCTAssertNodesLoaded(a, b); + XCTAssertEqual(a.supernode, b); + XCTAssertEqual(a.view.superview, b.view); + + [a removeFromSupernode]; + XCTAssertNil(a.supernode); + XCTAssertNil(a.view.superview); +} + +- (void)testRemoveFromLayerBackedLoadedSupernode +{ + DeclareNodeNamed(a); + a.layerBacked = YES; + DeclareNodeNamed(b); + b.layerBacked = YES; + [b addSubnode:a]; + [a layer]; + [b layer]; + XCTAssertNodesLoaded(a, b); + XCTAssertEqual(a.supernode, b); + XCTAssertEqual(a.layer.superlayer, b.layer); + + [a removeFromSupernode]; + XCTAssertNil(a.supernode); + XCTAssertNil(a.layer.superlayer); +} + +- (void)testRemoveLayerBackedFromViewBackedLoadedSupernode +{ + DeclareNodeNamed(a); + a.layerBacked = YES; + DeclareNodeNamed(b); + [b addSubnode:a]; + [a layer]; + [b view]; + XCTAssertNodesLoaded(a, b); + XCTAssertEqual(a.supernode, b); + XCTAssertEqual(a.layer.superlayer, b.layer); + + [a removeFromSupernode]; + XCTAssertNil(a.supernode); + XCTAssertNil(a.layer.superlayer); +} + +- (void)testSubnodeAddedBeforeLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + + __block ASDisplayNode *parent = nil; + __block ASDisplayNode *child = nil; + [self executeOffThread:^{ + parent = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + child = [[ASDisplayNode alloc] init]; + [parent addSubnode:child]; + }]; + + XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode"); + XCTAssertEqualObjects(parent, child.supernode, @"Child has the wrong parent"); + XCTAssertEqual(0, view.subviews.count, @"View shouldn't have any subviews"); + + [parent view]; + + XCTAssertEqual(1, view.subviews.count, @"View should have 1 subview"); +} + +- (void)testSubnodeAddedAfterLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + ASDisplayNode *parent = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + + [parent view]; + + ASDisplayNode *child = [[ASDisplayNode alloc] init]; + [parent addSubnode:child]; + + XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode"); + XCTAssertEqualObjects(parent, child.supernode, @"Child has the wrong parent"); + XCTAssertEqual(1, view.subviews.count, @"View should have 1 subview"); +} + +- (void)checkBackgroundColorOpaqueRelationshipWithViewLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.layerBacked = isLayerBacked; + + if (loaded) { + // Force load + [node layer]; + } + + XCTAssertTrue(node.opaque, @"Node should start opaque"); + XCTAssertTrue(node.layer.opaque, @"Node should start opaque"); + + node.backgroundColor = [UIColor clearColor]; + + // This could be debated, but at the moment we differ from UIView's behavior to change the other property in response + XCTAssertTrue(node.opaque, @"Set background color should not have made this not opaque"); + XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this not opaque"); + + [node layer]; + + XCTAssertTrue(node.opaque, @"Set background color should not have made this not opaque"); + XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this not opaque"); +} + +- (void)testBackgroundColorOpaqueRelationshipView +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:YES layerBacked:NO]; +} + +- (void)testBackgroundColorOpaqueRelationshipLayer +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:YES layerBacked:YES]; +} + +- (void)testBackgroundColorOpaqueRelationshipNoView +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:NO layerBacked:NO]; +} + +- (void)testBackgroundColorOpaqueRelationshipNoLayer +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:NO layerBacked:YES]; +} + +// Check that nodes who have no cell node (no range controller) +// do get their `preload` called, and they do report +// the preload interface state. +- (void)testInterfaceStateForNonCellNode +{ + ASTestWindow *window = [ASTestWindow new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + XCTAssert(node.interfaceState == ASInterfaceStateNone); + XCTAssert(!node.hasPreloaded); + + [window addSubview:node.view]; + XCTAssert(node.hasPreloaded); + XCTAssert(node.interfaceState == ASInterfaceStateInHierarchy); + + [node.view removeFromSuperview]; + // We don't want to call -didExitPreloadState on nodes that aren't being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + // Still, the interfaceState should be None to reflect the current state of the node. + // We just don't proactively clear contents or fetched data for this state transition. + XCTAssert(node.hasPreloaded); + XCTAssert(node.interfaceState == ASInterfaceStateNone); +} + +// Check that nodes who have no cell node (no range controller) +// do get their `preload` called, and they do report +// the preload interface state. +- (void)testInterfaceStateForCellNode +{ + ASCellNode *cellNode = [ASCellNode new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + XCTAssert(node.interfaceState == ASInterfaceStateNone); + XCTAssert(!node.hasPreloaded); + + // Simulate range handler updating cell node. + [cellNode addSubnode:node]; + [cellNode enterInterfaceState:ASInterfaceStatePreload]; + XCTAssert(node.hasPreloaded); + XCTAssert(node.interfaceState == ASInterfaceStatePreload); + + // If the node goes into a view it should not adopt the `InHierarchy` state. + ASTestWindow *window = [ASTestWindow new]; + [window addSubview:cellNode.view]; + XCTAssert(node.hasPreloaded); + XCTAssert(node.interfaceState == ASInterfaceStateInHierarchy); +} + +- (void)testSetNeedsPreloadImmediateState +{ + ASCellNode *cellNode = [ASCellNode new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + [cellNode addSubnode:node]; + [cellNode enterInterfaceState:ASInterfaceStatePreload]; + node.hasPreloaded = NO; + [cellNode setNeedsPreload]; + XCTAssert(node.hasPreloaded); +} + +- (void)testPreloadExitingAndEnteringRange +{ + ASCellNode *cellNode = [ASCellNode new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + [cellNode addSubnode:node]; + [cellNode setHierarchyState:ASHierarchyStateRangeManaged]; + + // Simulate enter range, preload, exit range + [cellNode enterInterfaceState:ASInterfaceStatePreload]; + [cellNode exitInterfaceState:ASInterfaceStatePreload]; + node.hasPreloaded = NO; + [cellNode enterInterfaceState:ASInterfaceStatePreload]; + + XCTAssert(node.hasPreloaded); +} + +- (void)testInitWithViewClass +{ + ASDisplayNode *scrollNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]]; + + XCTAssertFalse(scrollNode.isLayerBacked, @"Can't be layer backed"); + XCTAssertFalse(scrollNode.nodeLoaded, @"Shouldn't have a view yet"); + + scrollNode.frame = CGRectMake(12, 52, 100, 53); + scrollNode.alpha = 0.5; + + XCTAssertTrue([scrollNode.view isKindOfClass:[UIScrollView class]], @"scrollview should load as expected"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(12, 52, 100, 53), scrollNode.frame), @"Should have set the frame on the scroll node"); + XCTAssertEqual(0.5f, scrollNode.alpha, @"Alpha not working"); +} + +- (void)testInitWithLayerClass +{ + ASDisplayNode *transformNode = [[ASDisplayNode alloc] initWithLayerClass:[CATransformLayer class]]; + + XCTAssertTrue(transformNode.isLayerBacked, @"Created with layer class => should be layer-backed by default"); + XCTAssertFalse(transformNode.nodeLoaded, @"Shouldn't have a view yet"); + + transformNode.frame = CGRectMake(12, 52, 100, 53); + transformNode.alpha = 0.5; + + XCTAssertTrue([transformNode.layer isKindOfClass:[CATransformLayer class]], @"scrollview should load as expected"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(12, 52, 100, 53), transformNode.frame), @"Should have set the frame on the scroll node"); + XCTAssertEqual(0.5f, transformNode.alpha, @"Alpha not working"); +} + +static bool stringContainsPointer(NSString *description, id p) { + return [description rangeOfString:[NSString stringWithFormat:@"%p", p]].location != NSNotFound; +} + +- (void)testDebugDescription +{ + // View node has subnodes. Make sure all of the nodes are included in the description + ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + + ASDisplayNode *a = [[ASDisplayNode alloc] init]; + a.layerBacked = YES; + ASDisplayNode *b = [[ASDisplayNode alloc] init]; + b.layerBacked = YES; + b.frame = CGRectMake(0, 0, 100, 123); + ASDisplayNode *c = [[ASDisplayNode alloc] init]; + + for (ASDisplayNode *child in @[a, b, c]) { + [parent addSubnode:child]; + } + + NSString *nodeDescription = [parent displayNodeRecursiveDescription]; + + // Make sure [parent recursiveDescription] contains a, b, and c's pointer string + XCTAssertTrue(stringContainsPointer(nodeDescription, a), @"Layer backed node not present in [parent displayNodeRecursiveDescription]"); + XCTAssertTrue(stringContainsPointer(nodeDescription, b), @"Layer-backed node not present in [parent displayNodeRecursiveDescription]"); + XCTAssertTrue(stringContainsPointer(nodeDescription, c), @"View-backed node not present in [parent displayNodeRecursiveDescription]"); + + NSString *viewDescription = [parent.view valueForKey:@"recursiveDescription"]; + + // Make sure string contains a, b, and c's pointer string + XCTAssertTrue(stringContainsPointer(viewDescription, a), @"Layer backed node not present"); + XCTAssertTrue(stringContainsPointer(viewDescription, b), @"Layer-backed node not present"); + XCTAssertTrue(stringContainsPointer(viewDescription, c), @"View-backed node not present"); + + // Make sure layer names have display node in description + XCTAssertTrue(stringContainsPointer([a.layer debugDescription], a), @"Layer backed node not present"); + XCTAssertTrue(stringContainsPointer([b.layer debugDescription], b), @"Layer-backed node not present"); +} + +- (void)checkNameInDescriptionIsLayerBacked:(BOOL)isLayerBacked +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.layerBacked = isLayerBacked; + + XCTAssertFalse([node.description containsString:@"debugName"], @"Shouldn't reference 'debugName' in description"); + node.debugName = @"big troll eater name"; + + XCTAssertTrue([node.description containsString:node.debugName], @"debugName didn't end up in description"); + [node layer]; + XCTAssertTrue([node.description containsString:node.debugName], @"debugName didn't end up in description"); +} + +- (void)testNameInDescriptionLayer +{ + [self checkNameInDescriptionIsLayerBacked:YES]; +} + +- (void)testNameInDescriptionView +{ + [self checkNameInDescriptionIsLayerBacked:NO]; +} + +- (void)testBounds +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.bounds = CGRectMake(1, 2, 3, 4); + node.frame = CGRectMake(5, 6, 7, 8); + + XCTAssert(node.bounds.origin.x == 1, @"Wrong ASDisplayNode.bounds.origin.x"); + XCTAssert(node.bounds.origin.y == 2, @"Wrong ASDisplayNode.bounds.origin.y"); + XCTAssert(node.bounds.size.width == 7, @"Wrong ASDisplayNode.bounds.size.width"); + XCTAssert(node.bounds.size.height == 8, @"Wrong ASDisplayNode.bounds.size.height"); +} + +- (void)testDidEnterDisplayIsCalledWhenNodesEnterDisplayRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; + + XCTAssert([node displayRangeStateChangedToYES]); +} + +- (void)testDidExitDisplayIsCalledWhenNodesExitDisplayRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; + [node recursivelySetInterfaceState:ASInterfaceStatePreload]; + + XCTAssert([node displayRangeStateChangedToNO]); +} + +- (void)testDidEnterPreloadIsCalledWhenNodesEnterPreloadRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStatePreload]; + + XCTAssert([node preloadStateChangedToYES]); +} + +- (void)testDidExitPreloadIsCalledWhenNodesExitPreloadRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + [node setHierarchyState:ASHierarchyStateRangeManaged]; + + [node recursivelySetInterfaceState:ASInterfaceStatePreload]; + [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; + + XCTAssert([node preloadStateChangedToNO]); +} + + +- (void)testThatNodeGetsRenderedIfItGoesFromZeroSizeToRealSizeButOnlyOnce +{ + NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"logo-square" + ofType:@"png" inDirectory:@"TestResources"]; + UIImage *image = [UIImage imageWithContentsOfFile:path]; + ASImageNode *node = [[ASImageNode alloc] init]; + node.image = image; + + // When rendered at zero-size, we get no contents + XCTAssert(CGSizeEqualToSize(node.bounds.size, CGSizeZero)); + [node recursivelyEnsureDisplaySynchronously:YES]; + XCTAssertNil(node.contents); + + // When size becomes positive, we got some new contents + node.bounds = CGRectMake(0, 0, 100, 100); + [node recursivelyEnsureDisplaySynchronously:YES]; + id contentsAfterRedisplay = node.contents; + XCTAssertNotNil(contentsAfterRedisplay); + + // When size changes again, we do not get new contents + node.bounds = CGRectMake(0, 0, 1000, 1000); + [node recursivelyEnsureDisplaySynchronously:YES]; + XCTAssertEqual(contentsAfterRedisplay, node.contents); +} + +// Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2205 +- (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenContainerEntersHierarchy +{ + ASDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; + [supernode enableSubtreeRasterization]; + ASDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + ASSetDebugNames(supernode, subnode); + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + [supernode addSubnode:subnode]; + [window addSubnode:supernode]; + [window makeKeyAndVisible]; + XCTAssertTrue(ASHierarchyStateIncludesRasterized(subnode.hierarchyState)); + XCTAssertTrue(subnode.isVisible); + [supernode.view removeFromSuperview]; + XCTAssertTrue(ASHierarchyStateIncludesRasterized(subnode.hierarchyState)); + XCTAssertFalse(subnode.isVisible); +} + +// Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2205 +- (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenAddedToContainerThatIsInHierarchy +{ + ASDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; + [supernode enableSubtreeRasterization]; + ASDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + ASSetDebugNames(supernode, subnode); + + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + [window addSubnode:supernode]; + [window makeKeyAndVisible]; + [supernode addSubnode:subnode]; + XCTAssertTrue(ASHierarchyStateIncludesRasterized(subnode.hierarchyState)); + XCTAssertTrue(subnode.isVisible); + [subnode removeFromSupernode]; + XCTAssertFalse(ASHierarchyStateIncludesRasterized(subnode.hierarchyState)); + XCTAssertFalse(subnode.isVisible); +} + +- (void)testThatRasterizingWrapperNodesIsNotAllowed +{ + ASDisplayNode *rasterizedSupernode = [[ASDisplayNode alloc] init]; + [rasterizedSupernode enableSubtreeRasterization]; + ASDisplayNode *subnode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{ + return [[UIView alloc] init]; + }]; + ASSetDebugNames(rasterizedSupernode, subnode); + XCTAssertThrows([rasterizedSupernode addSubnode:subnode]); +} + +- (void)testThatSubnodesGetDisplayUpdatesIfRasterized +{ + ASTestDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; + supernode.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [supernode enableSubtreeRasterization]; + + ASTestDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *subSubnode = [[ASTestDisplayNode alloc] init]; + + ASSetDebugNames(supernode, subnode); + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + [subnode addSubnode:subSubnode]; + [supernode addSubnode:subnode]; + [window addSubnode:supernode]; + [window makeKeyAndVisible]; + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subnode.didDisplayCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subSubnode.didDisplayCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subnode.displayWillStartCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subSubnode.displayWillStartCount == 1); + })); +} + +// Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2011 +- (void)testThatLayerBackedSubnodesAreMarkedInvisibleBeforeDeallocWhenSupernodesViewIsRemovedFromHierarchyWhileBeingRetained +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + + NS_VALID_UNTIL_END_OF_SCOPE UIView *nodeView = nil; + { + NS_VALID_UNTIL_END_OF_SCOPE ASDisplayNode *node = [[ASDisplayNode alloc] init]; + nodeView = node.view; + node.debugName = @"Node"; + + NS_VALID_UNTIL_END_OF_SCOPE ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + subnode.layerBacked = YES; + [node addSubnode:subnode]; + subnode.debugName = @"Subnode"; + + [window addSubview:nodeView]; + } + + // nodeView must continue to be retained across this call, but the nodes must not. + XCTAssertNoThrow([nodeView removeFromSuperview]); +} + +// Running on main thread +// Cause retain count of node to fall to zero synchronously on a background thread (pausing main thread) +// ASDealloc2MainObject queues actual call to -dealloc to occur on the main thread +// Continue execution on main, before the dealloc can run, to dealloc the host view +// Node is in an invalid state (about to dealloc, not valid to retain) but accesses to sublayer delegates +// causes attempted retain — unless weak variable works correctly +- (void)testThatLayerDelegateDoesntDangleAndCauseCrash +{ + NS_VALID_UNTIL_END_OF_SCOPE UIView *host = [[UIView alloc] init]; + + __block NS_VALID_UNTIL_END_OF_SCOPE ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.layerBacked = YES; + + [host addSubnode:node]; + [self executeOffThread:^{ + node = nil; + }]; + host = nil; // <- Would crash here, when UIView accesses its sublayers' delegates in -dealloc. +} + +- (void)testThatSubnodeGetsInterfaceStateSetIfRasterized +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + node.debugName = @"Node"; + [node enableSubtreeRasterization]; + + ASTestDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + subnode.debugName = @"Subnode"; + [node addSubnode:subnode]; + + [node view]; // Node needs to be loaded + + [node enterInterfaceState:ASInterfaceStatePreload]; + + XCTAssertTrue((node.interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); + XCTAssertTrue((subnode.interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); + XCTAssertTrue(node.hasPreloaded); + XCTAssertTrue(subnode.hasPreloaded); +} + +// FIXME +// Supernode is measured, subnode isnt, transition starts, UIKit does a layout pass before measurement finishes +- (void)testThatItsSafeToAutomeasureANodeMidTransition +{ + ASDisplayNode *supernode = [[ASDisplayNode alloc] init]; + [supernode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.bounds = CGRectMake(0, 0, 50, 50); + [supernode addSubnode:node]; + + XCTAssertNil(node.calculatedLayout); + XCTAssertTrue(node.layer.needsLayout); + + [supernode transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:nil]; + + XCTAssertNoThrow([node.view layoutIfNeeded]); +} + +- (void)testThatOnDidLoadThrowsIfCalledOnLoadedOffMain +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + [node view]; + [self executeOffThread:^{ + XCTAssertThrows([node onDidLoad:^(ASDisplayNode * _Nonnull node) { }]); + }]; +} + +- (void)testThatOnDidLoadWorks +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + NSMutableArray *calls = [NSMutableArray array]; + [node onDidLoad:^(ASTestDisplayNode * _Nonnull node) { + [calls addObject:@0]; + }]; + [node onDidLoad:^(ASTestDisplayNode * _Nonnull node) { + [calls addObject:@1]; + }]; + [node onDidLoad:^(ASTestDisplayNode * _Nonnull node) { + [calls addObject:@2]; + }]; + [node view]; + NSArray *expected = @[ @0, @1, @2 ]; + XCTAssertEqualObjects(calls, expected); +} + +- (void)testSettingPropertiesViaStyllableProtocol +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + id returnedNode = + [node styledWithBlock:^(ASLayoutElementStyle * _Nonnull style) { + style.width = ASDimensionMake(100); + style.flexGrow = 1.0; + style.flexShrink = 1.0; + }]; + + XCTAssertEqualObjects(node, returnedNode); + ASXCTAssertEqualDimensions(node.style.width, ASDimensionMake(100)); + XCTAssertEqual(node.style.flexGrow, 1.0, @"flexGrow should have have the value 1.0"); + XCTAssertEqual(node.style.flexShrink, 1.0, @"flexShrink should have have the value 1.0"); +} + +- (void)testSubnodesFastEnumeration +{ + DeclareNodeNamed(parentNode); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareViewNamed(d); + + NSArray *subnodes = @[a, b, c, d]; + for (ASDisplayNode *node in subnodes) { + [parentNode addSubnode:node]; + } + + NSInteger i = 0; + for (ASDisplayNode *subnode in parentNode.subnodes) { + XCTAssertEqualObjects(subnode, subnodes[i]); + i++; + } +} + +- (void)testThatHavingTheSameNodeTwiceInALayoutSpecCausesExceptionOnLayoutCalculation +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + node.layoutSpecBlock = ^ASLayoutSpec *(ASDisplayNode *node, ASSizeRange constrainedSize) { + return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:subnode] overlay:subnode]; + }; + XCTAssertThrowsSpecificNamed([node calculateLayoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))], NSException, NSInternalInconsistencyException); +} + +- (void)testThatStackSpecOrdersSubnodesCorrectlyRandomness +{ + // This test ensures that the z-order of nodes matches the stack spec, including after several random relayouts / transitions. + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareNodeNamed(d); + DeclareNodeNamed(e); + DeclareNodeNamed(f); + DeclareNodeNamed(g); + DeclareNodeNamed(h); + DeclareNodeNamed(i); + DeclareNodeNamed(j); + + NSMutableArray *allNodes = [@[a, b, c, d, e, f, g, h, i, j] mutableCopy]; + NSArray *testPrevious = @[]; + NSArray __block *testPending = @[]; + + int len1 = 1 + arc4random_uniform(9); + for (NSUInteger n = 0; n < len1; n++) { // shuffle and add + [allNodes exchangeObjectAtIndex:n withObjectAtIndex:n + arc4random_uniform(10 - (uint32_t) n)]; + testPrevious = [testPrevious arrayByAddingObject:allNodes[n]]; + } + + __block NSUInteger testCount = 0; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { + ASStackLayoutSpec *stack = [ASStackLayoutSpec verticalStackLayoutSpec]; + + if (testCount++ == 0) { + stack.children = testPrevious; + } + else { + testPending = @[]; + int len2 = 1 + arc4random_uniform(9); + for (NSUInteger n = 0; n < len2; n++) { // shuffle and add + [allNodes exchangeObjectAtIndex:n withObjectAtIndex:n + arc4random_uniform(10 - (uint32_t) n)]; + testPending = [testPending arrayByAddingObject:allNodes[n]]; + } + stack.children = testPending; + } + + return stack; + }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + + // Because automaticallyManagesSubnodes is used, the subnodes array is constructed from the layout spec's children. + NSString *expected = [[testPrevious valueForKey:@"debugName"] componentsJoinedByString:@","]; + XCTAssert([node.subnodes isEqualToArray:testPrevious], @"subnodes: %@, array: %@", node.subnodes, testPrevious); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + expected, @"Initial order"); + + for (NSUInteger n = 0; n < 25; n++) { + [node invalidateCalculatedLayout]; + [node.view setNeedsLayout]; + [node.view layoutIfNeeded]; + + + XCTAssert([node.subnodes isEqualToArray:testPending], @"subnodes: %@, array: %@", node.subnodes, testPending); + expected = [[testPending valueForKey:@"debugName"] componentsJoinedByString:@","]; + + XCTAssertEqualObjects(orderStringFromSubnodes(node), expected, @"Incorrect node order for Random order #%ld", (unsigned long) n); + XCTAssertEqualObjects(orderStringFromSubviews(node.view), expected, @"Incorrect subviews for Random order #%ld", (unsigned long) n); + XCTAssertEqualObjects(orderStringFromSublayers(node.layer), expected, @"Incorrect sublayers for Random order #%ld", (unsigned long) n); + } +} + +- (void)testThatStackSpecOrdersSubnodesCorrectly +{ + // This test ensures that the z-order of nodes matches the stack spec, including after relayout / transition. + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareNodeNamed(d); + DeclareNodeNamed(e); + + NSArray *nodesForwardOrder = @[a, b, c, d]; + NSArray *nodesReverseOrder = @[d, c, b, a]; + NSArray *addAndMoveOrder = @[a, b, e, d, c]; + __block BOOL flipItemOrder = NO; + + __block NSUInteger testCount = 0; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { + ASStackLayoutSpec *stack = [ASStackLayoutSpec verticalStackLayoutSpec]; + switch(testCount) { + case 0: + stack.children = nodesForwardOrder; break; + case 1: + stack.children = nodesReverseOrder; break; + case 2: + default: + stack.children = addAndMoveOrder; break; + } + testCount++; + return stack; + }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + + // Because automaticallyManagesSubnodes is used, the subnodes array is constructed from the layout spec's children. + XCTAssert([node.subnodes isEqualToArray:nodesForwardOrder], @"subnodes: %@, array: %@", node.subnodes, nodesForwardOrder); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + @"a,b,c,d", @"Forward order"); + + flipItemOrder = YES; + [node invalidateCalculatedLayout]; + [node.view setNeedsLayout]; + [node.view layoutIfNeeded]; + + // In this case, it's critical that the items are in the new order so that event handling and apparent z-position are correct. + // FIXME: The reversal case is not currently passing. + XCTAssert([node.subnodes isEqualToArray:nodesReverseOrder], @"subnodes: %@, array: %@", node.subnodes, nodesReverseOrder); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + @"d,c,b,a", @"Reverse order"); + + [node invalidateCalculatedLayout]; + [node.view setNeedsLayout]; + [node.view layoutIfNeeded]; + XCTAssert([node.subnodes isEqualToArray:addAndMoveOrder], @"subnodes: %@, array: %@", node.subnodes, addAndMoveOrder); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + @"a,b,e,d,c", @"AddAndMove order"); + +} + +- (void)testThatOverlaySpecOrdersSubnodesCorrectly +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + ASDisplayNode *underlay = [[ASDisplayNode alloc] init]; + underlay.debugName = @"underlay"; + ASDisplayNode *overlay = [[ASDisplayNode alloc] init]; + overlay.debugName = @"overlay"; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { + // The inset spec here is crucial. If the nodes themselves are children, it passed before the fix. + return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:underlay] overlay:overlay]; + }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + + NSInteger underlayIndex = [node.subnodes indexOfObjectIdenticalTo:underlay]; + NSInteger overlayIndex = [node.subnodes indexOfObjectIdenticalTo:overlay]; + XCTAssertLessThan(underlayIndex, overlayIndex); +} + +- (void)testThatBackgroundLayoutSpecOrdersSubnodesCorrectly +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + ASDisplayNode *underlay = [[ASDisplayNode alloc] init]; + underlay.debugName = @"underlay"; + ASDisplayNode *overlay = [[ASDisplayNode alloc] init]; + overlay.debugName = @"overlay"; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { + // The inset spec here is crucial. If the nodes themselves are children, it passed before the fix. + return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:overlay background:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:underlay]]; + }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + + NSInteger underlayIndex = [node.subnodes indexOfObjectIdenticalTo:underlay]; + NSInteger overlayIndex = [node.subnodes indexOfObjectIdenticalTo:overlay]; + XCTAssertLessThan(underlayIndex, overlayIndex); +} + +- (void)testThatConvertPointGoesToWindowWhenPassedNil +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.frame = CGRectMake(10, 10, 10, 10); + [window addSubnode:node]; + CGPoint expectedOrigin = CGPointMake(10, 10); + ASXCTAssertEqualPoints([node convertPoint:node.bounds.origin toNode:nil], expectedOrigin); +} + +- (void)testThatConvertPointGoesToWindowWhenPassedNil_layerBacked +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.layerBacked = YES; + node.frame = CGRectMake(10, 10, 10, 10); + [window addSubnode:node]; + CGPoint expectedOrigin = CGPointMake(10, 10); + ASXCTAssertEqualPoints([node convertPoint:node.bounds.origin toNode:nil], expectedOrigin); +} + +- (void)testThatItIsAllowedToRetrieveDebugDescriptionIncludingVCOffMainThread +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + UIViewController *vc = [[UIViewController alloc] init]; + [vc.view addSubnode:node]; + dispatch_group_t g = dispatch_group_create(); + __block NSString *debugDescription; + dispatch_group_async(g, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + debugDescription = [node debugDescription]; + }); + dispatch_group_wait(g, DISPATCH_TIME_FOREVER); + // Ensure the debug description contains the VC string. + // Have to split into two lines because XCTAssert macro can't handle the stringWithFormat:. + BOOL hasVC = [debugDescription containsString:[NSString stringWithFormat:@"%p", vc]]; + XCTAssert(hasVC); +} + +- (void)testThatSubnodeSafeAreaInsetsAreCalculatedCorrectly +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + + rootNode.automaticallyManagesSubnodes = YES; + rootNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(1, 2, 3, 4) child:subnode]; + }; + + ASTestViewController *viewController = [[ASTestViewController alloc] initWithNode:rootNode]; + viewController.additionalSafeAreaInsets = UIEdgeInsetsMake(10, 10, 10, 10); + + // It looks like iOS 11 suppresses safeAreaInsets calculation for the views that are not on screen. + UIWindow *window = [[UIWindow alloc] init]; + window.rootViewController = viewController; + [window setHidden:NO]; + [window layoutIfNeeded]; + + UIEdgeInsets expectedRootNodeSafeArea = UIEdgeInsetsMake(10, 10, 10, 10); + UIEdgeInsets expectedSubnodeSafeArea = UIEdgeInsetsMake(9, 8, 7, 6); + + UIEdgeInsets windowSafeArea = UIEdgeInsetsZero; + if (AS_AVAILABLE_IOS(11.0)) { + windowSafeArea = window.safeAreaInsets; + } + + expectedRootNodeSafeArea = ASConcatInsets(expectedRootNodeSafeArea, windowSafeArea); + expectedSubnodeSafeArea = ASConcatInsets(expectedSubnodeSafeArea, windowSafeArea); + + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(expectedRootNodeSafeArea, rootNode.safeAreaInsets), + @"expected rootNode.safeAreaInsets to be %@ but got %@ (window.safeAreaInsets %@)", + NSStringFromUIEdgeInsets(expectedRootNodeSafeArea), + NSStringFromUIEdgeInsets(rootNode.safeAreaInsets), + NSStringFromUIEdgeInsets(windowSafeArea)); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(expectedSubnodeSafeArea, subnode.safeAreaInsets), + @"expected subnode.safeAreaInsets to be %@ but got %@ (window.safeAreaInsets %@)", + NSStringFromUIEdgeInsets(expectedSubnodeSafeArea), + NSStringFromUIEdgeInsets(subnode.safeAreaInsets), + NSStringFromUIEdgeInsets(windowSafeArea)); + + [window setHidden:YES]; +} + +- (void)testScreenScale +{ + XCTAssertEqual(ASScreenScale(), UIScreen.mainScreen.scale); +} + +- (void)testThatIfViewClassIsOverwrittenItsSynchronous +{ + ASSynchronousTestDisplayNodeViaViewClass *node = [[ASSynchronousTestDisplayNodeViaViewClass alloc] init]; + XCTAssertTrue([node isSynchronous], @"Node should be synchronous if viewClass is ovewritten and not a subclass of _ASDisplayView"); +} + +- (void)testThatIfLayerClassIsOverwrittenItsSynchronous +{ + ASSynchronousTestDisplayNodeViaLayerClass *node = [[ASSynchronousTestDisplayNodeViaLayerClass alloc] init]; + XCTAssertTrue([node isSynchronous], @"Node should be synchronous if viewClass is ovewritten and not a subclass of _ASDisplayView"); +} + +- (void)testCornerRoundingTypeClippingRoundedCornersIsUsingASDisplayNodeCornerLayerDelegate +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + node.cornerRoundingType = ASCornerRoundingTypeClipping; + node.cornerRadius = 10.0; + auto l = node.clipCornerLayers; + for (int i = 0; i < NUM_CLIP_CORNER_LAYERS; i++) { + CALayer *cornerLayer = (*l)[i]; + XCTAssertTrue([cornerLayer.delegate isKindOfClass:[ASDisplayNodeCornerLayerDelegate class]], @""); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTestsHelper.h b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTestsHelper.h new file mode 100644 index 0000000000..4e884f4428 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTestsHelper.h @@ -0,0 +1,21 @@ +// +// ASDisplayNodeTestsHelper.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@class ASCATransactionQueue, ASDisplayNode; + +typedef BOOL (^as_condition_block_t)(void); + +AS_EXTERN BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block); + +AS_EXTERN void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size); +AS_EXTERN void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange); +AS_EXTERN void ASCATransactionQueueWait(ASCATransactionQueue *q); // nil means shared queue diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTestsHelper.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTestsHelper.mm new file mode 100644 index 0000000000..ae20549117 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayNodeTestsHelper.mm @@ -0,0 +1,69 @@ +// +// ASDisplayNodeTestsHelper.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASDisplayNodeTestsHelper.h" +#import +#import +#import + +#import + +#import + +// Poll the condition 1000 times a second. +static CFTimeInterval kSingleRunLoopTimeout = 0.001; + +// Time out after 30 seconds. +static CFTimeInterval kTimeoutInterval = 30.0f; + +BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block) +{ + CFTimeInterval timeoutDate = CACurrentMediaTime() + kTimeoutInterval; + BOOL passed = NO; + while (true) { + OSMemoryBarrier(); + passed = block(); + OSMemoryBarrier(); + if (passed) { + break; + } + CFTimeInterval now = CACurrentMediaTime(); + if (now > timeoutDate) { + break; + } + // Run until the poll timeout or until timeoutDate, whichever is first. + CFTimeInterval runLoopTimeout = MIN(kSingleRunLoopTimeout, timeoutDate - now); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, runLoopTimeout, true); + } + return passed; +} + +void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size) +{ + CGSize sizeThatFits = [node layoutThatFits:ASSizeRangeMake(size)].size; + node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits}; +} + +void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange) +{ + CGSize sizeThatFits = [node layoutThatFits:sizeRange].size; + node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits}; +} + +void ASCATransactionQueueWait(ASCATransactionQueue *q) +{ + if (!q) { q = ASCATransactionQueueGet(); } + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:1]; + BOOL whileResult = YES; + while ([date timeIntervalSinceNow] > 0 && + (whileResult = ![q isEmpty])) { + [[NSRunLoop currentRunLoop] runUntilDate: + [NSDate dateWithTimeIntervalSinceNow:0.01]]; + } +} diff --git a/submodules/AsyncDisplayKit/Tests/ASDisplayViewAccessibilityTests.mm b/submodules/AsyncDisplayKit/Tests/ASDisplayViewAccessibilityTests.mm new file mode 100644 index 0000000000..bbc28cf0b3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASDisplayViewAccessibilityTests.mm @@ -0,0 +1,303 @@ +// +// ASDisplayViewAccessibilityTests.mm +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import +#import + +@interface ASDisplayViewAccessibilityTests : XCTestCase +@end + +@implementation ASDisplayViewAccessibilityTests + +- (void)setUp +{ + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalDisableAccessibilityCache; + [ASConfigurationManager test_resetWithConfiguration:config]; +} + +- (void)testAccessibilityElementsAccessors +{ + // Setup nodes with accessibility info + ASDisplayNode *node = nil; + ASDisplayNode *subnode = nil; + node = [[ASDisplayNode alloc] init]; + subnode = [[ASDisplayNode alloc] init]; + NSString *label = @"foo"; + subnode.isAccessibilityElement = YES; + subnode.accessibilityLabel = label; + [node addSubnode:subnode]; + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], label); + // NOTE: The following tests will fail unless accessibility is enabled, e.g. by turning the + // accessibility inspector on. See https://github.com/TextureGroup/Texture/pull/1069 for details. + /*XCTAssertEqualObjects([[node.view accessibilityElementAtIndex:0] accessibilityLabel], label); + XCTAssertEqual(node.view.accessibilityElementCount, 1); + XCTAssertEqual([node.view indexOfAccessibilityElement:node.view.accessibilityElements.firstObject], 0);*/ +} + +- (void)testThatSubnodeAccessibilityLabelAggregationWorks +{ + // Setup nodes + ASDisplayNode *node = nil; + ASDisplayNode *innerNode1 = nil; + ASDisplayNode *innerNode2 = nil; + node = [[ASDisplayNode alloc] init]; + innerNode1 = [[ASDisplayNode alloc] init]; + innerNode2 = [[ASDisplayNode alloc] init]; + + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + innerNode1.accessibilityLabel = @"hello"; + innerNode2.accessibilityLabel = @"world"; + + // Attach the subnodes to the parent node, then ensure their accessibility labels have been' + // aggregated to the parent's accessibility label + [node addSubnode:innerNode1]; + [node addSubnode:innerNode2]; + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], + @"hello, world", @"Subnode accessibility label aggregation broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); +} + +- (void)testThatContainerAccessibilityLabelOverrideStopsAggregation +{ + // Setup nodes + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + node.accessibilityLabel = @"hello"; + innerNode.accessibilityLabel = @"world"; + + // Attach the subnode to the parent node, then ensure the parent's accessibility label does not + // get aggregated with the subnode's label + [node addSubnode:innerNode]; + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], @"hello", + @"Container accessibility label override broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); +} + +- (void)testAccessibilityLayerbackedNodesOperationInContainer { + ASDisplayNode *contianer = [[ASDisplayNode alloc] init]; + contianer.frame = CGRectMake(50, 50, 200, 400); + contianer.backgroundColor = [UIColor grayColor]; + contianer.isAccessibilityContainer = YES; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.layerBacked = YES; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 100, 200, 200); + [contianer addSubnode:text1]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *elements = contianer.view.accessibilityElements; + XCTAssertTrue(elements.count == 1); + XCTAssertTrue([[elements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.layerBacked = YES; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 300, 200, 200); + [contianer addSubnode:text2]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements = contianer.view.accessibilityElements; + XCTAssertTrue(updatedElements.count == 1); + XCTAssertTrue([[updatedElements.firstObject accessibilityLabel] isEqualToString:@"hello, world"]); + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"!!!!"]; + text3.frame = CGRectMake(50, 400, 200, 100); + text3.layerBacked = YES; + [text2 addSubnode:text3]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements2 = contianer.view.accessibilityElements; + XCTAssertTrue([[updatedElements2.firstObject accessibilityLabel] isEqualToString:@"hello, world, !!!!"]); +} + +- (void)testAccessibilityNonLayerbackedNodesOperationInContainer +{ + ASDisplayNode *contianer = [[ASDisplayNode alloc] init]; + contianer.frame = CGRectMake(50, 50, 200, 600); + contianer.backgroundColor = [UIColor grayColor]; + contianer.isAccessibilityContainer = YES; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 100, 200, 200); + [contianer addSubnode:text1]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *elements = contianer.view.accessibilityElements; + XCTAssertTrue(elements.count == 1); + XCTAssertTrue([[elements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 300, 200, 200); + [contianer addSubnode:text2]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements = contianer.view.accessibilityElements; + XCTAssertTrue(updatedElements.count == 1); + XCTAssertTrue([[updatedElements.firstObject accessibilityLabel] isEqualToString:@"hello, world"]); + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"!!!!"]; + text3.frame = CGRectMake(50, 400, 200, 100); + [text2 addSubnode:text3]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements2 = contianer.view.accessibilityElements; + XCTAssertTrue([[updatedElements2.firstObject accessibilityLabel] isEqualToString:@"hello, world, !!!!"]); +} + +- (void)testAccessibilityNonLayerbackedNodesOperationInNonContainer +{ + ASDisplayNode *contianer = [[ASDisplayNode alloc] init]; + contianer.frame = CGRectMake(50, 50, 200, 600); + contianer.backgroundColor = [UIColor grayColor]; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 100, 200, 200); + [contianer addSubnode:text1]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *elements = contianer.view.accessibilityElements; + XCTAssertTrue(elements.count == 1); + XCTAssertTrue([[elements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 300, 200, 200); + [contianer addSubnode:text2]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements = contianer.view.accessibilityElements; + XCTAssertTrue(updatedElements.count == 2); + XCTAssertTrue([[updatedElements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + XCTAssertTrue([[updatedElements.lastObject accessibilityLabel] isEqualToString:@"world"]); + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"!!!!"]; + text3.frame = CGRectMake(50, 400, 200, 100); + [text2 addSubnode:text3]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements2 = contianer.view.accessibilityElements; + //text3 won't be read out cause it's overshadowed by text2 + XCTAssertTrue(updatedElements2.count == 2); + XCTAssertTrue([[updatedElements2.firstObject accessibilityLabel] isEqualToString:@"hello"]); + XCTAssertTrue([[updatedElements2.lastObject accessibilityLabel] isEqualToString:@"world"]); +} +- (void)testAccessibilityLayerbackedNodesOperationInNonContainer +{ + ASDisplayNode *contianer = [[ASDisplayNode alloc] init]; + contianer.frame = CGRectMake(50, 50, 200, 600); + contianer.backgroundColor = [UIColor grayColor]; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.layerBacked = YES; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 0, 100, 100); + [contianer addSubnode:text1]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *elements = contianer.view.accessibilityElements; + XCTAssertTrue(elements.count == 1); + XCTAssertTrue([[elements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.layerBacked = YES; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 100, 100, 100); + [contianer addSubnode:text2]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements = contianer.view.accessibilityElements; + XCTAssertTrue(updatedElements.count == 2); + XCTAssertTrue([[updatedElements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + XCTAssertTrue([[updatedElements.lastObject accessibilityLabel] isEqualToString:@"world"]); + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.layerBacked = YES; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"!!!!"]; + text3.frame = CGRectMake(50, 200, 100, 100); + [text2 addSubnode:text3]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *updatedElements2 = contianer.view.accessibilityElements; + //text3 won't be read out cause it's overshadowed by text2 + XCTAssertTrue(updatedElements2.count == 2); + XCTAssertTrue([[updatedElements2.firstObject accessibilityLabel] isEqualToString:@"hello"]); + XCTAssertTrue([[updatedElements2.lastObject accessibilityLabel] isEqualToString:@"world"]); +} + +- (void)testAccessibilityUpdatesWithElementsChanges +{ + ASDisplayNode *contianer = [[ASDisplayNode alloc] init]; + contianer.frame = CGRectMake(50, 50, 200, 600); + contianer.backgroundColor = [UIColor grayColor]; + contianer.isAccessibilityContainer = YES; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.layerBacked = YES; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 0, 100, 100); + [contianer addSubnode:text1]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *elements = contianer.view.accessibilityElements; + XCTAssertTrue(elements.count == 1); + XCTAssertTrue([[elements.firstObject accessibilityLabel] isEqualToString:@"hello"]); + text1.attributedText = [[NSAttributedString alloc] initWithString:@"greeting"]; + [contianer layoutIfNeeded]; + [contianer.layer displayIfNeeded]; + NSArray *elements2 = contianer.view.accessibilityElements; + XCTAssertTrue(elements2.count == 1); + XCTAssertTrue([[elements2.firstObject accessibilityLabel] isEqualToString:@"greeting"]); +} + +#pragma mark - +#pragma mark UIAccessibilityAction Forwarding + +- (void)testActionForwarding { + ASDisplayNode *node = [ASDisplayNode new]; + UIView *view = node.view; + + id mockNode = OCMPartialMock(node); + + OCMExpect([mockNode accessibilityActivate]); + [view accessibilityActivate]; + + OCMExpect([mockNode accessibilityIncrement]); + [view accessibilityIncrement]; + + OCMExpect([mockNode accessibilityDecrement]); + [view accessibilityDecrement]; + + OCMExpect([mockNode accessibilityScroll:UIAccessibilityScrollDirectionDown]); + [view accessibilityScroll:UIAccessibilityScrollDirectionDown]; + + OCMExpect([mockNode accessibilityPerformEscape]); + [view accessibilityPerformEscape]; + + OCMExpect([mockNode accessibilityPerformMagicTap]); + [view accessibilityPerformMagicTap]; + + OCMVerifyAll(mockNode); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASEditableTextNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASEditableTextNodeTests.mm new file mode 100644 index 0000000000..a359629014 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASEditableTextNodeTests.mm @@ -0,0 +1,171 @@ +// +// ASEditableTextNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta) +{ + return fabs(size1.width - size2.width) < delta && fabs(size1.height - size2.height) < delta; +} + +@interface ASEditableTextNodeTests : XCTestCase +@property (nonatomic) ASEditableTextNode *editableTextNode; +@property (nonatomic, copy) NSAttributedString *attributedText; +@end + +@implementation ASEditableTextNodeTests + +- (void)setUp +{ + [super setUp]; + + _editableTextNode = [[ASEditableTextNode alloc] init]; + + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."]; + NSMutableParagraphStyle *para = [NSMutableParagraphStyle new]; + para.alignment = NSTextAlignmentCenter; + para.lineSpacing = 1.0; + [mas addAttribute:NSParagraphStyleAttributeName value:para + range:NSMakeRange(0, mas.length - 1)]; + + // Vary the linespacing on the last line + NSMutableParagraphStyle *lastLinePara = [NSMutableParagraphStyle new]; + lastLinePara.alignment = para.alignment; + lastLinePara.lineSpacing = 5.0; + [mas addAttribute:NSParagraphStyleAttributeName value:lastLinePara + range:NSMakeRange(mas.length - 1, 1)]; + + _attributedText = mas; + _editableTextNode.attributedText = _attributedText; +} + +#pragma mark - ASEditableTextNode + +- (void)testAllocASEditableTextNode +{ + ASEditableTextNode *node = [[ASEditableTextNode alloc] init]; + XCTAssertTrue([[node class] isSubclassOfClass:[ASEditableTextNode class]], @"ASEditableTextNode alloc should return an instance of ASEditableTextNode, instead returned %@", [node class]); +} + +#pragma mark - ASEditableTextNode Tests + +- (void)testUITextInputTraitDefaults +{ + ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init]; + + XCTAssertTrue(editableTextNode.autocapitalizationType == UITextAutocapitalizationTypeSentences, @"_ASTextInputTraitsPendingState's autocapitalizationType default should be UITextAutocapitalizationTypeSentences."); + XCTAssertTrue(editableTextNode.autocorrectionType == UITextAutocorrectionTypeDefault, @"_ASTextInputTraitsPendingState's autocorrectionType default should be UITextAutocorrectionTypeDefault."); + XCTAssertTrue(editableTextNode.spellCheckingType == UITextSpellCheckingTypeDefault, @"_ASTextInputTraitsPendingState's spellCheckingType default should be UITextSpellCheckingTypeDefault."); + XCTAssertTrue(editableTextNode.keyboardType == UIKeyboardTypeDefault, @"_ASTextInputTraitsPendingState's keyboardType default should be UIKeyboardTypeDefault."); + XCTAssertTrue(editableTextNode.keyboardAppearance == UIKeyboardAppearanceDefault, @"_ASTextInputTraitsPendingState's keyboardAppearance default should be UIKeyboardAppearanceDefault."); + XCTAssertTrue(editableTextNode.returnKeyType == UIReturnKeyDefault, @"_ASTextInputTraitsPendingState's returnKeyType default should be UIReturnKeyDefault."); + XCTAssertTrue(editableTextNode.enablesReturnKeyAutomatically == NO, @"_ASTextInputTraitsPendingState's enablesReturnKeyAutomatically default should be NO."); + XCTAssertTrue(editableTextNode.isSecureTextEntry == NO, @"_ASTextInputTraitsPendingState's isSecureTextEntry default should be NO."); + + XCTAssertTrue(editableTextNode.textView.autocapitalizationType == UITextAutocapitalizationTypeSentences, @"textView's autocapitalizationType default should be UITextAutocapitalizationTypeSentences."); + XCTAssertTrue(editableTextNode.textView.autocorrectionType == UITextAutocorrectionTypeDefault, @"textView's autocorrectionType default should be UITextAutocorrectionTypeDefault."); + XCTAssertTrue(editableTextNode.textView.spellCheckingType == UITextSpellCheckingTypeDefault, @"textView's spellCheckingType default should be UITextSpellCheckingTypeDefault."); + XCTAssertTrue(editableTextNode.textView.keyboardType == UIKeyboardTypeDefault, @"textView's keyboardType default should be UIKeyboardTypeDefault."); + XCTAssertTrue(editableTextNode.textView.keyboardAppearance == UIKeyboardAppearanceDefault, @"textView's keyboardAppearance default should be UIKeyboardAppearanceDefault."); + XCTAssertTrue(editableTextNode.textView.returnKeyType == UIReturnKeyDefault, @"textView's returnKeyType default should be UIReturnKeyDefault."); + XCTAssertTrue(editableTextNode.textView.enablesReturnKeyAutomatically == NO, @"textView's enablesReturnKeyAutomatically default should be NO."); + XCTAssertTrue(editableTextNode.textView.isSecureTextEntry == NO, @"textView's isSecureTextEntry default should be NO."); +} + +- (void)testUITextInputTraitsSetTraitsBeforeViewLoaded +{ + // UITextView ignores any values set on the first 3 properties below if secureTextEntry is enabled. + // Because of this UIKit behavior, we'll test secure entry seperately + ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init]; + + editableTextNode.autocapitalizationType = UITextAutocapitalizationTypeWords; + editableTextNode.autocorrectionType = UITextAutocorrectionTypeYes; + editableTextNode.spellCheckingType = UITextSpellCheckingTypeYes; + editableTextNode.keyboardType = UIKeyboardTypeTwitter; + editableTextNode.keyboardAppearance = UIKeyboardAppearanceDark; + editableTextNode.returnKeyType = UIReturnKeyGo; + editableTextNode.enablesReturnKeyAutomatically = YES; + + XCTAssertTrue(editableTextNode.textView.autocapitalizationType == UITextAutocapitalizationTypeWords, @"textView's autocapitalizationType should be UITextAutocapitalizationTypeAllCharacters."); + XCTAssertTrue(editableTextNode.textView.autocorrectionType == UITextAutocorrectionTypeYes, @"textView's autocorrectionType should be UITextAutocorrectionTypeYes."); + XCTAssertTrue(editableTextNode.textView.spellCheckingType == UITextSpellCheckingTypeYes, @"textView's spellCheckingType should be UITextSpellCheckingTypeYes."); + XCTAssertTrue(editableTextNode.textView.keyboardType == UIKeyboardTypeTwitter, @"textView's keyboardType should be UIKeyboardTypeTwitter."); + XCTAssertTrue(editableTextNode.textView.keyboardAppearance == UIKeyboardAppearanceDark, @"textView's keyboardAppearance should be UIKeyboardAppearanceDark."); + XCTAssertTrue(editableTextNode.textView.returnKeyType == UIReturnKeyGo, @"textView's returnKeyType should be UIReturnKeyGo."); + XCTAssertTrue(editableTextNode.textView.enablesReturnKeyAutomatically == YES, @"textView's enablesReturnKeyAutomatically should be YES."); + + ASEditableTextNode *secureEditableTextNode = [[ASEditableTextNode alloc] init]; + secureEditableTextNode.secureTextEntry = YES; + + XCTAssertTrue(secureEditableTextNode.textView.secureTextEntry == YES, @"textView's isSecureTextEntry should be YES."); +} + +- (void)testUITextInputTraitsChangeTraitAfterViewLoaded +{ + // UITextView ignores any values set on the first 3 properties below if secureTextEntry is enabled. + // Because of this UIKit behavior, we'll test secure entry seperately + ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init]; + + editableTextNode.textView.autocapitalizationType = UITextAutocapitalizationTypeWords; + editableTextNode.textView.autocorrectionType = UITextAutocorrectionTypeYes; + editableTextNode.textView.spellCheckingType = UITextSpellCheckingTypeYes; + editableTextNode.textView.keyboardType = UIKeyboardTypeTwitter; + editableTextNode.textView.keyboardAppearance = UIKeyboardAppearanceDark; + editableTextNode.textView.returnKeyType = UIReturnKeyGo; + editableTextNode.textView.enablesReturnKeyAutomatically = YES; + + XCTAssertTrue(editableTextNode.textView.autocapitalizationType == UITextAutocapitalizationTypeWords, @"textView's autocapitalizationType should be UITextAutocapitalizationTypeAllCharacters."); + XCTAssertTrue(editableTextNode.textView.autocorrectionType == UITextAutocorrectionTypeYes, @"textView's autocorrectionType should be UITextAutocorrectionTypeYes."); + XCTAssertTrue(editableTextNode.textView.spellCheckingType == UITextSpellCheckingTypeYes, @"textView's spellCheckingType should be UITextSpellCheckingTypeYes."); + XCTAssertTrue(editableTextNode.textView.keyboardType == UIKeyboardTypeTwitter, @"textView's keyboardType should be UIKeyboardTypeTwitter."); + XCTAssertTrue(editableTextNode.textView.keyboardAppearance == UIKeyboardAppearanceDark, @"textView's keyboardAppearance should be UIKeyboardAppearanceDark."); + XCTAssertTrue(editableTextNode.textView.returnKeyType == UIReturnKeyGo, @"textView's returnKeyType should be UIReturnKeyGo."); + XCTAssertTrue(editableTextNode.textView.enablesReturnKeyAutomatically == YES, @"textView's enablesReturnKeyAutomatically should be YES."); + + ASEditableTextNode *secureEditableTextNode = [[ASEditableTextNode alloc] init]; + secureEditableTextNode.textView.secureTextEntry = YES; + + XCTAssertTrue(secureEditableTextNode.textView.secureTextEntry == YES, @"textView's isSecureTextEntry should be YES."); +} + +- (void)testCalculatedSizeIsGreaterThanOrEqualToConstrainedSize +{ + for (NSInteger i = 10; i < 500; i += 50) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_editableTextNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + XCTAssertTrue(calculatedSize.width <= constrainedSize.width, @"Calculated width (%f) should be less than or equal to constrained width (%f)", calculatedSize.width, constrainedSize.width); + XCTAssertTrue(calculatedSize.height <= constrainedSize.height, @"Calculated height (%f) should be less than or equal to constrained height (%f)", calculatedSize.height, constrainedSize.height); + } +} + +- (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedSize +{ + for (NSInteger i = 10; i < 500; i += 50) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_editableTextNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + CGSize recalculatedSize = [_editableTextNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 4.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); + } +} + +- (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedFloatingPointSize +{ + for (CGFloat i = 10; i < 500; i *= 1.3) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_editableTextNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + CGSize recalculatedSize = [_editableTextNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 11.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASImageNodeSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASImageNodeSnapshotTests.mm new file mode 100644 index 0000000000..55cb5f866e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASImageNodeSnapshotTests.mm @@ -0,0 +1,94 @@ +// +// ASImageNodeSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASSnapshotTestCase.h" + +#import + +@interface ASImageNodeSnapshotTests : ASSnapshotTestCase +@end + +@implementation ASImageNodeSnapshotTests + +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; +} + +- (UIImage *)testImage +{ + NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"logo-square" + ofType:@"png" + inDirectory:@"TestResources"]; + return [UIImage imageWithContentsOfFile:path]; +} + +- (void)testRenderLogoSquare +{ + // trivial test case to ensure ASSnapshotTestCase works + ASImageNode *imageNode = [[ASImageNode alloc] init]; + imageNode.image = [self testImage]; + ASDisplayNodeSizeToFitSize(imageNode, CGSizeMake(100, 100)); + + ASSnapshotVerifyNode(imageNode, nil); +} + +- (void)testForcedScaling +{ + CGSize forcedImageSize = CGSizeMake(100, 100); + + ASImageNode *imageNode = [[ASImageNode alloc] init]; + imageNode.forcedSize = forcedImageSize; + imageNode.image = [self testImage]; + + // Snapshot testing requires that node is formally laid out. + imageNode.style.width = ASDimensionMake(forcedImageSize.width); + imageNode.style.height = ASDimensionMake(forcedImageSize.height); + ASDisplayNodeSizeToFitSize(imageNode, forcedImageSize); + ASSnapshotVerifyNode(imageNode, @"first"); + + imageNode.style.width = ASDimensionMake(200); + imageNode.style.height = ASDimensionMake(200); + ASDisplayNodeSizeToFitSize(imageNode, CGSizeMake(200, 200)); + ASSnapshotVerifyNode(imageNode, @"second"); + + XCTAssert(CGImageGetWidth((CGImageRef)imageNode.contents) == forcedImageSize.width * imageNode.contentsScale && + CGImageGetHeight((CGImageRef)imageNode.contents) == forcedImageSize.height * imageNode.contentsScale, + @"Contents should be 100 x 100 by contents scale."); +} + +- (void)testTintColorBlock +{ + UIImage *test = [self testImage]; + UIImage *tinted = ASImageNodeTintColorModificationBlock([UIColor redColor])(test); + ASImageNode *node = [[ASImageNode alloc] init]; + node.image = tinted; + ASDisplayNodeSizeToFitSize(node, test.size); + + ASSnapshotVerifyNode(node, nil); +} + +- (void)testRoundedCornerBlock +{ + UIGraphicsBeginImageContext(CGSizeMake(100, 100)); + [[UIColor blueColor] setFill]; + UIRectFill(CGRectMake(0, 0, 100, 100)); + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + UIImage *rounded = ASImageNodeRoundBorderModificationBlock(2, [UIColor redColor])(result); + ASImageNode *node = [[ASImageNode alloc] init]; + node.image = rounded; + ASDisplayNodeSizeToFitSize(node, rounded.size); + + ASSnapshotVerifyNode(node, nil); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASInsetLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASInsetLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..d6047d35f3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASInsetLayoutSpecSnapshotTests.mm @@ -0,0 +1,117 @@ +// +// ASInsetLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import + +typedef NS_OPTIONS(NSUInteger, ASInsetLayoutSpecTestEdge) { + ASInsetLayoutSpecTestEdgeTop = 1 << 0, + ASInsetLayoutSpecTestEdgeLeft = 1 << 1, + ASInsetLayoutSpecTestEdgeBottom = 1 << 2, + ASInsetLayoutSpecTestEdgeRight = 1 << 3, +}; + +static CGFloat insetForEdge(NSUInteger combination, ASInsetLayoutSpecTestEdge edge, CGFloat insetValue) +{ + return combination & edge ? INFINITY : insetValue; +} + +static UIEdgeInsets insetsForCombination(NSUInteger combination, CGFloat insetValue) +{ + return { + .top = insetForEdge(combination, ASInsetLayoutSpecTestEdgeTop, insetValue), + .left = insetForEdge(combination, ASInsetLayoutSpecTestEdgeLeft, insetValue), + .bottom = insetForEdge(combination, ASInsetLayoutSpecTestEdgeBottom, insetValue), + .right = insetForEdge(combination, ASInsetLayoutSpecTestEdgeRight, insetValue), + }; +} + +static NSString *nameForInsets(UIEdgeInsets insets) +{ + return [NSString stringWithFormat:@"%.f-%.f-%.f-%.f", insets.top, insets.left, insets.bottom, insets.right]; +} + +@interface ASInsetLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASInsetLayoutSpecSnapshotTests + +- (void)testInsetsWithVariableSize +{ + for (NSUInteger combination = 0; combination < 16; combination++) { + UIEdgeInsets insets = insetsForCombination(combination, 10); + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor grayColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], {10, 10}); + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:insets + child:foregroundNode] + background:backgroundNode]; + + static ASSizeRange kVariableSize = {{0, 0}, {300, 300}}; + [self testLayoutSpec:layoutSpec + sizeRange:kVariableSize + subnodes:@[backgroundNode, foregroundNode] + identifier:nameForInsets(insets)]; + } +} + +- (void)testInsetsWithFixedSize +{ + for (NSUInteger combination = 0; combination < 16; combination++) { + UIEdgeInsets insets = insetsForCombination(combination, 10); + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor grayColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], {10, 10}); + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:insets + child:foregroundNode] + background:backgroundNode]; + + static ASSizeRange kFixedSize = {{300, 300}, {300, 300}}; + [self testLayoutSpec:layoutSpec + sizeRange:kFixedSize + subnodes:@[backgroundNode, foregroundNode] + identifier:nameForInsets(insets)]; + } +} + +/** Regression test, there was a bug mixing insets with infinite and zero sizes */ +- (void)testInsetsWithInfinityAndZeroInsetValue +{ + for (NSUInteger combination = 0; combination < 16; combination++) { + UIEdgeInsets insets = insetsForCombination(combination, 0); + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor grayColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], {10, 10}); + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:insets + child:foregroundNode] + background:backgroundNode]; + + static ASSizeRange kFixedSize = {{300, 300}, {300, 300}}; + [self testLayoutSpec:layoutSpec + sizeRange:kFixedSize + subnodes:@[backgroundNode, foregroundNode] + identifier:nameForInsets(insets)]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASIntegerMapTests.mm b/submodules/AsyncDisplayKit/Tests/ASIntegerMapTests.mm new file mode 100644 index 0000000000..d23e5bf49f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASIntegerMapTests.mm @@ -0,0 +1,113 @@ +// +// ASIntegerMapTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import "ASIntegerMap.h" + +@interface ASIntegerMapTests : ASTestCase + +@end + +@implementation ASIntegerMapTests + +- (void)testIsEqual +{ + ASIntegerMap *map = [[ASIntegerMap alloc] init]; + [map setInteger:1 forKey:0]; + ASIntegerMap *alsoMap = [[ASIntegerMap alloc] init]; + [alsoMap setInteger:1 forKey:0]; + ASIntegerMap *notMap = [[ASIntegerMap alloc] init]; + [notMap setInteger:2 forKey:0]; + XCTAssertEqualObjects(map, alsoMap); + XCTAssertNotEqualObjects(map, notMap); +} + +#pragma mark - Changeset mapping + +/// 1 item, no changes -> identity map +- (void)testEmptyChange +{ + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:1 deleted:nil inserted:nil]; + XCTAssertEqual(map, ASIntegerMap.identityMap); +} + +/// 0 items -> empty map +- (void)testChangeOnNoData +{ + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:0 deleted:nil inserted:nil]; + XCTAssertEqual(map, ASIntegerMap.emptyMap); +} + +/// 2 items, delete 0 +- (void)testBasicChange1 +{ + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:2 deleted:[NSIndexSet indexSetWithIndex:0] inserted:nil]; + XCTAssertEqual([map integerForKey:0], NSNotFound); + XCTAssertEqual([map integerForKey:1], 0); + XCTAssertEqual([map integerForKey:2], NSNotFound); +} + +/// 2 items, insert 0 +- (void)testBasicChange2 +{ + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:2 deleted:nil inserted:[NSIndexSet indexSetWithIndex:0]]; + XCTAssertEqual([map integerForKey:0], 1); + XCTAssertEqual([map integerForKey:1], 2); + XCTAssertEqual([map integerForKey:2], NSNotFound); +} + +/// 2 items, insert 0, delete 0 +- (void)testChange1 +{ + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:2 deleted:[NSIndexSet indexSetWithIndex:0] inserted:[NSIndexSet indexSetWithIndex:0]]; + XCTAssertEqual([map integerForKey:0], NSNotFound); + XCTAssertEqual([map integerForKey:1], 1); + XCTAssertEqual([map integerForKey:2], NSNotFound); +} + +/// 4 items, insert {0-1, 3} +- (void)testChange2 +{ + NSMutableIndexSet *inserts = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]; + [inserts addIndex:3]; + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:4 deleted:nil inserted:inserts]; + XCTAssertEqual([map integerForKey:0], 2); + XCTAssertEqual([map integerForKey:1], 4); + XCTAssertEqual([map integerForKey:2], 5); + XCTAssertEqual([map integerForKey:3], 6); +} + +/// 4 items, delete {0-1, 3} +- (void)testChange3 +{ + NSMutableIndexSet *deletes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]; + [deletes addIndex:3]; + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:4 deleted:deletes inserted:nil]; + XCTAssertEqual([map integerForKey:0], NSNotFound); + XCTAssertEqual([map integerForKey:1], NSNotFound); + XCTAssertEqual([map integerForKey:2], 0); + XCTAssertEqual([map integerForKey:3], NSNotFound); +} + +/// 5 items, delete {0-1, 3} insert {1-2, 4} +- (void)testChange4 +{ + NSMutableIndexSet *deletes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]; + [deletes addIndex:3]; + NSMutableIndexSet *inserts = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]; + [inserts addIndex:4]; + ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:5 deleted:deletes inserted:inserts]; + XCTAssertEqual([map integerForKey:0], NSNotFound); + XCTAssertEqual([map integerForKey:1], NSNotFound); + XCTAssertEqual([map integerForKey:2], 0); + XCTAssertEqual([map integerForKey:3], NSNotFound); + XCTAssertEqual([map integerForKey:4], 3); + XCTAssertEqual([map integerForKey:5], NSNotFound); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutElementStyleTests.mm b/submodules/AsyncDisplayKit/Tests/ASLayoutElementStyleTests.mm new file mode 100644 index 0000000000..f6f95066e8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutElementStyleTests.mm @@ -0,0 +1,127 @@ +// +// ASLayoutElementStyleTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASXCTExtensions.h" +#import + +#pragma mark - ASLayoutElementStyleTestsDelegate + +@interface ASLayoutElementStyleTestsDelegate : NSObject +@property (copy, nonatomic) NSString *propertyNameChanged; +@end + +@implementation ASLayoutElementStyleTestsDelegate + +- (void)style:(id)style propertyDidChange:(NSString *)propertyName +{ + self.propertyNameChanged = propertyName; +} + +@end + +#pragma mark - ASLayoutElementStyleTests + +@interface ASLayoutElementStyleTests : XCTestCase + +@end + +@implementation ASLayoutElementStyleTests + +- (void)testSettingSize +{ + ASLayoutElementStyle *style = [ASLayoutElementStyle new]; + + style.width = ASDimensionMake(100); + style.height = ASDimensionMake(100); + XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionMake(100))); + XCTAssertTrue(ASDimensionEqualToDimension(style.height, ASDimensionMake(100))); + + style.minWidth = ASDimensionMake(100); + style.minHeight = ASDimensionMake(100); + XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionMake(100))); + XCTAssertTrue(ASDimensionEqualToDimension(style.height, ASDimensionMake(100))); + + style.maxWidth = ASDimensionMake(100); + style.maxHeight = ASDimensionMake(100); + XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionMake(100))); + XCTAssertTrue(ASDimensionEqualToDimension(style.height, ASDimensionMake(100))); +} + +- (void)testSettingSizeViaCGSize +{ + ASLayoutElementStyle *style = [ASLayoutElementStyle new]; + + ASXCTAssertEqualSizes(style.preferredSize, CGSizeZero); + + CGSize size = CGSizeMake(100, 100); + + style.preferredSize = size; + ASXCTAssertEqualSizes(style.preferredSize, size); + XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionMakeWithPoints(size.width))); + XCTAssertTrue(ASDimensionEqualToDimension(style.height, ASDimensionMakeWithPoints(size.height))); + + style.minSize = size; + XCTAssertTrue(ASDimensionEqualToDimension(style.minWidth, ASDimensionMakeWithPoints(size.width))); + XCTAssertTrue(ASDimensionEqualToDimension(style.minHeight, ASDimensionMakeWithPoints(size.height))); + + style.maxSize = size; + XCTAssertTrue(ASDimensionEqualToDimension(style.maxWidth, ASDimensionMakeWithPoints(size.width))); + XCTAssertTrue(ASDimensionEqualToDimension(style.maxHeight, ASDimensionMakeWithPoints(size.height))); +} + +- (void)testReadingInvalidSizeForPreferredSize +{ + ASLayoutElementStyle *style = [ASLayoutElementStyle new]; + + XCTAssertNoThrow(style.preferredSize); + + style.width = ASDimensionMake(ASDimensionUnitFraction, 0.5); + XCTAssertThrows(style.preferredSize); + + style.preferredSize = CGSizeMake(100, 100); + XCTAssertNoThrow(style.preferredSize); +} + +- (void)testSettingSizeViaLayoutSize +{ + ASLayoutElementStyle *style = [ASLayoutElementStyle new]; + + ASLayoutSize layoutSize = ASLayoutSizeMake(ASDimensionMake(100), ASDimensionMake(100)); + + style.preferredLayoutSize = layoutSize; + XCTAssertTrue(ASDimensionEqualToDimension(style.width, layoutSize.width)); + XCTAssertTrue(ASDimensionEqualToDimension(style.height, layoutSize.height)); + XCTAssertTrue(ASDimensionEqualToDimension(style.preferredLayoutSize.width, layoutSize.width)); + XCTAssertTrue(ASDimensionEqualToDimension(style.preferredLayoutSize.height, layoutSize.height)); + + style.minLayoutSize = layoutSize; + XCTAssertTrue(ASDimensionEqualToDimension(style.minWidth, layoutSize.width)); + XCTAssertTrue(ASDimensionEqualToDimension(style.minHeight, layoutSize.height)); + XCTAssertTrue(ASDimensionEqualToDimension(style.minLayoutSize.width, layoutSize.width)); + XCTAssertTrue(ASDimensionEqualToDimension(style.minLayoutSize.height, layoutSize.height)); + + style.maxLayoutSize = layoutSize; + XCTAssertTrue(ASDimensionEqualToDimension(style.maxWidth, layoutSize.width)); + XCTAssertTrue(ASDimensionEqualToDimension(style.maxHeight, layoutSize.height)); + XCTAssertTrue(ASDimensionEqualToDimension(style.maxLayoutSize.width, layoutSize.width)); + XCTAssertTrue(ASDimensionEqualToDimension(style.maxLayoutSize.height, layoutSize.height)); +} + +- (void)testSettingPropertiesWillCallDelegate +{ + ASLayoutElementStyleTestsDelegate *delegate = [ASLayoutElementStyleTestsDelegate new]; + ASLayoutElementStyle *style = [[ASLayoutElementStyle alloc] initWithDelegate:delegate]; + XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionAuto)); + style.width = ASDimensionMake(100); + XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionMake(100))); + XCTAssertTrue([delegate.propertyNameChanged isEqualToString:ASLayoutElementStyleWidthProperty]); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutEngineTests.mm b/submodules/AsyncDisplayKit/Tests/ASLayoutEngineTests.mm new file mode 100644 index 0000000000..3222f78b5f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutEngineTests.mm @@ -0,0 +1,591 @@ +// +// ASLayoutEngineTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import "ASLayoutTestNode.h" +#import "ASXCTExtensions.h" +#import "ASTLayoutFixture.h" + +@interface ASLayoutEngineTests : ASTestCase + +@end + +@implementation ASLayoutEngineTests { + ASLayoutTestNode *nodeA; + ASLayoutTestNode *nodeB; + ASLayoutTestNode *nodeC; + ASLayoutTestNode *nodeD; + ASLayoutTestNode *nodeE; + ASTLayoutFixture *fixture1; + ASTLayoutFixture *fixture2; + ASTLayoutFixture *fixture3; + ASTLayoutFixture *fixture4; + ASTLayoutFixture *fixture5; + + // fixtures 1, 3 and 5 share the same exact node A layout spec block. + // we don't want the infra to call -setNeedsLayout when we switch fixtures + // so we need to use the same exact block. + ASLayoutSpecBlock fixture1and3and5NodeALayoutSpecBlock; + + UIWindow *window; + UIViewController *vc; + NSArray *allNodes; + NSTimeInterval verifyDelay; + // See -stubCalculatedLayoutDidChange. + BOOL stubbedCalculatedLayoutDidChange; +} + +- (void)setUp +{ + [super setUp]; + verifyDelay = 3; + window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; + vc = [[UIViewController alloc] init]; + nodeA = [ASLayoutTestNode new]; + nodeA.backgroundColor = [UIColor redColor]; + + // NOTE: nodeB has flexShrink, the others don't + nodeB = [ASLayoutTestNode new]; + nodeB.style.flexShrink = 1; + nodeB.backgroundColor = [UIColor orangeColor]; + + nodeC = [ASLayoutTestNode new]; + nodeC.backgroundColor = [UIColor yellowColor]; + nodeD = [ASLayoutTestNode new]; + nodeD.backgroundColor = [UIColor greenColor]; + nodeE = [ASLayoutTestNode new]; + nodeE.backgroundColor = [UIColor blueColor]; + allNodes = @[ nodeA, nodeB, nodeC, nodeD, nodeE ]; + ASSetDebugNames(nodeA, nodeB, nodeC, nodeD, nodeE); + ASLayoutSpecBlock b = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeD ]]; + }; + fixture1and3and5NodeALayoutSpecBlock = b; + fixture1 = [self createFixture1]; + fixture2 = [self createFixture2]; + fixture3 = [self createFixture3]; + fixture4 = [self createFixture4]; + fixture5 = [self createFixture5]; + + nodeA.frame = vc.view.bounds; + nodeA.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [vc.view addSubnode:nodeA]; + + window.rootViewController = vc; + [window makeKeyAndVisible]; +} + +- (void)tearDown +{ + nodeA.layoutSpecBlock = nil; + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + [super tearDown]; +} + +- (void)testFirstLayoutPassWhenInWindow +{ + [self runFirstLayoutPassWithFixture:fixture1]; +} + +- (void)testSetNeedsLayoutAndNormalLayoutPass +{ + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // skip nodeB because its layout doesn't change. + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread(); + }]; + OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread(); + } + + [window layoutIfNeeded]; + [self verifyFixture:fixture2]; +} + +/** + * Transition from fixture1 to Fixture2 on node A. + * + * Expect A and D to calculate once off main, and + * to receive calculatedLayoutDidChange on main, + * then to get the measurement completion call on main, + * then to get animateLayoutTransition: and didCompleteLayoutTransition: on main. + */ +- (void)testLayoutTransitionWithAsyncMeasurement +{ + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // Expect A, C, E to calculate new layouts off-main + // dispatch_once onto main to run our injectedMainThread work while the transition calculates. + __block dispatch_block_t injectedMainThreadWork = nil; + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .offMainThread() + .andDo(^(NSInvocation *inv) { + // On first calculateLayoutThatFits, schedule our injected main thread work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + injectedMainThreadWork(); + }); + }); + }); + }]; + } + + // The code in this section is designed to move in time order, all on the main thread: + + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + + // Trigger the layout transition. + __block dispatch_block_t measurementCompletionBlock = nil; + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{ + measurementCompletionBlock(); + }]; + + // This block will get run after bg layout calculate starts, but before measurementCompletion + __block BOOL injectedMainThreadWorkDone = NO; + injectedMainThreadWork = ^{ + injectedMainThreadWorkDone = YES; + + [window layoutIfNeeded]; + + // Ensure we're still on the old layout. We should stay on this until the transition completes. + [self verifyFixture:fixture1]; + }; + + measurementCompletionBlock = ^{ + XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran."); + }; + + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + + [self verifyFixture:fixture2]; +} + +/** + * Transition from fixture1 to Fixture2 on node A. + * + * Expect A and D to calculate once on main, and + * to receive calculatedLayoutDidChange on main, + * then to get animateLayoutTransition: and didCompleteLayoutTransition: on main. + */ +- (void)testLayoutTransitionWithSyncMeasurement +{ + [self stubCalculatedLayoutDidChange]; + + // Precondition + XCTAssertFalse(CGSizeEqualToSize(fixture5.layout.size, fixture1.layout.size)); + + // First, apply fixture 5 and run a measurement pass, but don't run a layout pass + // After this step, nodes will have pending layouts that are not yet applied + [fixture5 apply]; + [fixture5 withSizeRangesForAllNodesUsingBlock:^(ASLayoutTestNode * _Nonnull node, ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .onMainThread(); + }]; + + [nodeA layoutThatFits:ASSizeRangeMake(fixture5.layout.size)]; + + // Assert that node A has layout size and size range from fixture 5 + XCTAssertTrue(CGSizeEqualToSize(fixture5.layout.size, nodeA.calculatedSize)); + XCTAssertTrue(ASSizeRangeEqualToSizeRange([fixture5 firstSizeRangeForNode:nodeA], nodeA.constrainedSizeForCalculatedLayout)); + + // Then switch to fixture 1 and kick off a synchronous layout transition + // Unapplied pending layouts from the previous measurement pass will be outdated + [fixture1 apply]; + [fixture1 withSizeRangesForAllNodesUsingBlock:^(ASLayoutTestNode * _Nonnull node, ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .onMainThread(); + }]; + + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:NO measurementCompletion:nil]; + + // Assert that node A picks up new layout size and size range from fixture 1 + XCTAssertTrue(CGSizeEqualToSize(fixture1.layout.size, nodeA.calculatedSize)); + XCTAssertTrue(ASSizeRangeEqualToSizeRange([fixture1 firstSizeRangeForNode:nodeA], nodeA.constrainedSizeForCalculatedLayout)); + + [window layoutIfNeeded]; + [self verifyFixture:fixture1]; +} + +/** + * Start at fixture 1. + * Trigger an async transition to fixture 2. + * While it's measuring, on main switch to fixture 4 (setNeedsLayout A, D) and run a CA layout pass. + * + * Correct behavior, we end up at fixture 4 since it's newer. + * Current incorrect behavior, we end up at fixture 2 and we remeasure surviving node C. + * Note: incorrect behavior likely introduced by the early check in __layout added in + * https://github.com/facebookarchive/AsyncDisplayKit/pull/2657 + */ +- (void)DISABLE_testASetNeedsLayoutInterferingWithTheCurrentTransition +{ + static BOOL enforceCorrectBehavior = NO; + + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // Expect A, C, E to calculate new layouts off-main + // dispatch_once onto main to run our injectedMainThread work while the transition calculates. + __block dispatch_block_t injectedMainThreadWork = nil; + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .offMainThread() + .andDo(^(NSInvocation *inv) { + // On first calculateLayoutThatFits, schedule our injected main thread work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + injectedMainThreadWork(); + }); + }); + }); + }]; + } + + // The code in this section is designed to move in time order, all on the main thread: + + // With the current behavior, the transition will continue and complete. + if (!enforceCorrectBehavior) { + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + } + + // Trigger the layout transition. + __block dispatch_block_t measurementCompletionBlock = nil; + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{ + measurementCompletionBlock(); + }]; + + // Injected block will get run on main after bg layout calculate starts, but before measurementCompletion + __block BOOL injectedMainThreadWorkDone = NO; + injectedMainThreadWork = ^{ + as_log_verbose(OS_LOG_DEFAULT, "Begin injectedMainThreadWork"); + injectedMainThreadWorkDone = YES; + + [fixture4 apply]; + as_log_verbose(OS_LOG_DEFAULT, "Did apply new fixture"); + + if (enforceCorrectBehavior) { + // Correct measurement behavior here is unclear, may depend on whether the layouts which + // are common to both fixture2 and fixture4 are available from the cache. + } else { + // Incorrect behavior: nodeC will get measured against its new bounds on main. + const auto cPendingSize = [fixture2 layoutForNode:nodeC].size; + OCMExpect([nodeC.mock calculateLayoutThatFits:ASSizeRangeMake(cPendingSize)]).onMainThread(); + } + [window layoutIfNeeded]; + as_log_verbose(OS_LOG_DEFAULT, "End injectedMainThreadWork"); + }; + + measurementCompletionBlock = ^{ + XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran."); + }; + + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + + // Incorrect behavior: The transition will "win" even though its transitioning to stale data. + if (enforceCorrectBehavior) { + [self verifyFixture:fixture4]; + } else { + [self verifyFixture:fixture2]; + } +} + +/** + * Start on fixture 3 where nodeB is force-shrunk via multipass layout. + * Apply fixture 1, which just changes nodeB's size and calls -setNeedsLayout on it. + * + * This behavior is currently broken. See implementation for correct behavior and incorrect behavior. + */ +- (void)testCallingSetNeedsLayoutOnANodeThatWasSubjectToMultipassLayout +{ + static BOOL const enforceCorrectBehavior = NO; + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture3]; + + // Switch to fixture 1, updating nodeB's desired size and calling -setNeedsLayout + // Now nodeB will fit happily into the stack. + [fixture1 apply]; + + if (enforceCorrectBehavior) { + /* + * Correct behavior: nodeB is remeasured against the first (unconstrained) size + * and when it's discovered that now nodeB fits, nodeA will re-layout and we'll + * end up correctly at fixture1. + */ + OCMExpect([nodeB.mock calculateLayoutThatFits:[fixture3 firstSizeRangeForNode:nodeB]]); + + [fixture1 withSizeRangesForNode:nodeA block:^(ASSizeRange sizeRange) { + OCMExpect([nodeA.mock calculateLayoutThatFits:sizeRange]); + }]; + + [window layoutIfNeeded]; + [self verifyFixture:fixture1]; + } else { + /* + * Incorrect behavior: nodeB is remeasured against the second (fixed-width) constraint. + * The returned value (8) is clamped to the fixed with (7), and then compared to the previous + * width (7) and we decide not to propagate up the invalidation, and we stay stuck on the old + * layout (fixture3). + */ + OCMExpect([nodeB.mock calculateLayoutThatFits:nodeB.constrainedSizeForCalculatedLayout]); + [window layoutIfNeeded]; + [self verifyFixture:fixture3]; + } +} + +#pragma mark - Helpers + +- (void)verifyFixture:(ASTLayoutFixture *)fixture +{ + const auto expected = fixture.layout; + + // Ensure expected == frames + const auto frames = [fixture.rootNode currentLayoutBasedOnFrames]; + if (![expected isEqual:frames]) { + XCTFail(@"\n*** Layout verification failed – frames don't match expected. ***\nGot:\n%@\nExpected:\n%@", [frames recursiveDescription], [expected recursiveDescription]); + } + + // Ensure expected == calculatedLayout + const auto calculated = fixture.rootNode.calculatedLayout; + if (![expected isEqual:calculated]) { + XCTFail(@"\n*** Layout verification failed – calculated layout doesn't match expected. ***\nGot:\n%@\nExpected:\n%@", [calculated recursiveDescription], [expected recursiveDescription]); + } +} + +/** + * Stubs calculatedLayoutDidChange for all nodes. + * + * It's not really a core layout engine method, and it's also + * currently bugged and gets called a lot so for most + * tests its better not to have expectations about it littered around. + * https://github.com/TextureGroup/Texture/issues/422 + */ +- (void)stubCalculatedLayoutDidChange +{ + stubbedCalculatedLayoutDidChange = YES; + for (ASLayoutTestNode *node in allNodes) { + OCMStub([node.mock calculatedLayoutDidChange]); + } +} + +/** + * Fixture 1: A basic horizontal stack, all single-pass. + * + * [A: HorizStack([B, C, D])]. A is (10x1), B is (1x1), C is (2x1), D is (1x1) + */ +- (ASTLayoutFixture *)createFixture1 +{ + const auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + const auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + const auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + const auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + const auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3and5NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +/** + * Fixture 2: A simple transition away from fixture 1. + * + * [A: HorizStack([B, C, E])]. A is (10x1), B is (1x1), C is (4x1), E is (1x1) + * + * From fixture 1: + * B survives with same layout + * C survives with new layout + * D is removed + * E joins with first layout + */ +- (ASTLayoutFixture *)createFixture2 +{ + const auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + const auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + const auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{4,1} position:{3,0} sublayouts:nil]; + + // nodeE + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE]; + const auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + const auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutE ]]; + fixture.layout = layoutA; + + ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeE ]]; + }; + [fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA]; + return fixture; +} + +/** + * Fixture 3: Multipass stack layout + * + * [A: HorizStack([B, C, D])]. A is (10x1), B is (7x1), C is (2x1), D is (1x1) + * + * nodeB (which has flexShrink=1) will return 8x1 for its size during the first + * stack pass, and it'll be subject to a second pass where it returns 7x1. + * + */ +- (ASTLayoutFixture *)createFixture3 +{ + const auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB wants 8,1 but it will settle for 7,1 + [fixture setReturnedSize:{8,1} forNode:nodeB]; + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + [fixture addSizeRange:{{7, 0}, {7, 1}} forNode:nodeB]; + const auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{7,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + const auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{7,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + const auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + const auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3and5NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +/** + * Fixture 4: A different simple transition away from fixture 1. + * + * [A: HorizStack([B, D, E])]. A is (10x1), B is (1x1), D is (2x1), E is (1x1) + * + * From fixture 1: + * B survives with same layout + * C is removed + * D survives with new layout + * E joins with first layout + * + * From fixture 2: + * B survives with same layout + * C is removed + * D joins with first layout + * E survives with same layout + */ +- (ASTLayoutFixture *)createFixture4 +{ + const auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + const auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + const auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeE + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE]; + const auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + const auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutD, layoutE ]]; + fixture.layout = layoutA; + + ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeD, nodeE ]]; + }; + [fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA]; + return fixture; +} + +/** + * Fixture 5: Same as fixture 1, but with a bigger root node (node A). + * + * [A: HorizStack([B, C, D])]. A is (15x1), B is (1x1), C is (2x1), D is (1x1) + */ +- (ASTLayoutFixture *)createFixture5 +{ + const auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + const auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + const auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + const auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{15, 1}, {15, 1}} forNode:nodeA]; + const auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{15,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3and5NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +- (void)runFirstLayoutPassWithFixture:(ASTLayoutFixture *)fixture +{ + [fixture apply]; + [fixture withSizeRangesForAllNodesUsingBlock:^(ASLayoutTestNode * _Nonnull node, ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread(); + + if (!stubbedCalculatedLayoutDidChange) { + OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread(); + } + }]; + + // Trigger CA layout pass. + [window layoutIfNeeded]; + + // Make sure it went through. + [self verifyFixture:fixture]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutFlatteningTests.mm b/submodules/AsyncDisplayKit/Tests/ASLayoutFlatteningTests.mm new file mode 100644 index 0000000000..0a1fa02261 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutFlatteningTests.mm @@ -0,0 +1,206 @@ +// +// ASLayoutFlatteningTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@interface ASLayoutFlatteningTests : XCTestCase +@end + +@implementation ASLayoutFlatteningTests + +static ASLayout *layoutWithCustomPosition(CGPoint position, id element, NSArray *sublayouts) +{ + return [ASLayout layoutWithLayoutElement:element + size:CGSizeMake(100, 100) + position:position + sublayouts:sublayouts]; +} + +static ASLayout *layout(id element, NSArray *sublayouts) +{ + return layoutWithCustomPosition(CGPointZero, element, sublayouts); +} + +- (void)testThatFlattenedLayoutContainsOnlyDirectSubnodesInValidOrder +{ + ASLayout *flattenedLayout; + + @autoreleasepool { + NSMutableArray *subnodes = [NSMutableArray array]; + NSMutableArray *layoutSpecs = [NSMutableArray array]; + NSMutableArray *indirectSubnodes = [NSMutableArray array]; + + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)(void) = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)(void) = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + + NSArray *sublayouts = @[ + layout(subnode(), @[ + layout(indirectSubnode(), @[]), + ]), + layout(layoutSpec(), @[ + layout(subnode(), @[]), + layout(layoutSpec(), @[ + layout(layoutSpec(), @[]), + layout(subnode(), @[]), + ]), + layout(layoutSpec(), @[]), + ]), + layout(layoutSpec(), @[ + layout(subnode(), @[ + layout(indirectSubnode(), @[]), + layout(indirectSubnode(), @[ + layout(indirectSubnode(), @[]) + ]), + ]) + ]), + layout(subnode(), @[]), + ]; + + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASLayout *originalLayout = [ASLayout layoutWithLayoutElement:rootNode + size:CGSizeMake(1000, 1000) + sublayouts:sublayouts]; + flattenedLayout = [originalLayout filteredNodeLayoutTree]; + NSArray *flattenedSublayouts = flattenedLayout.sublayouts; + NSUInteger sublayoutsCount = flattenedSublayouts.count; + + XCTAssertEqualObjects(originalLayout.layoutElement, flattenedLayout.layoutElement, @"The root node should be reserved"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null"); + XCTAssertEqual(subnodes.count, sublayoutsCount, @"Flattened layout should only contain direct subnodes"); + for (int i = 0; i < sublayoutsCount; i++) { + XCTAssertEqualObjects(subnodes[i], flattenedSublayouts[i].layoutElement, @"Sublayouts should be in correct order (flattened in DFS fashion)"); + } + } + + for (ASLayout *sublayout in flattenedLayout.sublayouts) { + XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained"); + XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts"); + } +} + +#pragma mark - Test reusing ASLayouts while flattening + +- (void)testThatLayoutWithNonNullPositionIsNotReused +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASLayout *originalLayout = layoutWithCustomPosition(CGPointMake(10, 10), rootNode, @[]); + ASLayout *flattenedLayout = [originalLayout filteredNodeLayoutTree]; + XCTAssertNotEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of a root layout should be null"); +} + +- (void)testThatLayoutWithNullPositionAndNoSublayoutIsReused +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, rootNode, @[]); + ASLayout *flattenedLayout = [originalLayout filteredNodeLayoutTree]; + XCTAssertEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of a root layout should be null"); +} + +- (void)testThatLayoutWithNullPositionAndFlattenedNodeSublayoutsIsReused +{ + ASLayout *flattenedLayout; + + @autoreleasepool { + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + NSMutableArray *subnodes = [NSMutableArray array]; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, + rootNode, + @[ + layoutWithCustomPosition(CGPointMake(10, 10), subnode(), @[]), + layoutWithCustomPosition(CGPointMake(20, 20), subnode(), @[]), + layoutWithCustomPosition(CGPointMake(30, 30), subnode(), @[]), + ]); + flattenedLayout = [originalLayout filteredNodeLayoutTree]; + XCTAssertEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null"); + } + + for (ASLayout *sublayout in flattenedLayout.sublayouts) { + XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained"); + XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts"); + } +} + +- (void)testThatLayoutWithNullPositionAndUnflattenedSublayoutsIsNotReused +{ + ASLayout *flattenedLayout; + + @autoreleasepool { + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + NSMutableArray *subnodes = [NSMutableArray array]; + NSMutableArray *layoutSpecs = [NSMutableArray array]; + NSMutableArray *indirectSubnodes = [NSMutableArray array]; + NSMutableArray *reusedLayouts = [NSMutableArray array]; + + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)(void) = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)(void) = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + ASLayout *(^reusedLayout)(ASDisplayNode *) = ^ASLayout *(ASDisplayNode *subnode) { [reusedLayouts addObject:layout(subnode, @[])]; return [reusedLayouts lastObject]; }; + + /* + * Layouts with sublayouts of both nodes and layout specs should not be reused. + * However, all flattened node sublayouts with valid position should be reused. + */ + ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, + rootNode, + @[ + reusedLayout(subnode()), + // The 2 node sublayouts below should be reused although they are in a layout spec sublayout. + // That is because each of them have an absolute position of zero. + // This case can happen, for example, as the result of a background/overlay layout spec. + layout(layoutSpec(), @[ + reusedLayout(subnode()), + reusedLayout(subnode()) + ]), + layout(subnode(), @[ + layout(layoutSpec(), @[]) + ]), + layout(subnode(), @[ + layout(indirectSubnode(), @[]) + ]), + layoutWithCustomPosition(CGPointMake(10, 10), subnode(), @[]), + // The 2 node sublayouts below shouldn't be reused because they have non-zero absolute positions. + layoutWithCustomPosition(CGPointMake(20, 20), layoutSpec(), @[ + layout(subnode(), @[]), + layout(subnode(), @[]) + ]), + ]); + flattenedLayout = [originalLayout filteredNodeLayoutTree]; + NSArray *flattenedSublayouts = flattenedLayout.sublayouts; + NSUInteger sublayoutsCount = flattenedSublayouts.count; + + XCTAssertNotEqualObjects(originalLayout, flattenedLayout, @"Original layout should not be reused"); + XCTAssertEqualObjects(originalLayout.layoutElement, flattenedLayout.layoutElement, @"The root node should be reserved"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null"); + XCTAssertTrue(reusedLayouts.count <= sublayoutsCount, @"Some sublayouts can't be reused"); + XCTAssertEqual(subnodes.count, sublayoutsCount, @"Flattened layout should only contain direct subnodes"); + int numOfActualReusedLayouts = 0; + for (int i = 0; i < sublayoutsCount; i++) { + ASLayout *sublayout = flattenedSublayouts[i]; + XCTAssertEqualObjects(subnodes[i], sublayout.layoutElement, @"Sublayouts should be in correct order (flattened in DFS fashion)"); + if ([reusedLayouts containsObject:sublayout]) { + numOfActualReusedLayouts++; + } + } + XCTAssertEqual(numOfActualReusedLayouts, reusedLayouts.count, @"Should reuse all layouts that can be reused"); + } + + for (ASLayout *sublayout in flattenedLayout.sublayouts) { + XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained"); + XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts"); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutSpecSnapshotTestsHelper.h b/submodules/AsyncDisplayKit/Tests/ASLayoutSpecSnapshotTestsHelper.h new file mode 100644 index 0000000000..740017ee9e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutSpecSnapshotTestsHelper.h @@ -0,0 +1,45 @@ +// +// ASLayoutSpecSnapshotTestsHelper.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASSnapshotTestCase.h" +#import + +@class ASLayoutSpec; + +@interface ASLayoutSpecSnapshotTestCase: ASSnapshotTestCase +/** + Test the layout spec or records a snapshot if recordMode is YES. + @param layoutSpec The layout spec under test or to snapshot + @param sizeRange The size range used to calculate layout of the given layout spec. + @param subnodes An array of ASDisplayNodes used within the layout spec. + @param identifier An optional identifier, used to identify this snapshot test. + + @discussion In order to make the layout spec visible, it is embeded to a ASDisplayNode host. + Any subnodes used within the layout spec must be provided. + They will be added to the host in the same order as the array. + */ +- (void)testLayoutSpec:(ASLayoutSpec *)layoutSpec + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier; +@end + +__attribute__((overloadable)) static inline ASDisplayNode *ASDisplayNodeWithBackgroundColor(UIColor *backgroundColor, CGSize size) { + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.layerBacked = YES; + node.backgroundColor = backgroundColor; + node.style.width = ASDimensionMakeWithPoints(size.width); + node.style.height = ASDimensionMakeWithPoints(size.height); + return node; +} + +__attribute__((overloadable)) static inline ASDisplayNode *ASDisplayNodeWithBackgroundColor(UIColor *backgroundColor) +{ + return ASDisplayNodeWithBackgroundColor(backgroundColor, CGSizeZero); +} diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutSpecSnapshotTestsHelper.mm b/submodules/AsyncDisplayKit/Tests/ASLayoutSpecSnapshotTestsHelper.mm new file mode 100644 index 0000000000..dec82d4c39 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutSpecSnapshotTestsHelper.mm @@ -0,0 +1,62 @@ +// +// ASLayoutSpecSnapshotTestsHelper.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import +#import +#import + +@interface ASTestNode : ASDisplayNode +@property (nonatomic, nullable) ASLayoutSpec *layoutSpecUnderTest; +@end + +@implementation ASLayoutSpecSnapshotTestCase + +- (void)setUp +{ + [super setUp]; + self.recordMode = NO; +} + +- (void)testLayoutSpec:(ASLayoutSpec *)layoutSpec + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier +{ + ASTestNode *node = [[ASTestNode alloc] init]; + + for (ASDisplayNode *subnode in subnodes) { + [node addSubnode:subnode]; + } + + node.layoutSpecUnderTest = layoutSpec; + + ASDisplayNodeSizeToFitSizeRange(node, sizeRange); + ASSnapshotVerifyNode(node, identifier); +} + +@end + +@implementation ASTestNode +- (instancetype)init +{ + if (self = [super init]) { + self.layerBacked = YES; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return _layoutSpecUnderTest; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutSpecTests.mm b/submodules/AsyncDisplayKit/Tests/ASLayoutSpecTests.mm new file mode 100644 index 0000000000..ff13b66553 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutSpecTests.mm @@ -0,0 +1,112 @@ +// +// ASLayoutSpecTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#pragma mark - ASDKExtendedLayoutSpec + +/* + * Extend the ASDKExtendedLayoutElement + * It adds a + * - primitive / CGFloat (extendedWidth) + * - struct / ASDimension (extendedDimension) + * - primitive / ASStackLayoutDirection (extendedDirection) + */ +@protocol ASDKExtendedLayoutElement +@property (nonatomic) CGFloat extendedWidth; +@property (nonatomic) ASDimension extendedDimension; +@property (copy, nonatomic) NSString *extendedName; +@end + +/* + * Let the ASLayoutElementStyle conform to the ASDKExtendedLayoutElement protocol and add properties implementation + */ +@interface ASLayoutElementStyle (ASDKExtendedLayoutElement) +@end + +@implementation ASLayoutElementStyle (ASDKExtendedLayoutElement) +ASDK_STYLE_PROP_PRIM(CGFloat, extendedWidth, setExtendedWidth, 0); +ASDK_STYLE_PROP_STR(ASDimension, extendedDimension, setExtendedDimension, ASDimensionMake(ASDimensionUnitAuto, 0)); +ASDK_STYLE_PROP_OBJ(NSString *, extendedName, setExtendedName); +@end + +/* + * As the ASLayoutElementStyle conforms to the ASDKExtendedLayoutElement protocol now, ASDKExtendedLayoutElement properties + * can be accessed in ASDKExtendedLayoutSpec + */ +@interface ASDKExtendedLayoutSpec : ASLayoutSpec +@end + +@implementation ASDKExtendedLayoutSpec + +- (void)doSetSomeStyleValuesToChildren +{ + for (id child in self.children) { + child.style.extendedWidth = 100; + child.style.extendedDimension = ASDimensionMake(100); + child.style.extendedName = @"ASDK"; + } +} + +- (void)doUseSomeStyleValuesFromChildren +{ + for (id child in self.children) { + __unused CGFloat extendedWidth = child.style.extendedWidth; + __unused ASDimension extendedDimension = child.style.extendedDimension; + __unused NSString *extendedName = child.style.extendedName; + } +} + +@end + + +#pragma mark - ASLayoutSpecTests + +@interface ASLayoutSpecTests : XCTestCase + +@end + +@implementation ASLayoutSpecTests + +- (void)testSetPrimitiveToExtendedStyle +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.style.extendedWidth = 100; + XCTAssert(node.style.extendedWidth == 100, @"Primitive value should be set on extended style"); +} + +- (void)testSetStructToExtendedStyle +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.style.extendedDimension = ASDimensionMake(100); + XCTAssertTrue(ASDimensionEqualToDimension(node.style.extendedDimension, ASDimensionMake(100)), @"Struct should be set on extended style"); +} + +- (void)testSetObjectToExtendedStyle +{ + NSString *extendedName = @"ASDK"; + + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.style.extendedName = extendedName; + XCTAssertEqualObjects(node.style.extendedName, extendedName, @"Object should be set on extended style"); +} + + +- (void)testUseOfExtendedStyleProperties +{ + ASDKExtendedLayoutSpec *extendedLayoutSpec = [ASDKExtendedLayoutSpec new]; + extendedLayoutSpec.children = @[[[ASDisplayNode alloc] init], [[ASDisplayNode alloc] init]]; + XCTAssertNoThrow([extendedLayoutSpec doSetSomeStyleValuesToChildren]); + XCTAssertNoThrow([extendedLayoutSpec doUseSomeStyleValuesFromChildren]); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutTestNode.h b/submodules/AsyncDisplayKit/Tests/ASLayoutTestNode.h new file mode 100644 index 0000000000..231447abe1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutTestNode.h @@ -0,0 +1,38 @@ +// +// ASLayoutTestNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASLayoutTestNode : ASDisplayNode + +/** + * Mocking ASDisplayNodes directly isn't very safe because when you pump mock objects + * into the guts of the framework, bad things happen e.g. direct-ivar-access on mock + * objects will return garbage data. + * + * Instead we create a strict mock for each node, and forward a selected set of calls to it. + */ +@property (nonatomic, readonly) id mock; + +/** + * The size that this node will return in calculateLayoutThatFits (if it doesn't have a layoutSpecBlock). + * + * Changing this value will call -setNeedsLayout on the node. + */ +@property (nonatomic) CGSize testSize; + +/** + * Generate a layout based on the frame of this node and its subtree. + * + * The root layout will be unpositioned. This is so that the returned layout can be directly + * compared to `calculatedLayout` + */ +- (ASLayout *)currentLayoutBasedOnFrames; + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASLayoutTestNode.mm b/submodules/AsyncDisplayKit/Tests/ASLayoutTestNode.mm new file mode 100644 index 0000000000..2805aa36c9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASLayoutTestNode.mm @@ -0,0 +1,88 @@ +// +// ASLayoutTestNode.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutTestNode.h" +#import +#import "OCMockObject+ASAdditions.h" + +@implementation ASLayoutTestNode + +- (instancetype)init +{ + if (self = [super init]) { + _mock = OCMStrictClassMock([ASDisplayNode class]); + + // If errors occur (e.g. unexpected method) we need to quickly figure out + // which node is at fault, so we inject the node name into the mock instance + // description. + __weak __typeof(self) weakSelf = self; + [_mock setModifyDescriptionBlock:^(id mock, NSString *baseDescription){ + return [NSString stringWithFormat:@"Mock(%@)", weakSelf.description]; + }]; + } + return self; +} + +- (ASLayout *)currentLayoutBasedOnFrames +{ + return [self _currentLayoutBasedOnFramesForRootNode:YES]; +} + +- (ASLayout *)_currentLayoutBasedOnFramesForRootNode:(BOOL)isRootNode +{ + const auto sublayouts = [[NSMutableArray alloc] init]; + for (ASLayoutTestNode *subnode in self.subnodes) { + [sublayouts addObject:[subnode _currentLayoutBasedOnFramesForRootNode:NO]]; + } + CGPoint rootPosition = isRootNode ? ASPointNull : self.frame.origin; + return [ASLayout layoutWithLayoutElement:self size:self.frame.size position:rootPosition sublayouts:sublayouts]; +} + +- (void)setTestSize:(CGSize)testSize +{ + if (!CGSizeEqualToSize(testSize, _testSize)) { + _testSize = testSize; + [self setNeedsLayout]; + } +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + [_mock calculateLayoutThatFits:constrainedSize]; + + // If we have a layout spec block, or no test size, return super. + if (self.layoutSpecBlock || CGSizeEqualToSize(self.testSize, CGSizeZero)) { + return [super calculateLayoutThatFits:constrainedSize]; + } else { + // Interestingly, the infra will auto-clamp sizes from calculateSizeThatFits, but not from calculateLayoutThatFits. + const auto size = ASSizeRangeClamp(constrainedSize, self.testSize); + return [ASLayout layoutWithLayoutElement:self size:size]; + } +} + +#pragma mark - Forwarding to mock + +- (void)calculatedLayoutDidChange +{ + [_mock calculatedLayoutDidChange]; + [super calculatedLayoutDidChange]; +} + +- (void)didCompleteLayoutTransition:(id)context +{ + [_mock didCompleteLayoutTransition:context]; + [super didCompleteLayoutTransition:context]; +} + +- (void)animateLayoutTransition:(id)context +{ + [_mock animateLayoutTransition:context]; + [super animateLayoutTransition:context]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASMultiplexImageNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASMultiplexImageNodeTests.mm new file mode 100644 index 0000000000..caf86bb48e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASMultiplexImageNodeTests.mm @@ -0,0 +1,265 @@ +// +// ASMultiplexImageNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "NSInvocation+ASTestHelpers.h" + +#import +#import +#import + +#import + +@interface ASMultiplexImageNodeTests : XCTestCase +{ +@private + id mockCache; + id mockDownloader; + id mockDataSource; + id mockDelegate; + ASMultiplexImageNode *imageNode; +} + +@end + +@implementation ASMultiplexImageNodeTests + +#pragma mark - Helpers. + +- (NSURL *)_testImageURL +{ + return [[NSBundle bundleForClass:[self class]] URLForResource:@"logo-square" + withExtension:@"png" + subdirectory:@"TestResources"]; +} + +- (UIImage *)_testImage +{ + return [UIImage imageWithContentsOfFile:[self _testImageURL].path]; +} + +#pragma mark - Unit tests. + +// TODO: add tests for delegate display notifications + +- (void)setUp +{ + [super setUp]; + + mockCache = OCMStrictProtocolMock(@protocol(ASImageCacheProtocol)); + [mockCache setExpectationOrderMatters:YES]; + mockDownloader = OCMStrictProtocolMock(@protocol(ASImageDownloaderProtocol)); + [mockDownloader setExpectationOrderMatters:YES]; + imageNode = [[ASMultiplexImageNode alloc] initWithCache:mockCache downloader:mockDownloader]; + + mockDataSource = OCMStrictProtocolMock(@protocol(ASMultiplexImageNodeDataSource)); + [mockDataSource setExpectationOrderMatters:YES]; + imageNode.dataSource = mockDataSource; + + mockDelegate = OCMProtocolMock(@protocol(ASMultiplexImageNodeDelegate)); + [mockDelegate setExpectationOrderMatters:YES]; + imageNode.delegate = mockDelegate; +} + +- (void)tearDown +{ + OCMVerifyAll(mockDelegate); + OCMVerifyAll(mockDataSource); + OCMVerifyAll(mockDownloader); + OCMVerifyAll(mockCache); + [super tearDown]; +} + +- (void)testDataSourceImageMethod +{ + NSNumber *imageIdentifier = @1; + + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]) + .andReturn([self _testImage]); + + imageNode.imageIdentifiers = @[imageIdentifier]; + [imageNode reloadImageIdentifierSources]; + + // Also expect it to be loaded immediately. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded"); + // And for the image to be equivalent to the image we provided. + XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image), + UIImagePNGRepresentation([self _testImage]), + @"Loaded image isn't the one we provided"); +} + +- (void)testDataSourceURLMethod +{ + NSNumber *imageIdentifier = @1; + + // First expect to be hit for the image directly, and fail to return it. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]) + .andReturn((id)nil); + // BUG: -imageForImageIdentifier is called twice in this case (where we return nil). + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]) + .andReturn((id)nil); + // Then expect to be hit for the URL, which we'll return. + OCMExpect([mockDataSource multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]) + .andReturn([self _testImageURL]); + + // Mock the cache to do a cache-hit for the test image URL. + OCMExpect([mockCache cachedImageWithURL:[self _testImageURL] callbackQueue:OCMOCK_ANY completion:[OCMArg isNotNil]]) + .andDo(^(NSInvocation *inv) { + ASImageCacherCompletion completionBlock = [inv as_argumentAtIndexAsObject:4]; + completionBlock([self _testImage]); + }); + + imageNode.imageIdentifiers = @[imageIdentifier]; + // Kick off loading. + [imageNode reloadImageIdentifierSources]; + + // Also expect it to be loaded immediately. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded"); + // And for the image to be equivalent to the image we provided. + XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image), + UIImagePNGRepresentation([self _testImage]), + @"Loaded image isn't the one we provided"); +} + +- (void)testAddLowerQualityImageIdentifier +{ + // Adding a lower quality image identifier should not cause any loading. + NSNumber *highResIdentifier = @2, *lowResIdentifier = @1; + + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:highResIdentifier]) + .andReturn([self _testImage]); + imageNode.imageIdentifiers = @[highResIdentifier]; + [imageNode reloadImageIdentifierSources]; + + // At this point, we should have the high-res identifier loaded and the DS should have been hit once. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); + + // BUG: We should not get another -imageForImageIdentifier:highResIdentifier. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:highResIdentifier]) + .andReturn([self _testImage]); + + imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier]; + [imageNode reloadImageIdentifierSources]; + + // At this point the high-res should still be loaded, and the data source should not have been hit again (see BUG above). + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); +} + +- (void)testAddHigherQualityImageIdentifier +{ + NSNumber *lowResIdentifier = @1, *highResIdentifier = @2; + + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:lowResIdentifier]) + .andReturn([self _testImage]); + + imageNode.imageIdentifiers = @[lowResIdentifier]; + [imageNode reloadImageIdentifierSources]; + + // At this point, we should have the low-res identifier loaded and the DS should have been hit once. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, lowResIdentifier, @"Low res identifier should be loaded."); + + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:highResIdentifier]) + .andReturn([self _testImage]); + + imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier]; + [imageNode reloadImageIdentifierSources]; + + // At this point the high-res should be loaded, and the data source should been hit twice. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); +} + +- (void)testIntermediateImageDownloading +{ + imageNode.downloadsIntermediateImages = YES; + + // Let them call URLForImageIdentifier all they want. + OCMStub([mockDataSource multiplexImageNode:imageNode URLForImageIdentifier:[OCMArg isNotNil]]); + + // Set up a few identifiers to load. + NSInteger identifierCount = 5; + NSMutableArray *imageIdentifiers = [NSMutableArray array]; + for (NSInteger identifier = identifierCount; identifier > 0; identifier--) { + [imageIdentifiers addObject:@(identifier)]; + } + + // Create the array of IDs in the order we expect them to get -imageForImageIdentifier: + // BUG: The second to last ID (the last one that returns nil) will get -imageForImageIdentifier: called + // again after the last ID (the one that returns non-nil). + id secondToLastID = imageIdentifiers[identifierCount - 2]; + NSArray *imageIdentifiersThatWillBeCalled = [imageIdentifiers arrayByAddingObject:secondToLastID]; + + for (id imageID in imageIdentifiersThatWillBeCalled) { + // Return nil for everything except the worst ID. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageID]) + .andDo(^(NSInvocation *inv){ + id imageID = [inv as_argumentAtIndexAsObject:3]; + if ([imageID isEqual:imageIdentifiers.lastObject]) { + [inv as_setReturnValueWithObject:[self _testImage]]; + } else { + [inv as_setReturnValueWithObject:nil]; + } + }); + } + + imageNode.imageIdentifiers = imageIdentifiers; + [imageNode reloadImageIdentifierSources]; +} + +- (void)testUncachedDownload +{ + // Mock a cache miss. + OCMExpect([mockCache cachedImageWithURL:[self _testImageURL] callbackQueue:OCMOCK_ANY completion:[OCMArg isNotNil]]) + .andDo(^(NSInvocation *inv){ + ASImageCacherCompletion completion = [inv as_argumentAtIndexAsObject:4]; + completion(nil); + }); + + // Mock a 50%-progress URL download. + const CGFloat mockedProgress = 0.5; + OCMExpect([mockDownloader downloadImageWithURL:[self _testImageURL] callbackQueue:OCMOCK_ANY downloadProgress:[OCMArg isNotNil] completion:[OCMArg isNotNil]]) + .andDo(^(NSInvocation *inv){ + // Simulate progress. + ASImageDownloaderProgress progressBlock = [inv as_argumentAtIndexAsObject:4]; + progressBlock(mockedProgress); + + // Simulate completion. + ASImageDownloaderCompletion completionBlock = [inv as_argumentAtIndexAsObject:5]; + completionBlock([self _testImage], nil, nil, nil); + }); + + NSNumber *imageIdentifier = @1; + + // Mock the data source to return nil image, and our test URL. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]); + // BUG: Multiplex image node will call imageForImageIdentifier twice if we return nil. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]); + OCMExpect([mockDataSource multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]) + .andReturn([self _testImageURL]); + + // Mock the delegate to expect start, 50% progress, and completion invocations. + OCMExpect([mockDelegate multiplexImageNode:imageNode didStartDownloadOfImageWithIdentifier:imageIdentifier]); + OCMExpect([mockDelegate multiplexImageNode:imageNode didUpdateDownloadProgress:mockedProgress forImageWithIdentifier:imageIdentifier]); + OCMExpect([mockDelegate multiplexImageNode:imageNode didUpdateImage:[OCMArg isNotNil] withIdentifier:imageIdentifier fromImage:[OCMArg isNil] withIdentifier:[OCMArg isNil]]); + OCMExpect([mockDelegate multiplexImageNode:imageNode didFinishDownloadingImageWithIdentifier:imageIdentifier error:[OCMArg isNil]]); + + imageNode.imageIdentifiers = @[imageIdentifier]; + // Kick off loading. + [imageNode reloadImageIdentifierSources]; + + // Wait until the image is loaded. + [self expectationForPredicate:[NSPredicate predicateWithFormat:@"loadedImageIdentifier = %@", imageIdentifier] evaluatedWithObject:imageNode handler:nil]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testThatSettingAnImageExternallyWillThrow +{ + XCTAssertThrows(imageNode.image = [UIImage imageNamed:@""]); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASMutableAttributedStringBuilderTests.mm b/submodules/AsyncDisplayKit/Tests/ASMutableAttributedStringBuilderTests.mm new file mode 100644 index 0000000000..2415dfc626 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASMutableAttributedStringBuilderTests.mm @@ -0,0 +1,78 @@ +// +// ASMutableAttributedStringBuilderTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@interface ASMutableAttributedStringBuilderTests : XCTestCase + +@end + +@implementation ASMutableAttributedStringBuilderTests + +- (NSString *)_string +{ + return @"Normcore PBR hella, viral slow-carb mustache chillwave church-key cornhole messenger bag swag vinyl biodiesel ethnic. Fashion axe messenger bag raw denim street art. Flannel Wes Anderson normcore church-key 8-bit. Master cleanse four loko try-hard Carles stumptown ennui, twee literally wayfarers kitsch tofu PBR. Cliche organic post-ironic Wes Anderson kale chips fashion axe. Narwhal Blue Bottle sustainable, Odd Future Godard sriracha banjo disrupt Marfa irony pug Wes Anderson YOLO yr church-key. Mlkshk Intelligentsia semiotics quinoa, butcher meggings wolf Bushwick keffiyeh ethnic pour-over Pinterest letterpress."; +} + +- (ASMutableAttributedStringBuilder *)_builder +{ + return [[ASMutableAttributedStringBuilder alloc] initWithString:[self _string]]; +} + +- (NSRange)_randomizedRangeForStringBuilder:(ASMutableAttributedStringBuilder *)builder +{ + NSUInteger loc = arc4random() % (builder.length - 1); + NSUInteger len = arc4random() % (builder.length - loc); + len = ((len > 0) ? len : 1); + return NSMakeRange(loc, len); +} + +- (void)testSimpleAttributions +{ + // Add a attributes, and verify that they get set on the correct locations. + for (int i = 0; i < 100; i++) { + ASMutableAttributedStringBuilder *builder = [self _builder]; + NSRange range = [self _randomizedRangeForStringBuilder:builder]; + NSString *keyValue = [NSString stringWithFormat:@"%d", i]; + [builder addAttribute:keyValue value:keyValue range:range]; + NSAttributedString *attrStr = [builder composedAttributedString]; + XCTAssertEqual(builder.length, attrStr.length, @"out string should have same length as builder"); + __block BOOL found = NO; + [attrStr enumerateAttributesInRange:NSMakeRange(0, attrStr.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange r, BOOL *stop) { + if ([attrs[keyValue] isEqualToString:keyValue]) { + XCTAssertTrue(NSEqualRanges(range, r), @"enumerated range %@ should be equal to the set range %@", NSStringFromRange(r), NSStringFromRange(range)); + found = YES; + } + }]; + XCTAssertTrue(found, @"enumeration should have found the attribute we set"); + } +} + +- (void)testSetOverAdd +{ + ASMutableAttributedStringBuilder *builder = [self _builder]; + NSRange addRange = NSMakeRange(0, builder.length); + NSRange setRange = NSMakeRange(0, 1); + [builder addAttribute:@"attr" value:@"val1" range:addRange]; + [builder setAttributes:@{@"attr" : @"val2"} range:setRange]; + NSAttributedString *attrStr = [builder composedAttributedString]; + NSRange setRangeOut; + NSString *setAttr = [attrStr attribute:@"attr" atIndex:0 effectiveRange:&setRangeOut]; + XCTAssertTrue(NSEqualRanges(setRange, setRangeOut), @"The out set range should equal the range we used originally"); + XCTAssertEqualObjects(setAttr, @"val2", @"the set value should be val2"); + + NSRange addRangeOut; + NSString *addAttr = [attrStr attribute:@"attr" atIndex:2 effectiveRange:&addRangeOut]; + XCTAssertTrue(NSEqualRanges(NSMakeRange(1, builder.length - 1), addRangeOut), @"the add range should only cover beyond the set range"); + XCTAssertEqualObjects(addAttr, @"val1", @"the added attribute should be present at index 2"); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASNavigationControllerTests.mm b/submodules/AsyncDisplayKit/Tests/ASNavigationControllerTests.mm new file mode 100644 index 0000000000..d9ed2464c0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASNavigationControllerTests.mm @@ -0,0 +1,52 @@ +// +// ASNavigationControllerTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@interface ASNavigationControllerTests : XCTestCase +@end + +@implementation ASNavigationControllerTests + +- (void)testSetViewControllers { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController, secondController]; + ASNavigationController *navigationController = [ASNavigationController new]; + [navigationController setViewControllers:@[firstController, secondController]]; + XCTAssertEqual(navigationController.topViewController, secondController); + XCTAssertEqual(navigationController.visibleViewController, secondController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +- (void)testPopViewController { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController]; + ASNavigationController *navigationController = [ASNavigationController new]; + [navigationController setViewControllers:@[firstController, secondController]]; + [navigationController popViewControllerAnimated:false]; + XCTAssertEqual(navigationController.topViewController, firstController); + XCTAssertEqual(navigationController.visibleViewController, firstController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +- (void)testPushViewController { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController, secondController]; + ASNavigationController *navigationController = [[ASNavigationController new] initWithRootViewController:firstController]; + [navigationController pushViewController:secondController animated:false]; + XCTAssertEqual(navigationController.topViewController, secondController); + XCTAssertEqual(navigationController.visibleViewController, secondController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASNetworkImageNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASNetworkImageNodeTests.mm new file mode 100644 index 0000000000..f794ba9e7f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASNetworkImageNodeTests.mm @@ -0,0 +1,135 @@ +// +// ASNetworkImageNodeTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@interface ASNetworkImageNodeTests : XCTestCase + +@end + +@interface ASTestImageDownloader : NSObject +@end +@interface ASTestImageCache : NSObject +@end + +@implementation ASNetworkImageNodeTests { + ASNetworkImageNode *node; + id downloader; + id cache; +} + +- (void)setUp +{ + [super setUp]; + cache = [OCMockObject partialMockForObject:[[ASTestImageCache alloc] init]]; + downloader = [OCMockObject partialMockForObject:[[ASTestImageDownloader alloc] init]]; + node = [[ASNetworkImageNode alloc] initWithCache:cache downloader:downloader]; +} + +/// Test is flaky: https://github.com/facebook/AsyncDisplayKit/issues/2898 +- (void)DISABLED_testThatProgressBlockIsSetAndClearedCorrectlyOnVisibility +{ + node.URL = [NSURL URLWithString:@"http://imageA"]; + + // Enter preload range, wait for download start. + [[[downloader expect] andForwardToRealObject] downloadImageWithURL:[OCMArg isNotNil] callbackQueue:OCMOCK_ANY downloadProgress:OCMOCK_ANY completion:OCMOCK_ANY]; + [node enterInterfaceState:ASInterfaceStatePreload]; + [downloader verifyWithDelay:5]; + + // Make the node visible. + [[downloader expect] setProgressImageBlock:[OCMArg isNotNil] callbackQueue:OCMOCK_ANY withDownloadIdentifier:@0]; + [node enterInterfaceState:ASInterfaceStateInHierarchy]; + [downloader verify]; + + // Make the node invisible. + [[downloader expect] setProgressImageBlock:[OCMArg isNil] callbackQueue:OCMOCK_ANY withDownloadIdentifier:@0]; + [node exitInterfaceState:ASInterfaceStateInHierarchy]; + [downloader verify]; +} + +- (void)testThatProgressBlockIsSetAndClearedCorrectlyOnChangeURL +{ + [node layer]; + [node enterInterfaceState:ASInterfaceStateInHierarchy]; + + // Set URL while visible, should set progress block + [[downloader expect] setProgressImageBlock:[OCMArg isNotNil] callbackQueue:OCMOCK_ANY withDownloadIdentifier:@0]; + node.URL = [NSURL URLWithString:@"http://imageA"]; + [downloader verifyWithDelay:5]; + + // Change URL while visible, should clear prior block and set new one + [[downloader expect] setProgressImageBlock:[OCMArg isNil] callbackQueue:OCMOCK_ANY withDownloadIdentifier:@0]; + [[downloader expect] cancelImageDownloadForIdentifier:@0]; + [[downloader expect] setProgressImageBlock:[OCMArg isNotNil] callbackQueue:OCMOCK_ANY withDownloadIdentifier:@1]; + node.URL = [NSURL URLWithString:@"http://imageB"]; + [downloader verifyWithDelay:5]; +} + +- (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadState +{ + UIImage *image = [[UIImage alloc] init]; + ASNetworkImageNode *networkImageNode = [[ASNetworkImageNode alloc] init]; + networkImageNode.image = image; + [networkImageNode enterHierarchyState:ASHierarchyStateRangeManaged]; // Ensures didExitPreloadState is called + XCTAssertEqualObjects(image, networkImageNode.image); + [networkImageNode enterInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.image); + [networkImageNode exitInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.image); + [networkImageNode exitHierarchyState:ASHierarchyStateRangeManaged]; + XCTAssertEqualObjects(image, networkImageNode.image); +} + +- (void)testThatSettingADefaultImageWillStayForEnteringAndExitingPreloadState +{ + UIImage *image = [[UIImage alloc] init]; + ASNetworkImageNode *networkImageNode = [[ASNetworkImageNode alloc] init]; + networkImageNode.defaultImage = image; + [networkImageNode enterHierarchyState:ASHierarchyStateRangeManaged]; // Ensures didExitPreloadState is called + XCTAssertEqualObjects(image, networkImageNode.defaultImage); + [networkImageNode enterInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.defaultImage); + [networkImageNode exitInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.defaultImage); + [networkImageNode exitHierarchyState:ASHierarchyStateRangeManaged]; + XCTAssertEqualObjects(image, networkImageNode.defaultImage); +} + +@end + +@implementation ASTestImageCache + +- (void)cachedImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue completion:(ASImageCacherCompletion)completion +{ + completion(nil); +} + +@end + +@implementation ASTestImageDownloader { + NSInteger _currentDownloadID; +} + +- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier +{ + // nop +} + +- (id)downloadImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion +{ + return @(_currentDownloadID++); +} + +- (void)setProgressImageBlock:(ASImageDownloaderProgressImage)progressBlock callbackQueue:(dispatch_queue_t)callbackQueue withDownloadIdentifier:(id)downloadIdentifier +{ + // nop +} +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASOverlayLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASOverlayLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..a50baa9ef4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASOverlayLayoutSpecSnapshotTests.mm @@ -0,0 +1,39 @@ +// +// ASOverlayLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import + +static const ASSizeRange kSize = {{320, 320}, {320, 320}}; + +@interface ASOverlayLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASOverlayLayoutSpecSnapshotTests + +- (void)testOverlay +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor blueColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor blackColor], {20, 20}); + + ASLayoutSpec *layoutSpec = + [ASOverlayLayoutSpec + overlayLayoutSpecWithChild:backgroundNode + overlay: + [ASCenterLayoutSpec + centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY + sizingOptions:{} + child:foregroundNode]]; + + [self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:@[backgroundNode, foregroundNode] identifier: nil]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASPagerNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASPagerNodeTests.mm new file mode 100644 index 0000000000..416efaae3d --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASPagerNodeTests.mm @@ -0,0 +1,179 @@ +// +// ASPagerNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASPagerNodeTestDataSource : NSObject +@end + +@implementation ASPagerNodeTestDataSource + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + return self; +} + +- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode +{ + return 2; +} + +- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index +{ + return [[ASCellNode alloc] init]; +} + +@end + +@interface ASPagerNodeTestController: UIViewController +@property (nonatomic) ASPagerNodeTestDataSource *testDataSource; +@property (nonatomic) ASPagerNode *pagerNode; +@end + +@implementation ASPagerNodeTestController + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Populate these immediately so that they're not unexpectedly nil during tests. + self.testDataSource = [[ASPagerNodeTestDataSource alloc] init]; + + self.pagerNode = [[ASPagerNode alloc] init]; + self.pagerNode.dataSource = self.testDataSource; + + [self.view addSubnode:self.pagerNode]; + } + return self; +} + +@end + +@interface ASPagerNodeTests : XCTestCase +@property (nonatomic) ASPagerNode *pagerNode; + +@property (nonatomic) ASPagerNodeTestDataSource *testDataSource; +@end + +@implementation ASPagerNodeTests + +- (void)testPagerReturnsIndexOfPages +{ + ASPagerNodeTestController *testController = [self testController]; + + ASCellNode *cellNode = [testController.pagerNode nodeForPageAtIndex:0]; + + XCTAssertEqual([testController.pagerNode indexOfPageWithNode:cellNode], 0); +} + +- (void)testPagerReturnsNotFoundForCellThatDontExistInPager +{ + ASPagerNodeTestController *testController = [self testController]; + + ASCellNode *badNode = [[ASCellNode alloc] init]; + + XCTAssertEqual([testController.pagerNode indexOfPageWithNode:badNode], NSNotFound); +} + +- (void)testScrollPageToIndex +{ + ASPagerNodeTestController *testController = [self testController]; + testController.pagerNode.frame = CGRectMake(0, 0, 500, 500); + [testController.pagerNode scrollToPageAtIndex:1 animated:false]; + + XCTAssertEqual(testController.pagerNode.currentPageIndex, 1); +} + +- (ASPagerNodeTestController *)testController +{ + ASPagerNodeTestController *testController = [[ASPagerNodeTestController alloc] initWithNibName:nil bundle:nil]; + UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + [window makeKeyAndVisible]; + window.rootViewController = testController; + + [testController.pagerNode reloadData]; + [testController.pagerNode setNeedsLayout]; + + return testController; +} + +// Disabled due to flakiness https://github.com/facebook/AsyncDisplayKit/issues/2818 +- (void)DISABLED_testThatRootPagerNodeDoesGetTheRightInsetWhilePoppingBack +{ + UICollectionViewCell *cell = nil; + + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + + ASPagerNodeTestDataSource *dataSource = [[ASPagerNodeTestDataSource alloc] init]; + ASPagerNode *pagerNode = [[ASPagerNode alloc] init]; + pagerNode.dataSource = dataSource; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange constrainedSize){ + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:pagerNode]; + }; + ASViewController *vc = [[ASViewController alloc] initWithNode:node]; + UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc]; + window.rootViewController = nav; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + // Wait until view controller is visible + XCTestExpectation *e = [self expectationWithDescription:@"Transition completed"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [e fulfill]; + }); + [self waitForExpectationsWithTimeout:2 handler:nil]; + + // Test initial values +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + cell = [pagerNode.view cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); + XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); + XCTAssertEqual(pagerNode.contentOffset.y, 0); + XCTAssertEqual(pagerNode.contentInset.top, 0); + + e = [self expectationWithDescription:@"Transition completed"]; + // Push another view controller + UIViewController *vc2 = [[UIViewController alloc] init]; + vc2.view.frame = nav.view.bounds; + vc2.view.backgroundColor = [UIColor blueColor]; + [nav pushViewController:vc2 animated:YES]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.505 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [e fulfill]; + }); + [self waitForExpectationsWithTimeout:2 handler:nil]; + + // Pop view controller + e = [self expectationWithDescription:@"Transition completed"]; + [vc2.navigationController popViewControllerAnimated:YES]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.505 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [e fulfill]; + }); + [self waitForExpectationsWithTimeout:2 handler:nil]; + + // Test values again after popping the view controller +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + cell = [pagerNode.view cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); + XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); + XCTAssertEqual(pagerNode.contentOffset.y, 0); + XCTAssertEqual(pagerNode.contentInset.top, 0); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASPerformanceTestContext.h b/submodules/AsyncDisplayKit/Tests/ASPerformanceTestContext.h new file mode 100644 index 0000000000..49196d451c --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASPerformanceTestContext.h @@ -0,0 +1,44 @@ +// +// ASPerformanceTestContext.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +#define ASXCTAssertRelativePerformanceInRange(test, caseName, min, max) \ + _XCTPrimitiveAssertLessThanOrEqual(self, test.results[caseName].relativePerformance, @#caseName, max, @#max);\ + _XCTPrimitiveAssertGreaterThanOrEqual(self, test.results[caseName].relativePerformance, @#caseName, min, @#min) + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^ASTestPerformanceCaseBlock)(NSUInteger i, dispatch_block_t startMeasuring, dispatch_block_t stopMeasuring); + +@interface ASPerformanceTestResult : NSObject +@property (nonatomic, readonly) NSTimeInterval timePer1000; +@property (nonatomic, readonly) NSString *caseName; + +@property (nonatomic, readonly, getter=isReferenceCase) BOOL referenceCase; +@property (nonatomic, readonly) float relativePerformance; + +@property (nonatomic, readonly) NSMutableDictionary *userInfo; +@end + +@interface ASPerformanceTestContext : NSObject + +/** + * The first case you add here will be considered the reference case. + */ +- (void)addCaseWithName:(NSString *)caseName block:(AS_NOESCAPE ASTestPerformanceCaseBlock)block; + +@property (nonatomic, copy, readonly) NSDictionary *results; + +- (BOOL)areAllUserInfosEqual; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Tests/ASPerformanceTestContext.mm b/submodules/AsyncDisplayKit/Tests/ASPerformanceTestContext.mm new file mode 100644 index 0000000000..15c6787596 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASPerformanceTestContext.mm @@ -0,0 +1,135 @@ +// +// ASPerformanceTestContext.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASPerformanceTestContext.h" + +#import + +#import +#import + +@interface ASPerformanceTestResult () +@property (nonatomic) NSTimeInterval timePer1000; +@property (nonatomic) NSString *caseName; + +@property (nonatomic, getter=isReferenceCase) BOOL referenceCase; +@property (nonatomic) float relativePerformance; +@end + +@implementation ASPerformanceTestResult + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + _userInfo = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSString *)description +{ + NSString *userInfoStr = [_userInfo.description stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; + return [NSString stringWithFormat:@"<%-20s: time-per-1000=%04.2f rel-perf=%04.2f user-info=%@>", _caseName.UTF8String, _timePer1000, _relativePerformance, userInfoStr]; +} + +@end + +@implementation ASPerformanceTestContext { + NSMutableDictionary *_results; + NSInteger _iterationCount; + ASPerformanceTestResult * _Nullable _referenceResult; +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + _iterationCount = 1E4; + _results = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSDictionary *)results +{ + return _results; +} + +- (void)dealloc +{ + /** + * I know this seems wacky but it's a pain to have to put this in every single test method. + */ + NSLog(@"%@", self.description); +} + +- (BOOL)areAllUserInfosEqual +{ + ASDisplayNodeAssert(_results.count >= 2, nil); + NSEnumerator *resultsEnumerator = [_results objectEnumerator]; + NSDictionary *userInfo = [[resultsEnumerator nextObject] userInfo]; + for (ASPerformanceTestResult *otherResult in resultsEnumerator) { + if ([userInfo isEqualToDictionary:otherResult.userInfo] == NO) { + return NO; + } + } + return YES; +} + +- (void)addCaseWithName:(NSString *)caseName block:(AS_NOESCAPE ASTestPerformanceCaseBlock)block +{ + ASDisplayNodeAssert(_results[caseName] == nil, @"Already have a case named %@", caseName); + ASPerformanceTestResult *result = [[ASPerformanceTestResult alloc] init]; + result.caseName = caseName; + result.timePer1000 = [self _testPerformanceForCaseWithBlock:block] / (_iterationCount / 1000); + if (_referenceResult == nil) { + result.referenceCase = YES; + result.relativePerformance = 1.0f; + _referenceResult = result; + } else { + result.relativePerformance = _referenceResult.timePer1000 / result.timePer1000; + } + _results[caseName] = result; +} + +/// Returns total work time +- (CFTimeInterval)_testPerformanceForCaseWithBlock:(AS_NOESCAPE ASTestPerformanceCaseBlock)block +{ + __block CFTimeInterval time = 0; + for (NSInteger i = 0; i < _iterationCount; i++) { + __block CFTimeInterval start = 0; + __block BOOL calledStop = NO; + @autoreleasepool { + block(i, ^{ + ASDisplayNodeAssert(start == 0, @"Called startMeasuring block twice."); + start = CACurrentMediaTime(); + }, ^{ + time += (CACurrentMediaTime() - start); + ASDisplayNodeAssert(calledStop == NO, @"Called stopMeasuring block twice."); + ASDisplayNodeAssert(start != 0, @"Failed to call startMeasuring block"); + calledStop = YES; + }); + } + + ASDisplayNodeAssert(calledStop, @"Failed to call stopMeasuring block."); + } + return time; +} + +- (NSString *)description +{ + NSMutableString *str = [NSMutableString stringWithString:@"Results:\n"]; + for (ASPerformanceTestResult *result in [_results objectEnumerator]) { + [str appendFormat:@"\t%@\n", result]; + } + return str; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASPhotosFrameworkImageRequestTests.mm b/submodules/AsyncDisplayKit/Tests/ASPhotosFrameworkImageRequestTests.mm new file mode 100644 index 0000000000..fe1cb408b4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASPhotosFrameworkImageRequestTests.mm @@ -0,0 +1,65 @@ +// +// ASPhotosFrameworkImageRequestTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#if AS_USE_PHOTOS + +#import +#import + +static NSString *const kTestAssetID = @"testAssetID"; + +@interface ASPhotosFrameworkImageRequestTests : XCTestCase + +@end + +@implementation ASPhotosFrameworkImageRequestTests + +#pragma mark Example Data + ++ (ASPhotosFrameworkImageRequest *)exampleImageRequest +{ + ASPhotosFrameworkImageRequest *req = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:kTestAssetID]; + req.options.networkAccessAllowed = YES; + req.options.normalizedCropRect = CGRectMake(0.2, 0.1, 0.6, 0.8); + req.targetSize = CGSizeMake(1024, 1536); + req.contentMode = PHImageContentModeAspectFill; + req.options.version = PHImageRequestOptionsVersionOriginal; + req.options.resizeMode = PHImageRequestOptionsResizeModeFast; + return req; +} + ++ (NSURL *)urlForExampleImageRequest +{ + NSString *str = [NSString stringWithFormat:@"ph://%@?width=1024&height=1536&version=2&contentmode=1&network=1&resizemode=1&deliverymode=0&crop_x=0.2&crop_y=0.1&crop_w=0.6&crop_h=0.8", kTestAssetID]; + return [NSURL URLWithString:str]; +} + +#pragma mark Test cases + +- (void)testThatConvertingToURLWorks +{ + XCTAssertEqualObjects([self.class exampleImageRequest].url, [self.class urlForExampleImageRequest]); +} + +- (void)testThatParsingFromURLWorks +{ + NSURL *url = [self.class urlForExampleImageRequest]; + XCTAssertEqualObjects([ASPhotosFrameworkImageRequest requestWithURL:url], [self.class exampleImageRequest]); +} + +- (void)testThatCopyingWorks +{ + ASPhotosFrameworkImageRequest *example = [self.class exampleImageRequest]; + ASPhotosFrameworkImageRequest *copy = [[self.class exampleImageRequest] copy]; + XCTAssertEqualObjects(example, copy); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Tests/ASRatioLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASRatioLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..f5bad2723f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASRatioLayoutSpecSnapshotTests.mm @@ -0,0 +1,38 @@ +// +// ASRatioLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import + +static const ASSizeRange kFixedSize = {{0, 0}, {100, 100}}; + +@interface ASRatioLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASRatioLayoutSpecSnapshotTests + +- (void)testRatioLayoutSpecWithRatio:(CGFloat)ratio childSize:(CGSize)childSize identifier:(NSString *)identifier +{ + ASDisplayNode *subnode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], childSize); + + ASLayoutSpec *layoutSpec = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:ratio child:subnode]; + + [self testLayoutSpec:layoutSpec sizeRange:kFixedSize subnodes:@[subnode] identifier:identifier]; +} + +- (void)testRatioLayout +{ + [self testRatioLayoutSpecWithRatio:0.5 childSize:CGSizeMake(100, 100) identifier:@"HalfRatio"]; + [self testRatioLayoutSpecWithRatio:2.0 childSize:CGSizeMake(100, 100) identifier:@"DoubleRatio"]; + [self testRatioLayoutSpecWithRatio:7.0 childSize:CGSizeMake(100, 100) identifier:@"SevenTimesRatio"]; + [self testRatioLayoutSpecWithRatio:10.0 childSize:CGSizeMake(20, 200) identifier:@"TenTimesRatioWithItemTooBig"]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASRecursiveUnfairLockTests.mm b/submodules/AsyncDisplayKit/Tests/ASRecursiveUnfairLockTests.mm new file mode 100644 index 0000000000..3916b319e3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASRecursiveUnfairLockTests.mm @@ -0,0 +1,184 @@ +// +// ASRecursiveUnfairLockTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import +#import +#import + +@interface ASRecursiveUnfairLockTests : ASTestCase + +@end + +@implementation ASRecursiveUnfairLockTests { + ASRecursiveUnfairLock lock; +} + +- (void)setUp +{ + [super setUp]; + lock = AS_RECURSIVE_UNFAIR_LOCK_INIT; +} + +- (void)testTheAtomicIsLockFree +{ + XCTAssertTrue(atomic_is_lock_free(&lock._thread)); +} + +- (void)testRelockingFromSameThread +{ + ASRecursiveUnfairLockLock(&lock); + ASRecursiveUnfairLockLock(&lock); + ASRecursiveUnfairLockUnlock(&lock); + // Now try locking from another thread. + XCTestExpectation *e1 = [self expectationWithDescription:@"Other thread tried lock."]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertFalse(ASRecursiveUnfairLockTryLock(&self->lock)); + [e1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + ASRecursiveUnfairLockUnlock(&lock); + + XCTestExpectation *e2 = [self expectationWithDescription:@"Other thread tried lock again"]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertTrue(ASRecursiveUnfairLockTryLock(&self->lock)); + ASRecursiveUnfairLockUnlock(&self->lock); + [e2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testThatUnlockingWithoutHoldingMakesAssertion +{ +#ifdef NS_BLOCK_ASSERTIONS +#warning Assertions should be on for `testThatUnlockingWithoutHoldingMakesAssertion` + NSLog(@"Passing because assertions are off."); +#else + ASRecursiveUnfairLockLock(&lock); + XCTestExpectation *e1 = [self expectationWithDescription:@"Other thread tried lock."]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertThrows(ASRecursiveUnfairLockUnlock(&lock)); + [e1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + ASRecursiveUnfairLockUnlock(&lock); +#endif +} + +#define CHAOS_TEST_BODY(contested, prefix, infix, postfix) \ +dispatch_group_t g = dispatch_group_create(); \ +for (int i = 0; i < (contested ? 16 : 2); i++) {\ +dispatch_group_enter(g);\ +[NSThread detachNewThreadWithBlock:^{\ + for (int i = 0; i < 20000; i++) {\ + prefix;\ + value += 150;\ + infix;\ + value -= 150;\ + postfix;\ + }\ + dispatch_group_leave(g);\ +}];\ +}\ +dispatch_group_wait(g, DISPATCH_TIME_FOREVER); + +#pragma mark - Correctness Tests + +- (void)testRecursiveUnfairLockContested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, ASRecursiveUnfairLockLock(&lock), {}, ASRecursiveUnfairLockUnlock(&lock)); + }]; + XCTAssertEqual(value, 0); +} + +- (void)testRecursiveUnfairLockUncontested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, ASRecursiveUnfairLockLock(&lock), {}, ASRecursiveUnfairLockUnlock(&lock)); + }]; + XCTAssertEqual(value, 0); +} + +#pragma mark - Lock performance tests + +#if RUN_LOCK_PERF_TESTS +- (void)testNoLockContested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, {}, {}, {}); + }]; + XCTAssertNotEqual(value, 0); +} + +- (void)testPlainUnfairLockContested +{ + __block int value = 0; + __block os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT; + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, os_unfair_lock_lock(&unfairLock), {}, os_unfair_lock_unlock(&unfairLock)); + }]; + XCTAssertEqual(value, 0); +} + +- (void)testRecursiveMutexContested +{ + __block int value = 0; + pthread_mutexattr_t attr; + pthread_mutexattr_init (&attr); + pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); + __block pthread_mutex_t m; + pthread_mutex_init (&m, &attr); + pthread_mutexattr_destroy (&attr); + + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, pthread_mutex_lock(&m), {}, pthread_mutex_unlock(&m)); + }]; + pthread_mutex_destroy(&m); +} + +- (void)testNoLockUncontested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, {}, {}, {}); + }]; + XCTAssertNotEqual(value, 0); +} + +- (void)testPlainUnfairLockUncontested +{ + __block int value = 0; + __block os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT; + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, os_unfair_lock_lock(&unfairLock), {}, os_unfair_lock_unlock(&unfairLock)); + }]; + XCTAssertEqual(value, 0); +} + +- (void)testRecursiveMutexUncontested +{ + __block int value = 0; + pthread_mutexattr_t attr; + pthread_mutexattr_init (&attr); + pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); + __block pthread_mutex_t m; + pthread_mutex_init (&m, &attr); + pthread_mutexattr_destroy (&attr); + + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, pthread_mutex_lock(&m), {}, pthread_mutex_unlock(&m)); + }]; + pthread_mutex_destroy(&m); +} + +#endif // RUN_LOCK_PERF_TESTS +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASRelativeLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASRelativeLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..93887a9ce8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASRelativeLayoutSpecSnapshotTests.mm @@ -0,0 +1,135 @@ +// +// ASRelativeLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import +#import + +static const ASSizeRange kSize = {{100, 120}, {320, 160}}; + +@interface ASRelativeLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASRelativeLayoutSpecSnapshotTests + +#pragma mark - XCTestCase + +- (void)testWithOptions +{ + [self testAllVerticalPositionsForHorizontalPosition:ASRelativeLayoutSpecPositionStart]; + [self testAllVerticalPositionsForHorizontalPosition:ASRelativeLayoutSpecPositionCenter]; + [self testAllVerticalPositionsForHorizontalPosition:ASRelativeLayoutSpecPositionEnd]; + +} + +- (void)testAllVerticalPositionsForHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition +{ + [self testWithHorizontalPosition:horizontalPosition verticalPosition:ASRelativeLayoutSpecPositionStart sizingOptions:{}]; + [self testWithHorizontalPosition:horizontalPosition verticalPosition:ASRelativeLayoutSpecPositionCenter sizingOptions:{}]; + [self testWithHorizontalPosition:horizontalPosition verticalPosition:ASRelativeLayoutSpecPositionEnd sizingOptions:{}]; +} + +- (void)testWithSizingOptions +{ + [self testWithHorizontalPosition:ASRelativeLayoutSpecPositionStart + verticalPosition:ASRelativeLayoutSpecPositionStart + sizingOptions:ASRelativeLayoutSpecSizingOptionDefault]; + [self testWithHorizontalPosition:ASRelativeLayoutSpecPositionStart + verticalPosition:ASRelativeLayoutSpecPositionStart + sizingOptions:ASRelativeLayoutSpecSizingOptionMinimumWidth]; + [self testWithHorizontalPosition:ASRelativeLayoutSpecPositionStart + verticalPosition:ASRelativeLayoutSpecPositionStart + sizingOptions:ASRelativeLayoutSpecSizingOptionMinimumHeight]; + [self testWithHorizontalPosition:ASRelativeLayoutSpecPositionStart + verticalPosition:ASRelativeLayoutSpecPositionStart + sizingOptions:ASRelativeLayoutSpecSizingOptionMinimumSize]; +} + +- (void)testWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition + verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition + sizingOptions:(ASRelativeLayoutSpecSizingOption)sizingOptions +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], CGSizeMake(70, 100)); + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASRelativeLayoutSpec + relativePositionLayoutSpecWithHorizontalPosition:horizontalPosition + verticalPosition:verticalPosition + sizingOption:sizingOptions + child:foregroundNode] + background:backgroundNode]; + + [self testLayoutSpec:layoutSpec + sizeRange:kSize + subnodes:@[backgroundNode, foregroundNode] + identifier:suffixForPositionOptions(horizontalPosition, verticalPosition, sizingOptions)]; +} + +static NSString *suffixForPositionOptions(ASRelativeLayoutSpecPosition horizontalPosition, + ASRelativeLayoutSpecPosition verticalPosition, + ASRelativeLayoutSpecSizingOption sizingOptions) +{ + NSMutableString *suffix = [NSMutableString string]; + + if (horizontalPosition == ASRelativeLayoutSpecPositionCenter) { + [suffix appendString:@"CenterX"]; + } else if (horizontalPosition == ASRelativeLayoutSpecPositionEnd) { + [suffix appendString:@"EndX"]; + } + + if (verticalPosition == ASRelativeLayoutSpecPositionCenter) { + [suffix appendString:@"CenterY"]; + } else if (verticalPosition == ASRelativeLayoutSpecPositionEnd) { + [suffix appendString:@"EndY"]; + } + + if ((sizingOptions & ASRelativeLayoutSpecSizingOptionMinimumWidth) != 0) { + [suffix appendString:@"SizingMinimumWidth"]; + } + + if ((sizingOptions & ASRelativeLayoutSpecSizingOptionMinimumHeight) != 0) { + [suffix appendString:@"SizingMinimumHeight"]; + } + + return suffix; +} + +- (void)testMinimumSizeRangeIsGivenToChildWhenNotPositioning +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor], CGSizeMake(10, 10)); + foregroundNode.style.flexGrow = 1; + + ASLayoutSpec *childSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[foregroundNode]] + background:backgroundNode]; + + ASRelativeLayoutSpec *layoutSpec = + [ASRelativeLayoutSpec + relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionNone + verticalPosition:ASRelativeLayoutSpecPositionNone + sizingOption:{} + child:childSpec]; + + [self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:@[backgroundNode, foregroundNode] identifier:nil]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASRunLoopQueueTests.mm b/submodules/AsyncDisplayKit/Tests/ASRunLoopQueueTests.mm new file mode 100644 index 0000000000..2881e30187 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASRunLoopQueueTests.mm @@ -0,0 +1,201 @@ +// +// ASRunLoopQueueTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" + +#import + +#import "ASDisplayNodeTestsHelper.h" + +static NSTimeInterval const kRunLoopRunTime = 0.001; // Allow the RunLoop to run for one millisecond each time. + +@interface QueueObject : NSObject +@property (nonatomic) BOOL queueObjectProcessed; +@end + +@implementation QueueObject +- (void)prepareForCATransactionCommit +{ + self.queueObjectProcessed = YES; +} +@end + +@interface ASRunLoopQueueTests : ASTestCase + +@end + +@implementation ASRunLoopQueueTests + +#pragma mark enqueue tests + +- (void)testEnqueueNilObjectsToQueue +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; + id object = nil; + [queue enqueue:object]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testEnqueueSameObjectTwiceToDefaultQueue +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block NSUInteger dequeuedCount = 0; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + dequeuedCount++; + } + }]; + [queue enqueue:object]; + [queue enqueue:object]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssert(dequeuedCount == 1); +} + +- (void)testEnqueueSameObjectTwiceToNonExclusiveMembershipQueue +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block NSUInteger dequeuedCount = 0; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + dequeuedCount++; + } + }]; + queue.ensureExclusiveMembership = NO; + [queue enqueue:object]; + [queue enqueue:object]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssert(dequeuedCount == 2); +} + +#pragma mark processQueue tests + +- (void)testDefaultQueueProcessObjectsOneAtATime +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + [NSThread sleepForTimeInterval:kRunLoopRunTime * 2]; // So each element takes more time than the available + }]; + [queue enqueue:[[NSObject alloc] init]]; + [queue enqueue:[[NSObject alloc] init]]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(queue.isEmpty); +} + +- (void)testQueueProcessObjectsInBatchesOfSpecifiedSize +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + [NSThread sleepForTimeInterval:kRunLoopRunTime * 2]; // So each element takes more time than the available + }]; + queue.batchSize = 2; + [queue enqueue:[[NSObject alloc] init]]; + [queue enqueue:[[NSObject alloc] init]]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testQueueOnlySendsIsDrainedForLastObjectInBatch +{ + id objectA = [[NSObject alloc] init]; + id objectB = [[NSObject alloc] init]; + __unsafe_unretained id weakObjectA = objectA; + __unsafe_unretained id weakObjectB = objectB; + __block BOOL isQueueDrainedWhenProcessingA = NO; + __block BOOL isQueueDrainedWhenProcessingB = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObjectA) { + isQueueDrainedWhenProcessingA = isQueueDrained; + } else if (dequeuedItem == weakObjectB) { + isQueueDrainedWhenProcessingB = isQueueDrained; + } + }]; + queue.batchSize = 2; + [queue enqueue:objectA]; + [queue enqueue:objectB]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(isQueueDrainedWhenProcessingA); + XCTAssertTrue(isQueueDrainedWhenProcessingB); +} + +#pragma mark strong/weak tests + +- (void)testStrongQueueRetainsObjects +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block BOOL didProcessObject = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + didProcessObject = YES; + } + }]; + [queue enqueue:object]; + object = nil; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(didProcessObject); +} + +- (void)testWeakQueueDoesNotRetainsObjects +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block BOOL didProcessObject = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:NO handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + didProcessObject = YES; + } + }]; + [queue enqueue:object]; + object = nil; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(didProcessObject); +} + +- (void)testWeakQueueWithAllDeallocatedObjectsIsDrained +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:NO handler:nil]; + id object = [[NSObject alloc] init]; + [queue enqueue:object]; + object = nil; + XCTAssertFalse(queue.isEmpty); + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testASCATransactionQueueDisable +{ + // Disable coalescing. + ASConfiguration *config = [[ASConfiguration alloc] init]; + config.experimentalFeatures = kNilOptions; + [ASConfigurationManager test_resetWithConfiguration:config]; + + ASCATransactionQueue *queue = [[ASCATransactionQueue alloc] init]; + QueueObject *object = [[QueueObject alloc] init]; + XCTAssertFalse(object.queueObjectProcessed); + [queue enqueue:object]; + XCTAssertTrue(object.queueObjectProcessed); + XCTAssertTrue([queue isEmpty]); + XCTAssertFalse(queue.enabled); +} + +- (void)testASCATransactionQueueProcess +{ + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; + [ASConfigurationManager test_resetWithConfiguration:config]; + + ASCATransactionQueue *queue = [[ASCATransactionQueue alloc] init]; + QueueObject *object = [[QueueObject alloc] init]; + [queue enqueue:object]; + XCTAssertFalse(object.queueObjectProcessed); + ASCATransactionQueueWait(queue); + XCTAssertTrue(object.queueObjectProcessed); + XCTAssertTrue(queue.enabled); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASScrollNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASScrollNodeTests.mm new file mode 100644 index 0000000000..fe2c4540c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASScrollNodeTests.mm @@ -0,0 +1,168 @@ +// +// ASScrollNodeTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +#import "ASXCTExtensions.h" + +@interface ASScrollNodeTests : XCTestCase + +@property (nonatomic) ASScrollNode *scrollNode; +@property (nonatomic) ASDisplayNode *subnode; + +@end + +@implementation ASScrollNodeTests + +- (void)setUp +{ + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + self.subnode = subnode; + + self.scrollNode = [[ASScrollNode alloc] init]; + self.scrollNode.scrollableDirections = ASScrollDirectionVerticalDirections; + self.scrollNode.automaticallyManagesContentSize = YES; + self.scrollNode.automaticallyManagesSubnodes = YES; + self.scrollNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [[ASWrapperLayoutSpec alloc] initWithLayoutElement:subnode]; + }; + [self.scrollNode view]; +} + +- (void)testSubnodeLayoutCalculatedWithUnconstrainedMaxSizeInScrollableDirection +{ + CGSize parentSize = CGSizeMake(100, 100); + ASSizeRange sizeRange = ASSizeRangeMake(parentSize); + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + + ASSizeRange subnodeSizeRange = sizeRange; + subnodeSizeRange.max.height = CGFLOAT_MAX; + XCTAssertEqual(self.scrollNode.scrollableDirections, ASScrollDirectionVerticalDirections); + ASXCTAssertEqualSizeRanges(self.subnode.constrainedSizeForCalculatedLayout, subnodeSizeRange); + + // Same test for horizontal scrollable directions + self.scrollNode.scrollableDirections = ASScrollDirectionHorizontalDirections; + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + + subnodeSizeRange = sizeRange; + subnodeSizeRange.max.width = CGFLOAT_MAX; + + ASXCTAssertEqualSizeRanges(self.subnode.constrainedSizeForCalculatedLayout, subnodeSizeRange); +} + +- (void)testAutomaticallyManagesContentSizeUnderflow +{ + CGSize subnodeSize = CGSizeMake(100, 100); + CGSize parentSize = CGSizeMake(100, 200); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, parentSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeOverflow +{ + CGSize subnodeSize = CGSizeMake(100, 500); + CGSize parentSize = CGSizeMake(100, 200); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, parentSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithSizeRangeSmallerThanParentSize +{ + CGSize subnodeSize = CGSizeMake(100, 100); + CGSize parentSize = CGSizeMake(100, 500); + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(100, 100), CGSizeMake(100, 200)); + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, sizeRange.max); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithSizeRangeBiggerThanParentSize +{ + CGSize subnodeSize = CGSizeMake(100, 200); + CGSize parentSize = CGSizeMake(100, 100); + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(100, 150)); + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, sizeRange.min); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithInvalidCalculatedSizeForLayout +{ + CGSize subnodeSize = CGSizeMake(100, 200); + CGSize parentSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, subnodeSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testASScrollNodeAccessibility { + ASDisplayNode *scrollNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.isAccessibilityContainer = YES; + node.accessibilityLabel = @"node"; + [scrollNode addSubnode:node]; + node.frame = CGRectMake(0,0,100,100); + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + text.attributedText = [[NSAttributedString alloc] initWithString:@"text"]; + [node addSubnode:text]; + + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"text2"]; + [node addSubnode:text2]; + __unused UIView *view = scrollNode.view; + XCTAssertTrue(node.view.accessibilityElements.firstObject, @"node"); + + // Following tests will only pass when accessibility is enabled. + // More details: https://github.com/TextureGroup/Texture/pull/1188 + + // A bunch of a11y containers each of which hold aggregated labels. + /* NSArray *a11yElements = [scrollNode.view accessibilityElements]; + XCTAssertTrue(a11yElements.count > 0, @"accessibilityElements should exist"); + + UIAccessibilityElement *container = a11yElements.firstObject; + XCTAssertTrue(container.isAccessibilityElement == false && container.accessibilityElements.count > 0); + UIAccessibilityElement *ae = container.accessibilityElements.firstObject; + XCTAssertTrue([[ae accessibilityLabel] isEqualToString:@"node, text, text2"]); + */ +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASSnapshotTestCase.h b/submodules/AsyncDisplayKit/Tests/ASSnapshotTestCase.h new file mode 100644 index 0000000000..195e4b7918 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASSnapshotTestCase.h @@ -0,0 +1,43 @@ +// +// ASSnapshotTestCase.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" +#import +#pragma clang diagnostic pop + +#import "ASDisplayNodeTestsHelper.h" + +@class ASDisplayNode; + +NSOrderedSet *ASSnapshotTestCaseDefaultSuffixes(void); + +#define ASSnapshotVerifyNode(node__, identifier__) \ +{ \ + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:node__]; \ + FBSnapshotVerifyLayerWithOptions(node__.layer, identifier__, ASSnapshotTestCaseDefaultSuffixes(), 0) \ +} + +#define ASSnapshotVerifyLayer(layer__, identifier__) \ + FBSnapshotVerifyLayerWithOptions(layer__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), 0); + +#define ASSnapshotVerifyView(view__, identifier__) \ + FBSnapshotVerifyViewWithOptions(view__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), 0); + +#define ASSnapshotVerifyViewWithTolerance(view__, identifier__, tolerance__) \ + FBSnapshotVerifyViewWithOptions(view__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), tolerance__); + +@interface ASSnapshotTestCase : FBSnapshotTestCase + +/** + * Hack for testing. ASDisplayNode lacks an explicit -render method, so we manually hit its layout & display codepaths. + */ ++ (void)hackilySynchronouslyRecursivelyRenderNode:(ASDisplayNode *)node; + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASSnapshotTestCase.mm b/submodules/AsyncDisplayKit/Tests/ASSnapshotTestCase.mm new file mode 100644 index 0000000000..560c25988e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASSnapshotTestCase.mm @@ -0,0 +1,40 @@ +// +// ASSnapshotTestCase.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASSnapshotTestCase.h" +#import +#import +#import +#import + +NSOrderedSet *ASSnapshotTestCaseDefaultSuffixes(void) +{ + NSMutableOrderedSet *suffixesSet = [[NSMutableOrderedSet alloc] init]; + // In some rare cases, slightly different rendering may occur on iOS 10 (text rasterization). + // If the test folders find any image that exactly matches, they pass; + // if an image is not present at all, or it fails, it moves on to check the others. + // This means the order doesn't matter besides reducing logging / performance. + if (AS_AT_LEAST_IOS10) { + [suffixesSet addObject:@"_iOS_10"]; + } + [suffixesSet addObject:@"_64"]; + return [suffixesSet copy]; +} + +@implementation ASSnapshotTestCase + ++ (void)hackilySynchronouslyRecursivelyRenderNode:(ASDisplayNode *)node +{ + ASDisplayNodePerformBlockOnEveryNode(nil, node, YES, ^(ASDisplayNode * _Nonnull node) { + [node.layer setNeedsDisplay]; + }); + [node recursivelyEnsureDisplaySynchronously:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASStackLayoutSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASStackLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..ac6a2a254d --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASStackLayoutSpecSnapshotTests.mm @@ -0,0 +1,1387 @@ +// +// ASStackLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import +#import +#import +#import +#import +#import + +@interface ASStackLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASStackLayoutSpecSnapshotTests + +#pragma mark - Utility methods + +static NSArray *defaultSubnodes() +{ + return defaultSubnodesWithSameSize(CGSizeZero, 0); +} + +static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, CGFloat flex) +{ + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize) + ]; + for (ASDisplayNode *subnode in subnodes) { + subnode.style.flexGrow = flex; + subnode.style.flexShrink = flex; + } + return subnodes; +} + +static void setCGSizeToNode(CGSize size, ASDisplayNode *node) +{ + node.style.width = ASDimensionMakeWithPoints(size.width); + node.style.height = ASDimensionMakeWithPoints(size.height); +} + +static NSArray *defaultTextNodes() +{ + ASTextNode *textNode1 = [[ASTextNode alloc] init]; + textNode1.attributedText = [[NSAttributedString alloc] initWithString:@"Hello" + attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:20]}]; + textNode1.backgroundColor = [UIColor redColor]; + textNode1.layerBacked = YES; + + ASTextNode *textNode2 = [[ASTextNode alloc] init]; + textNode2.attributedText = [[NSAttributedString alloc] initWithString:@"Why, hello there! How are you?" + attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:12]}]; + textNode2.backgroundColor = [UIColor blueColor]; + textNode2.layerBacked = YES; + + return @[textNode1, textNode2]; +} + +- (void)testStackLayoutSpecWithJustify:(ASStackLayoutJustifyContent)justify + flexFactor:(CGFloat)flex + sizeRange:(ASSizeRange)sizeRange + identifier:(NSString *)identifier +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = justify + }; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, flex); + + [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier +{ + [self testStackLayoutSpecWithStyle:style children:subnodes sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style + children:(NSArray *)children + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier +{ + ASStackLayoutSpec *stackLayoutSpec = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:style.direction + spacing:style.spacing + justifyContent:style.justifyContent + alignItems:style.alignItems + flexWrap:style.flexWrap + alignContent:style.alignContent + lineSpacing:style.lineSpacing + children:children]; + + [self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + itemsHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment + itemsVerticalAlignment:(ASVerticalAlignment)verticalAlignment + identifier:(NSString *)identifier +{ + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; + stackLayoutSpec.direction = direction; + stackLayoutSpec.children = subnodes; + stackLayoutSpec.horizontalAlignment = horizontalAlignment; + stackLayoutSpec.verticalAlignment = verticalAlignment; + + CGSize exactSize = CGSizeMake(200, 200); + static ASSizeRange kSize = ASSizeRangeMake(exactSize, exactSize); + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpecWithBaselineAlignment:(ASStackLayoutAlignItems)baselineAlignment + identifier:(NSString *)identifier +{ + NSAssert(baselineAlignment == ASStackLayoutAlignItemsBaselineFirst || baselineAlignment == ASStackLayoutAlignItemsBaselineLast, @"Unexpected baseline alignment"); + NSArray *textNodes = defaultTextNodes(); + textNodes[1].style.flexShrink = 1.0; + + ASStackLayoutSpec *stackLayoutSpec = [ASStackLayoutSpec horizontalStackLayoutSpec]; + stackLayoutSpec.children = textNodes; + stackLayoutSpec.alignItems = baselineAlignment; + + static ASSizeRange kSize = ASSizeRangeMake(CGSizeMake(150, 0), CGSizeMake(150, CGFLOAT_MAX)); + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:textNodes identifier:identifier]; +} + +- (void)testStackLayoutSpec:(ASStackLayoutSpec *)stackLayoutSpec + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); + ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:stackLayoutSpec background:backgroundNode]; + + NSMutableArray *newSubnodes = [NSMutableArray arrayWithObject:backgroundNode]; + [newSubnodes addObjectsFromArray:subnodes]; + + [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier]; +} + +- (void)testStackLayoutSpecWithAlignContent:(ASStackLayoutAlignContent)alignContent + lineSpacing:(CGFloat)lineSpacing + sizeRange:(ASSizeRange)sizeRange + identifier:(NSString *)identifier +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = alignContent, + .lineSpacing = lineSpacing, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + ]; + + [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpecWithAlignContent:(ASStackLayoutAlignContent)alignContent + sizeRange:(ASSizeRange)sizeRange + identifier:(NSString *)identifier +{ + [self testStackLayoutSpecWithAlignContent:alignContent lineSpacing:0.0 sizeRange:sizeRange identifier:identifier]; +} + +#pragma mark - + +- (void)testDefaultStackLayoutElementFlexProperties +{ + ASDisplayNode *displayNode = [[ASDisplayNode alloc] init]; + + XCTAssertEqual(displayNode.style.flexShrink, NO); + XCTAssertEqual(displayNode.style.flexGrow, NO); + + const ASDimension unconstrainedDimension = ASDimensionAuto; + const ASDimension flexBasis = displayNode.style.flexBasis; + XCTAssertEqual(flexBasis.unit, unconstrainedDimension.unit); + XCTAssertEqual(flexBasis.value, unconstrainedDimension.value); +} + +- (void)testUnderflowBehaviors +{ + // width 300px; height 0-300px + static ASSizeRange kSize = {{300, 0}, {300, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:0 sizeRange:kSize identifier:@"justifyStart"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flexFactor:0 sizeRange:kSize identifier:@"justifyCenter"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flexFactor:0 sizeRange:kSize identifier:@"justifyEnd"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:1 sizeRange:kSize identifier:@"flex"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:@"justifySpaceBetween"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:@"justifySpaceAround"]; +} + +- (void)testOverflowBehaviors +{ + // width 110px; height 0-300px + static ASSizeRange kSize = {{110, 0}, {110, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:0 sizeRange:kSize identifier:@"justifyStart"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flexFactor:0 sizeRange:kSize identifier:@"justifyCenter"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flexFactor:0 sizeRange:kSize identifier:@"justifyEnd"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:1 sizeRange:kSize identifier:@"flex"]; + // On overflow, "space between" is identical to "content start" + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:@"justifyStart"]; + // On overflow, "space around" is identical to "content center" + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:@"justifyCenter"]; +} + +- (void)testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + subnodes[1].style.flexShrink = 1; + + // Width is 75px--that's less than the sum of the widths of the children, which is 100px. + static ASSizeRange kSize = {{75, 0}, {75, 150}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testFlexWithUnequalIntrinsicSizes +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 1); + setCGSizeToNode({150, 150}, subnodes[1]); + + // width 300px; height 0-150px. + static ASSizeRange kUnderflowSize = {{300, 0}, {300, 150}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kUnderflowSize subnodes:subnodes identifier:@"underflow"]; + + // width 200px; height 0-150px. + static ASSizeRange kOverflowSize = {{200, 0}, {200, 150}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kOverflowSize subnodes:subnodes identifier:@"overflow"]; +} + +- (void)testCrossAxisSizeBehaviors +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 50}, subnodes[1]); + setCGSizeToNode({150, 50}, subnodes[2]); + + // width 0-300px; height 300px + static ASSizeRange kVariableHeight = {{0, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:subnodes identifier:@"variableHeight"]; + + // width 300px; height 300px + static ASSizeRange kFixedHeight = {{300, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kFixedHeight subnodes:subnodes identifier:@"fixedHeight"]; +} + +- (void)testStackSpacing +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .spacing = 10 + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 50}, subnodes[1]); + setCGSizeToNode({150, 50}, subnodes[2]); + + // width 0-300px; height 300px + static ASSizeRange kVariableHeight = {{0, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:subnodes identifier:@"variableHeight"]; +} + +- (void)testStackSpacingWithChildrenHavingNilObjects +{ + // This should take a zero height since all children have a nil node. If it takes a height > 0, a blue background + // will show up, hence failing the test. + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor blueColor]); + + ASLayoutSpec *layoutSpec = + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:{10, 10, 10, 10} + child: + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:10 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[]] + background:backgroundNode]]; + + // width 300px; height 0-300px + static ASSizeRange kVariableHeight = {{300, 0}, {300, 300}}; + [self testLayoutSpec:layoutSpec sizeRange:kVariableHeight subnodes:@[backgroundNode] identifier:@"variableHeight"]; +} + +- (void)testChildSpacing +{ + // width 0-INF; height 0-INF + static ASSizeRange kAnySize = {{0, 0}, {INFINITY, INFINITY}}; + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[1].style.spacingBefore = 10; + subnodes[2].style.spacingBefore = 20; + [self testStackLayoutSpecWithStyle:style sizeRange:kAnySize subnodes:subnodes identifier:@"spacingBefore"]; + // Reset above spacing values + subnodes[1].style.spacingBefore = 0; + subnodes[2].style.spacingBefore = 0; + + subnodes[1].style.spacingAfter = 10; + subnodes[2].style.spacingAfter = 20; + [self testStackLayoutSpecWithStyle:style sizeRange:kAnySize subnodes:subnodes identifier:@"spacingAfter"]; + // Reset above spacing values + subnodes[1].style.spacingAfter = 0; + subnodes[2].style.spacingAfter = 0; + + style.spacing = 10; + subnodes[1].style.spacingBefore = -10; + subnodes[1].style.spacingAfter = -10; + [self testStackLayoutSpecWithStyle:style sizeRange:kAnySize subnodes:subnodes identifier:@"spacingBalancedOut"]; +} + +- (void)testJustifiedCenterWithChildSpacing +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .justifyContent = ASStackLayoutJustifyContentCenter + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[0].style.spacingBefore = 0; + subnodes[1].style.spacingBefore = 20; + subnodes[2].style.spacingBefore = 30; + + // width 0-300px; height 300px + static ASSizeRange kVariableHeight = {{0, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:subnodes identifier:@"variableHeight"]; +} + +- (void)testJustifiedSpaceBetweenWithOneChild +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = ASStackLayoutJustifyContentSpaceBetween + }; + + ASDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}); + + // width 300px; height 0-INF + static ASSizeRange kVariableHeight = {{300, 0}, {300, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:@[child] identifier:nil]; +} + +- (void)testJustifiedSpaceAroundWithOneChild +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = ASStackLayoutJustifyContentSpaceAround + }; + + ASDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}); + + // width 300px; height 0-INF + static ASSizeRange kVariableHeight = {{300, 0}, {300, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:@[child] identifier:nil]; +} + +- (void)testJustifiedSpaceBetweenWithRemainingSpace +{ + // width 301px; height 0-300px; + static ASSizeRange kSize = {{301, 0}, {301, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:nil]; +} + +- (void)testJustifiedSpaceAroundWithRemainingSpace +{ + // width 305px; height 0-300px; + static ASSizeRange kSize = {{305, 0}, {305, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:nil]; +} + +- (void)testChildThatChangesCrossSizeWhenMainSizeIsFlexed +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + ASDisplayNode *subnode1 = ASDisplayNodeWithBackgroundColor([UIColor blueColor]); + ASDisplayNode *subnode2 = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}); + + ASRatioLayoutSpec *child1 = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.5 child:subnode1]; + child1.style.flexBasis = ASDimensionMakeWithFraction(1); + child1.style.flexGrow = 1; + child1.style.flexShrink = 1; + + static ASSizeRange kFixedWidth = {{150, 0}, {150, INFINITY}}; + [self testStackLayoutSpecWithStyle:style children:@[child1, subnode2] sizeRange:kFixedWidth subnodes:@[subnode1, subnode2] identifier:nil]; +} + +- (void)testAlignCenterWithFlexedMainDimension +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .alignItems = ASStackLayoutAlignItemsCenter + }; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], {100, 100}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 50}) + ]; + subnodes[0].style.flexShrink = 1; + subnodes[1].style.flexShrink = 1; + + static ASSizeRange kFixedWidth = {{150, 0}, {150, 100}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kFixedWidth subnodes:subnodes identifier:nil]; +} + +- (void)testAlignCenterWithIndefiniteCrossDimension +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + ASDisplayNode *subnode1 = ASDisplayNodeWithBackgroundColor([UIColor redColor], {100, 100}); + + ASDisplayNode *subnode2 = ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 50}); + subnode2.style.alignSelf = ASStackLayoutAlignSelfCenter; + + NSArray *subnodes = @[subnode1, subnode2]; + static ASSizeRange kFixedWidth = {{150, 0}, {150, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kFixedWidth subnodes:subnodes identifier:nil]; +} + +- (void)testAlignedStart +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .justifyContent = ASStackLayoutJustifyContentCenter, + .alignItems = ASStackLayoutAlignItemsStart + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[0].style.spacingBefore = 0; + subnodes[1].style.spacingBefore = 20; + subnodes[2].style.spacingBefore = 30; + + static ASSizeRange kExactSize = {{300, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kExactSize subnodes:subnodes identifier:nil]; +} + +- (void)testAlignedEnd +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .justifyContent = ASStackLayoutJustifyContentCenter, + .alignItems = ASStackLayoutAlignItemsEnd + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[0].style.spacingBefore = 0; + subnodes[1].style.spacingBefore = 20; + subnodes[2].style.spacingBefore = 30; + + static ASSizeRange kExactSize = {{300, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kExactSize subnodes:subnodes identifier:nil]; +} + +- (void)testAlignedCenter +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .justifyContent = ASStackLayoutJustifyContentCenter, + .alignItems = ASStackLayoutAlignItemsCenter + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[0].style.spacingBefore = 0; + subnodes[1].style.spacingBefore = 20; + subnodes[2].style.spacingBefore = 30; + + static ASSizeRange kExactSize = {{300, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kExactSize subnodes:subnodes identifier:nil]; +} + +- (void)testAlignedStretchNoChildExceedsMin +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .justifyContent = ASStackLayoutJustifyContentCenter, + .alignItems = ASStackLayoutAlignItemsStretch + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[0].style.spacingBefore = 0; + subnodes[1].style.spacingBefore = 20; + subnodes[2].style.spacingBefore = 30; + + static ASSizeRange kVariableSize = {{200, 200}, {300, 300}}; + // all children should be 200px wide + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableSize subnodes:subnodes identifier:nil]; +} + +- (void)testAlignedStretchOneChildExceedsMin +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionVertical, + .justifyContent = ASStackLayoutJustifyContentCenter, + .alignItems = ASStackLayoutAlignItemsStretch + }; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({100, 70}, subnodes[1]); + setCGSizeToNode({150, 90}, subnodes[2]); + + subnodes[0].style.spacingBefore = 0; + subnodes[1].style.spacingBefore = 20; + subnodes[2].style.spacingBefore = 30; + + static ASSizeRange kVariableSize = {{50, 50}, {300, 300}}; + // all children should be 150px wide + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableSize subnodes:subnodes identifier:nil]; +} + +- (void)testEmptyStack +{ + static ASSizeRange kVariableSize = {{50, 50}, {300, 300}}; + [self testStackLayoutSpecWithStyle:{} sizeRange:kVariableSize subnodes:@[] identifier:nil]; +} + +- (void)testFixedFlexBasisAppliedWhenFlexingItems +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + setCGSizeToNode({150, 150}, subnodes[1]); + + for (ASDisplayNode *subnode in subnodes) { + subnode.style.flexGrow = 1; + subnode.style.flexBasis = ASDimensionMakeWithPoints(10); + } + + // width 300px; height 0-150px. + static ASSizeRange kUnderflowSize = {{300, 0}, {300, 150}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kUnderflowSize subnodes:subnodes identifier:@"underflow"]; + + // width 200px; height 0-150px. + static ASSizeRange kOverflowSize = {{200, 0}, {200, 150}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kOverflowSize subnodes:subnodes identifier:@"overflow"]; +} + +- (void)testFractionalFlexBasisResolvesAgainstParentSize +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + for (ASDisplayNode *subnode in subnodes) { + subnode.style.flexGrow = 1; + } + + // This should override the intrinsic size of 50pts and instead compute to 50% = 100pts. + // The result should be that the red box is twice as wide as the blue and gree boxes after flexing. + subnodes[0].style.flexBasis = ASDimensionMakeWithFraction(0.5); + + static ASSizeRange kSize = {{200, 0}, {200, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testFixedFlexBasisOverridesIntrinsicSizeForNonFlexingChildren +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodes(); + setCGSizeToNode({50, 50}, subnodes[0]); + setCGSizeToNode({150, 150}, subnodes[1]); + setCGSizeToNode({150, 50}, subnodes[2]); + + for (ASDisplayNode *subnode in subnodes) { + subnode.style.flexBasis = ASDimensionMakeWithPoints(20); + } + + static ASSizeRange kSize = {{300, 0}, {300, 150}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testCrossAxisStretchingOccursAfterStackAxisFlexing +{ + // If cross axis stretching occurred *before* flexing, then the blue child would be stretched to 3000 points tall. + // Instead it should be stretched to 300 points tall, matching the red child and not overlapping the green inset. + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor]), // Inset background node + ASDisplayNodeWithBackgroundColor([UIColor blueColor]), // child1 of stack + ASDisplayNodeWithBackgroundColor([UIColor redColor], {500, 500}) // child2 of stack + ]; + + subnodes[1].style.width = ASDimensionMake(10); + + ASDisplayNode *child2 = subnodes[2]; + child2.style.flexGrow = 1; + child2.style.flexShrink = 1; + + // If cross axis stretching occurred *before* flexing, then the blue child would be stretched to 3000 points tall. + // Instead it should be stretched to 300 points tall, matching the red child and not overlapping the green inset. + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec + backgroundLayoutSpecWithChild: + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) + child: + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[subnodes[1], child2]] + ] + background:subnodes[0]]; + + static ASSizeRange kSize = {{300, 0}, {300, INFINITY}}; + [self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedEqually +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + subnodes[0].style.flexGrow = 1; + subnodes[2].style.flexGrow = 1; + + // In this scenario a width of 350 results in a positive violation of 200. + // Due to each flexible subnode specifying a flex grow factor of 1 the violation will be distributed evenly. + static ASSizeRange kSize = {{350, 350}, {350, 350}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedEquallyWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + subnodes[0].style.flexGrow = 0.5; + subnodes[2].style.flexGrow = 0.5; + + // In this scenario a width of 350 results in a positive violation of 200. + // Due to each flexible child component specifying a flex grow factor of 0.5 the violation will be distributed evenly. + static ASSizeRange kSize = {{350, 350}, {350, 350}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedProportionally +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + subnodes[0].style.flexGrow = 1; + subnodes[1].style.flexGrow = 2; + subnodes[2].style.flexGrow = 1; + + // In this scenario a width of 350 results in a positive violation of 200. + // The first and third subnodes specify a flex grow factor of 1 and will flex by 50. + // The second subnode specifies a flex grow factor of 2 and will flex by 100. + static ASSizeRange kSize = {{350, 350}, {350, 350}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedProportionallyWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, 0); + subnodes[0].style.flexGrow = 0.25; + subnodes[1].style.flexGrow = 0.50; + subnodes[2].style.flexGrow = 0.25; + + // In this scenario a width of 350 results in a positive violation of 200. + // The first and third child components specify a flex grow factor of 0.25 and will flex by 50. + // The second child component specifies a flex grow factor of 0.25 and will flex by 100. + static ASSizeRange kSize = {{350, 350}, {350, 350}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedEquallyAmongMixedChildren +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + const CGSize kSubnodeSize = {50, 50}; + NSArray *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0); + subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)]; + + subnodes[0].style.flexShrink = 1; + subnodes[1].style.flexGrow = 1; + subnodes[2].style.flexShrink = 0; + subnodes[3].style.flexGrow = 1; + + // In this scenario a width of 400 results in a positive violation of 200. + // The first and third subnode specify a flex shrink factor of 1 and 0, respectively. They won't flex. + // The second and fourth subnode specify a flex grow factor of 1 and will flex by 100. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedEquallyAmongMixedChildrenWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + const CGSize kSubnodeSize = {50, 50}; + NSArray *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0); + subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)]; + + subnodes[0].style.flexShrink = 1.0; + subnodes[1].style.flexGrow = 0.5; + subnodes[2].style.flexShrink = 0.0; + subnodes[3].style.flexGrow = 0.5; + + // In this scenario a width of 400 results in a positive violation of 200. + // The first and third child components specify a flex shrink factor of 1 and 0, respectively. They won't flex. + // The second and fourth child components specify a flex grow factor of 0.5 and will flex by 100. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedProportionallyAmongMixedChildren +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + const CGSize kSubnodeSize = {50, 50}; + NSArray *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0); + subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)]; + + subnodes[0].style.flexShrink = 1; + subnodes[1].style.flexGrow = 3; + subnodes[2].style.flexShrink = 0; + subnodes[3].style.flexGrow = 1; + + // In this scenario a width of 400 results in a positive violation of 200. + // The first and third subnodes specify a flex shrink factor of 1 and 0, respectively. They won't flex. + // The second child subnode specifies a flex grow factor of 3 and will flex by 150. + // The fourth child subnode specifies a flex grow factor of 1 and will flex by 50. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testPositiveViolationIsDistributedProportionallyAmongMixedChildrenWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + const CGSize kSubnodeSize = {50, 50}; + NSArray *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0); + subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)]; + + subnodes[0].style.flexShrink = 1.0; + subnodes[1].style.flexGrow = 0.75; + subnodes[2].style.flexShrink = 0.0; + subnodes[3].style.flexGrow = 0.25; + + // In this scenario a width of 400 results in a positive violation of 200. + // The first and third child components specify a flex shrink factor of 1 and 0, respectively. They won't flex. + // The second child component specifies a flex grow factor of 0.75 and will flex by 150. + // The fourth child component specifies a flex grow factor of 0.25 and will flex by 50. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testRemainingViolationIsAppliedProperlyToFirstFlexibleChild +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 25}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 0}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 100}) + ]; + + subnodes[0].style.flexGrow = 0; + subnodes[1].style.flexGrow = 1; + subnodes[2].style.flexGrow = 1; + + // In this scenario a width of 300 results in a positive violation of 175. + // The second and third subnodes specify a flex grow factor of 1 and will flex by 88 and 87, respectively. + static ASSizeRange kSize = {{300, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testRemainingViolationIsAppliedProperlyToFirstFlexibleChildWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 25}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 0}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 100}) + ]; + + subnodes[0].style.flexGrow = 0.0; + subnodes[1].style.flexGrow = 0.5; + subnodes[2].style.flexGrow = 0.5; + + // In this scenario a width of 300 results in a positive violation of 175. + // The second and third child components specify a flex grow factor of 0.5 and will flex by 88 and 87, respectively. + static ASSizeRange kSize = {{300, 300}, {300, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSize +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {300, 50}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {100, 50}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {200, 50}) + ]; + + subnodes[0].style.flexShrink = 1; + subnodes[1].style.flexShrink = 0; + subnodes[2].style.flexShrink = 1; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third subnodes specify a flex shrink factor of 1 and will flex by -120 and -80, respectively. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {300, 50}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {100, 50}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {200, 50}) + ]; + + subnodes[0].style.flexShrink = 0.5; + subnodes[1].style.flexShrink = 0.0; + subnodes[2].style.flexShrink = 0.5; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third child components specify a flex shrink factor of 0.5 and will flex by -120 and -80, respectively. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAndFlexFactor +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 300}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 100}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 200}) + ]; + + subnodes[0].style.flexShrink = 2; + subnodes[1].style.flexShrink = 1; + subnodes[2].style.flexShrink = 2; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third subnodes specify a flex shrink factor of 2 and will flex by -109 and -72, respectively. + // The second subnode specifies a flex shrink factor of 1 and will flex by -18. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 300}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 100}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 200}) + ]; + + subnodes[0].style.flexShrink = 0.4; + subnodes[1].style.flexShrink = 0.2; + subnodes[2].style.flexShrink = 0.4; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third child components specify a flex shrink factor of 0.4 and will flex by -109 and -72, respectively. + // The second child component specifies a flex shrink factor of 0.2 and will flex by -18. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenChildren +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + const CGSize kSubnodeSize = {150, 50}; + NSArray *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0); + subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)]; + + subnodes[0].style.flexGrow = 1; + subnodes[1].style.flexShrink = 1; + subnodes[2].style.flexGrow = 0; + subnodes[3].style.flexShrink = 1; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third subnodes specify a flex grow factor of 1 and 0, respectively. They won't flex. + // The second and fourth subnodes specify a flex grow factor of 1 and will flex by -100. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + const CGSize kSubnodeSize = {150, 50}; + NSArray *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0); + subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)]; + + subnodes[0].style.flexGrow = 1.0; + subnodes[1].style.flexShrink = 0.5; + subnodes[2].style.flexGrow = 0.0; + subnodes[3].style.flexShrink = 0.5; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third child components specify a flex grow factor of 1 and 0, respectively. They won't flex. + // The second and fourth child components specify a flex shrink factor of 0.5 and will flex by -100. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildren +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 150}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 100}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 150}), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], {50, 200}) + ]; + + subnodes[0].style.flexGrow = 1; + subnodes[1].style.flexShrink = 1; + subnodes[2].style.flexGrow = 0; + subnodes[3].style.flexShrink = 3; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third subnodes specify a flex grow factor of 1 and 0, respectively. They won't flex. + // The second subnode specifies a flex grow factor of 1 and will flex by -28. + // The fourth subnode specifies a flex grow factor of 3 and will flex by -171. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildrenArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 150}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 100}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 150}), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], {50, 200}) + ]; + + subnodes[0].style.flexGrow = 1.0; + subnodes[1].style.flexShrink = 0.25; + subnodes[2].style.flexGrow = 0.0; + subnodes[3].style.flexShrink = 0.75; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third child components specify a flex grow factor of 1 and 0, respectively. They won't flex. + // The second child component specifies a flex shrink factor of 0.25 and will flex by -28. + // The fourth child component specifies a flex shrink factor of 0.75 and will flex by -171. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZero +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {300, 50}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {100, 50}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {200, 50}) + ]; + + subnodes[0].style.flexShrink = 1; + subnodes[1].style.flexShrink = 2; + subnodes[2].style.flexShrink = 1; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third subnodes specify a flex shrink factor of 1 and will flex by 50. + // The second subnode specifies a flex shrink factor of 2 and will flex by -57. It will have a width of 43. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZeroWithArbitraryFloats +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor greenColor], {300, 50}), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], {100, 50}), + ASDisplayNodeWithBackgroundColor([UIColor redColor], {200, 50}) + ]; + + subnodes[0].style.flexShrink = 0.25; + subnodes[1].style.flexShrink = 0.50; + subnodes[2].style.flexShrink = 0.25; + + // In this scenario a width of 400 results in a negative violation of 200. + // The first and third child components specify a flex shrink factor of 0.25 and will flex by 50. + // The second child specifies a flex shrink factor of 0.50 and will flex by -57. It will have a width of 43. + static ASSizeRange kSize = {{400, 400}, {400, 400}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + + +- (void)testNegativeViolationAndFlexFactorIsNotRespected +{ + ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; + + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], CGSizeZero), + ]; + + subnodes[0].style.flexShrink = 0; + subnodes[1].style.flexShrink = 1; + + // In this scenario a width of 40 results in a negative violation of 10. + // The first child will not flex. + // The second child specifies a flex shrink factor of 1 but it has a zero size, so the factor won't be respected and it will not shrink. + static ASSizeRange kSize = {{40, 50}, {40, 50}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testNestedStackLayoutStretchDoesNotViolateWidth +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; // Default direction is horizontal + stackLayoutSpec.direction = ASStackLayoutDirectionHorizontal; + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsStretch; + stackLayoutSpec.style.width = ASDimensionMake(100); + stackLayoutSpec.style.height = ASDimensionMake(100); + + ASDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}); + stackLayoutSpec.children = @[child]; + + static ASSizeRange kSize = {{0, 0}, {300, INFINITY}}; + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:@[child] identifier:nil]; +} + +- (void)testHorizontalAndVerticalAlignments +{ + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASHorizontalAlignmentLeft itemsVerticalAlignment:ASVerticalAlignmentTop identifier:@"horizontalTopLeft"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASHorizontalAlignmentMiddle itemsVerticalAlignment:ASVerticalAlignmentCenter identifier:@"horizontalCenter"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASHorizontalAlignmentRight itemsVerticalAlignment:ASVerticalAlignmentBottom identifier:@"horizontalBottomRight"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASHorizontalAlignmentLeft itemsVerticalAlignment:ASVerticalAlignmentTop identifier:@"verticalTopLeft"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASHorizontalAlignmentMiddle itemsVerticalAlignment:ASVerticalAlignmentCenter identifier:@"verticalCenter"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASHorizontalAlignmentRight itemsVerticalAlignment:ASVerticalAlignmentBottom identifier:@"verticalBottomRight"]; +} + +- (void)testDirectionChangeAfterSettingHorizontalAndVerticalAlignments +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; // Default direction is horizontal + stackLayoutSpec.horizontalAlignment = ASHorizontalAlignmentRight; + stackLayoutSpec.verticalAlignment = ASVerticalAlignmentCenter; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd); + + stackLayoutSpec.direction = ASStackLayoutDirectionVertical; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter); +} + +- (void)testAlignItemsAndJustifyContentRestrictionsIfHorizontalAndVerticalAlignmentsAreUsed +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; + + // No assertions should be thrown here because alignments are not used + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd; + + // Set alignments and assert that assertions are thrown + stackLayoutSpec.horizontalAlignment = ASHorizontalAlignmentMiddle; + stackLayoutSpec.verticalAlignment = ASVerticalAlignmentCenter; + XCTAssertThrows(stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd); + XCTAssertThrows(stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd); + + // Unset alignments. alignItems and justifyContent should not be changed + stackLayoutSpec.horizontalAlignment = ASHorizontalAlignmentNone; + stackLayoutSpec.verticalAlignment = ASVerticalAlignmentNone; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter); + + // Now that alignments are none, setting alignItems and justifyContent should be allowed again + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd); +} + +#pragma mark - Baseline alignment tests + +- (void)testBaselineAlignment +{ + [self testStackLayoutSpecWithBaselineAlignment:ASStackLayoutAlignItemsBaselineFirst identifier:@"baselineFirst"]; + [self testStackLayoutSpecWithBaselineAlignment:ASStackLayoutAlignItemsBaselineLast identifier:@"baselineLast"]; +} + +- (void)testNestedBaselineAlignments +{ + NSArray *textNodes = defaultTextNodes(); + + ASDisplayNode *stretchedNode = [[ASDisplayNode alloc] init]; + stretchedNode.layerBacked = YES; + stretchedNode.backgroundColor = [UIColor greenColor]; + stretchedNode.style.alignSelf = ASStackLayoutAlignSelfStretch; + stretchedNode.style.height = ASDimensionMake(100); + + ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStack.children = @[stretchedNode, textNodes[1]]; + verticalStack.style.flexShrink = 1.0; + + ASStackLayoutSpec *horizontalStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; + horizontalStack.children = @[textNodes[0], verticalStack]; + horizontalStack.alignItems = ASStackLayoutAlignItemsBaselineLast; + + NSArray *subnodes = @[textNodes[0], textNodes[1], stretchedNode]; + + static ASSizeRange kSize = ASSizeRangeMake(CGSizeMake(150, 0), CGSizeMake(150, CGFLOAT_MAX)); + [self testStackLayoutSpec:horizontalStack sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testBaselineAlignmentWithSpaceBetween +{ + NSArray *textNodes = defaultTextNodes(); + + ASStackLayoutSpec *stackLayoutSpec = [ASStackLayoutSpec horizontalStackLayoutSpec]; + stackLayoutSpec.children = textNodes; + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsBaselineFirst; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentSpaceBetween; + + static ASSizeRange kSize = ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX)); + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:textNodes identifier:nil]; +} + +- (void)testBaselineAlignmentWithStretchedItem +{ + NSArray *textNodes = defaultTextNodes(); + + ASDisplayNode *stretchedNode = [[ASDisplayNode alloc] init]; + stretchedNode.layerBacked = YES; + stretchedNode.backgroundColor = [UIColor greenColor]; + stretchedNode.style.alignSelf = ASStackLayoutAlignSelfStretch; + stretchedNode.style.flexShrink = 1.0; + stretchedNode.style.flexGrow = 1.0; + + NSMutableArray *children = [NSMutableArray arrayWithArray:textNodes]; + [children insertObject:stretchedNode atIndex:1]; + + ASStackLayoutSpec *stackLayoutSpec = [ASStackLayoutSpec horizontalStackLayoutSpec]; + stackLayoutSpec.children = children; + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsBaselineLast; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentSpaceBetween; + + static ASSizeRange kSize = ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX)); + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:children identifier:nil]; +} + +#pragma mark - Flex wrap and item spacings test + +- (void)testFlexWrapWithItemSpacings +{ + ASStackLayoutSpecStyle style = { + .spacing = 50, + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStart, + .lineSpacing = 5, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ]; + + for (ASDisplayNode *subnode in subnodes) { + subnode.style.spacingBefore = 5; + subnode.style.spacingAfter = 5; + } + + // 3 items, each item has a size of {50, 50} + // width is 230px, enough to fit all items without taking all spacings into account + // Test that all spacings are included and therefore the last item is pushed to a second line. + // See: https://github.com/TextureGroup/Texture/pull/472 + static ASSizeRange kSize = {{230, 300}, {230, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testFlexWrapWithItemSpacingsBeingResetOnNewLines +{ + ASStackLayoutSpecStyle style = { + .spacing = 5, + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStart, + .lineSpacing = 5, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + // 1st line + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + // 2nd line + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + // 3rd line + ASDisplayNodeWithBackgroundColor([UIColor brownColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor orangeColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor purpleColor], subnodeSize), + ]; + + for (ASDisplayNode *subnode in subnodes) { + subnode.style.spacingBefore = 5; + subnode.style.spacingAfter = 5; + } + + // 3 lines, each line has 3 items, each item has a size of {50, 50} + // width is 190px, enough to fit 3 items into a line + // Test that interitem spacing is reset on new lines. Otherwise, lines after the 1st line would have only 2 items. + // See: https://github.com/TextureGroup/Texture/pull/472 + static ASSizeRange kSize = {{190, 300}, {190, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +#pragma mark - Content alignment tests + +- (void)testAlignContentUnderflow +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 300}, {110, 300}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:@"alignContentSpaceAround"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch sizeRange:kSize identifier:@"alignContentStretch"]; +} + +- (void)testAlignContentOverflow +{ + // 6 lines, each line has 1 item, each item has a size of {50, 50} + // width is 40px. It's 10px smaller than the width of each item (40px vs 50px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{40, 260}, {40, 260}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:@"alignContentSpaceAround"]; +} + +- (void)testAlignContentWithUnconstrainedCrossSize +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + // height is unconstrained. It causes no cross size violation and the end results are all similar to ASStackLayoutAlignContentStart. + static ASSizeRange kSize = {{110, 0}, {110, CGFLOAT_MAX}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch sizeRange:kSize identifier:nil]; +} + +- (void)testAlignContentStretchAndOtherAlignments +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStretch, + .alignItems = ASStackLayoutAlignItemsStart, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + // 1st line + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + // 2nd line + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + // 3rd line + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + ]; + + subnodes[1].style.alignSelf = ASStackLayoutAlignSelfStart; + subnodes[3].style.alignSelf = ASStackLayoutAlignSelfCenter; + subnodes[5].style.alignSelf = ASStackLayoutAlignSelfEnd; + + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 300}, {110, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +#pragma mark - Line spacing tests + +- (void)testAlignContentAndLineSpacingUnderflow +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // 10px between lines + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 320}, {110, 320}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart lineSpacing:10 sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter lineSpacing:10 sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd lineSpacing:10 sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceAround"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch lineSpacing:10 sizeRange:kSize identifier:@"alignContentStretch"]; +} + +- (void)testAlignContentAndLineSpacingOverflow +{ + // 6 lines, each line has 1 item, each item has a size of {50, 50} + // 10px between lines + // width is 40px. It's 10px smaller than the width of each item (40px vs 50px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{40, 310}, {40, 310}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart lineSpacing:10 sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter lineSpacing:10 sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd lineSpacing:10 sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceAround"]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTLayoutFixture.h b/submodules/AsyncDisplayKit/Tests/ASTLayoutFixture.h new file mode 100644 index 0000000000..23b2024126 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTLayoutFixture.h @@ -0,0 +1,60 @@ +// +// ASTLayoutFixture.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "ASTestCase.h" +#import "ASLayoutTestNode.h" + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTLayoutFixture : NSObject + +/// The correct layout. The root should be unpositioned (same as -calculatedLayout). +@property (nonatomic, nullable) ASLayout *layout; + +/// The layoutSpecBlocks for non-leaf nodes. +@property (nonatomic, readonly) NSMapTable *layoutSpecBlocks; + +@property (nonatomic, readonly) ASLayoutTestNode *rootNode; + +@property (nonatomic, readonly) NSSet *allNodes; + +/// Get the (correct) layout for the specified node. +- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node; + +/// Add this to the list of expected size ranges for the given node. +- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node; + +/// If you have a node that wants a size different than it gets, set it here. +/// For any leaf nodes that you don't call this on, the node will return the correct size +/// based on the fixture's layout. This is useful for triggering multipass stack layout. +- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node; + +/// Get the first expected size range for the node. +- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node; + +/// Enumerate all the size ranges for all the nodes using the provided block. +- (void)withSizeRangesForAllNodesUsingBlock:(void (^)(ASLayoutTestNode *node, ASSizeRange sizeRange))block; + +/// Enumerate all the size ranges for the node. +- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange sizeRange))block; + +/// Configure the nodes for this fixture. Set testSize on leaf nodes, layoutSpecBlock on container nodes. +- (void)apply; + +@end + +@interface ASLayout (TestHelpers) + +@property (nonatomic, readonly) NSArray *allNodes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Tests/ASTLayoutFixture.mm b/submodules/AsyncDisplayKit/Tests/ASTLayoutFixture.mm new file mode 100644 index 0000000000..7b12cd4b59 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTLayoutFixture.mm @@ -0,0 +1,139 @@ +// +// ASTLayoutFixture.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTLayoutFixture.h" + +@interface ASTLayoutFixture () + +/// The size ranges against which nodes are expected to be measured. +@property (nonatomic, readonly) NSMapTable *> *sizeRanges; + +/// The overridden returned sizes for nodes where you want to trigger multipass layout. +@property (nonatomic, readonly) NSMapTable *returnedSizes; + +@end + +@implementation ASTLayoutFixture + +- (instancetype)init +{ + if (self = [super init]) { + _sizeRanges = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + _layoutSpecBlocks = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + _returnedSizes = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + + } + return self; +} + +- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node +{ + auto ranges = [_sizeRanges objectForKey:node]; + if (ranges == nil) { + ranges = [[NSMutableArray alloc] init]; + [_sizeRanges setObject:ranges forKey:node]; + } + [ranges addObject:[NSValue valueWithBytes:&sizeRange objCType:@encode(ASSizeRange)]]; +} + +- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node +{ + [_returnedSizes setObject:[NSValue valueWithCGSize:size] forKey:node]; +} + +- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node +{ + const auto val = [_sizeRanges objectForKey:node].firstObject; + ASSizeRange r; + [val getValue:&r]; + return r; +} + +- (void)withSizeRangesForAllNodesUsingBlock:(void (^)(ASLayoutTestNode * _Nonnull, ASSizeRange))block +{ + for (ASLayoutTestNode *node in self.allNodes) { + [self withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + block(node, sizeRange); + }]; + } +} + +- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange))block +{ + for (NSValue *value in [_sizeRanges objectForKey:node]) { + ASSizeRange r; + [value getValue:&r]; + block(r); + } +} + +- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node +{ + NSMutableArray *allLayouts = [NSMutableArray array]; + [ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts]; + for (ASLayout *layout in allLayouts) { + if (layout.layoutElement == node) { + return layout; + } + } + return nil; +} + +/// A very dumb tree iteration approach. NSEnumerator or something would be way better. ++ (void)collectAllLayoutsFromLayout:(ASLayout *)layout array:(NSMutableArray *)array +{ + [array addObject:layout]; + for (ASLayout *sublayout in layout.sublayouts) { + [self collectAllLayoutsFromLayout:sublayout array:array]; + } +} + +- (ASLayoutTestNode *)rootNode +{ + return (ASLayoutTestNode *)self.layout.layoutElement; +} + +- (NSSet *)allNodes +{ + const auto allLayouts = [NSMutableArray array]; + [ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts]; + return [NSSet setWithArray:[allLayouts valueForKey:@"layoutElement"]]; +} + +- (void)apply +{ + // Update layoutSpecBlock for parent nodes, set automatic subnode management + for (ASDisplayNode *node in _layoutSpecBlocks) { + const auto block = [_layoutSpecBlocks objectForKey:node]; + if (node.layoutSpecBlock != block) { + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = block; + [node setNeedsLayout]; + } + } + + [self setTestSizesOfLeafNodesInLayout:self.layout]; +} + +/// Go through the given layout, and for all the leaf nodes, set their preferredSize +/// to the layout size if needed, then call -setNeedsLayout +- (void)setTestSizesOfLeafNodesInLayout:(ASLayout *)layout +{ + const auto node = (ASLayoutTestNode *)layout.layoutElement; + if (layout.sublayouts.count == 0) { + const auto override = [self.returnedSizes objectForKey:node]; + node.testSize = override ? override.CGSizeValue : layout.size; + } else { + node.testSize = CGSizeZero; + for (ASLayout *sublayout in layout.sublayouts) { + [self setTestSizesOfLeafNodesInLayout:sublayout]; + } + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTabBarControllerTests.mm b/submodules/AsyncDisplayKit/Tests/ASTabBarControllerTests.mm new file mode 100644 index 0000000000..764ab9b545 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTabBarControllerTests.mm @@ -0,0 +1,41 @@ +// +// ASTabBarControllerTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@interface ASTabBarControllerTests: XCTestCase + +@end + +@implementation ASTabBarControllerTests + +- (void)testTabBarControllerSelectIndex { + ASViewController *firstViewController = [ASViewController new]; + ASViewController *secondViewController = [ASViewController new]; + NSArray *viewControllers = @[firstViewController, secondViewController]; + ASTabBarController *tabBarController = [ASTabBarController new]; + [tabBarController setViewControllers:viewControllers]; + [tabBarController setSelectedIndex:1]; + XCTAssertTrue([tabBarController.viewControllers isEqualToArray:viewControllers]); + XCTAssertEqual(tabBarController.selectedViewController, secondViewController); +} + +- (void)testTabBarControllerSelectViewController { + ASViewController *firstViewController = [ASViewController new]; + ASViewController *secondViewController = [ASViewController new]; + NSArray *viewControllers = @[firstViewController, secondViewController]; + ASTabBarController *tabBarController = [ASTabBarController new]; + [tabBarController setViewControllers:viewControllers]; + [tabBarController setSelectedViewController:secondViewController]; + XCTAssertTrue([tabBarController.viewControllers isEqualToArray:viewControllers]); + XCTAssertEqual(tabBarController.selectedViewController, secondViewController); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTableViewTests.mm b/submodules/AsyncDisplayKit/Tests/ASTableViewTests.mm new file mode 100644 index 0000000000..7295b20ba5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTableViewTests.mm @@ -0,0 +1,930 @@ +// +// ASTableViewTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" +#import +#pragma clang diagnostic pop + +#import +#import +#import +#import +#import +#import +#import +#import + +#import "ASXCTExtensions.h" + +#define NumberOfSections 10 +#define NumberOfReloadIterations 50 + +@interface ASTestDataController : ASDataController +@property (nonatomic) int numberOfAllNodesRelayouts; +@end + +@implementation ASTestDataController + +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock +{ + _numberOfAllNodesRelayouts++; + [super relayoutAllNodesWithInvalidationBlock:invalidationBlock]; +} + +@end + +@interface UITableView (Testing) +// This will start recording all editing calls to UITableView +// into the provided array. +// Make sure to call [UITableView deswizzleInstanceMethods] to reset this. ++ (void)as_recordEditingCallsIntoArray:(NSMutableArray *)selectors; +@end + +@interface ASTestTableView : ASTableView +@property (nonatomic) void (^willDeallocBlock)(ASTableView *tableView); +@end + +@implementation ASTestTableView + +- (instancetype)__initWithFrame:(CGRect)frame style:(UITableViewStyle)style +{ + + return [super _initWithFrame:frame style:style dataControllerClass:[ASTestDataController class] owningNode:nil eventLog:nil]; +} + +- (ASTestDataController *)testDataController +{ + return (ASTestDataController *)self.dataController; +} + +- (void)dealloc +{ + if (_willDeallocBlock) { + _willDeallocBlock(self); + } +} + +@end + +@interface ASTableViewTestDelegate : NSObject +@property (nonatomic) void (^willDeallocBlock)(ASTableViewTestDelegate *delegate); +@property (nonatomic) CGFloat headerHeight; +@property (nonatomic) CGFloat footerHeight; +@end + +@implementation ASTableViewTestDelegate + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return 0; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return nil; +} + +- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return nil; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section +{ + return _footerHeight; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return _headerHeight; +} + +- (void)dealloc +{ + if (_willDeallocBlock) { + _willDeallocBlock(self); + } +} + +@end + +@interface ASTestTextCellNode : ASTextCellNode +/** Calculated by counting how many times -layoutSpecThatFits: is called on the main thread. */ +@property (nonatomic) int numberOfLayoutsOnMainThread; +@end + +@implementation ASTestTextCellNode + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + if ([NSThread isMainThread]) { + _numberOfLayoutsOnMainThread++; + } + return [super layoutSpecThatFits:constrainedSize]; +} + +@end + +@interface ASTableViewFilledDataSource : NSObject +@property (nonatomic) BOOL usesSectionIndex; +@property (nonatomic) NSInteger numberOfSections; +@property (nonatomic) NSInteger rowsPerSection; +@property (nonatomic, nullable) ASCellNodeBlock(^nodeBlockForItem)(NSIndexPath *); +@end + +@implementation ASTableViewFilledDataSource + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + _numberOfSections = NumberOfSections; + _rowsPerSection = 20; + } + return self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + if (aSelector == @selector(sectionIndexTitlesForTableView:) || aSelector == @selector(tableView:sectionForSectionIndexTitle:atIndex:)) { + return _usesSectionIndex; + } else { + return [super respondsToSelector:aSelector]; + } +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return _numberOfSections; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return _rowsPerSection; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASTestTextCellNode *textCellNode = [ASTestTextCellNode new]; + textCellNode.text = indexPath.description; + + return textCellNode; +} + +- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (_nodeBlockForItem) { + return _nodeBlockForItem(indexPath); + } + + return ^{ + ASTestTextCellNode *textCellNode = [ASTestTextCellNode new]; + textCellNode.text = [NSString stringWithFormat:@"{%d, %d}", (int)indexPath.section, (int)indexPath.row]; + textCellNode.backgroundColor = [UIColor whiteColor]; + return textCellNode; + }; +} + +- (nullable NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView +{ + return @[ @"A", @"B", @"C" ]; +} + +- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index +{ + return 0; +} + +@end + +@interface ASTableViewFilledDelegate : NSObject +@end + +@implementation ASTableViewFilledDelegate + +- (ASSizeRange)tableView:(ASTableView *)tableView constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return ASSizeRangeMake(CGSizeMake(10, 42)); +} + +@end + +@interface ASTableViewTests : XCTestCase +@property (nonatomic, retain) ASTableView *testTableView; +@end + +@implementation ASTableViewTests + +- (void)testDataSourceImplementsNecessaryMethods +{ + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, 100, 400) + style:UITableViewStylePlain]; + + + + ASTableViewFilledDataSource *dataSource = (ASTableViewFilledDataSource *)[NSObject new]; + XCTAssertThrows((tableView.asyncDataSource = dataSource)); + + dataSource = [ASTableViewFilledDataSource new]; + XCTAssertNoThrow((tableView.asyncDataSource = dataSource)); +} + +- (void)testConstrainedSizeForRowAtIndexPath +{ + // Initial width of the table view is non-zero and all nodes are measured with this size. + // Any subsequent size change must trigger a relayout. + // Width and height are swapped so that a later size change will simulate a rotation + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, 100, 400) + style:UITableViewStylePlain]; + + ASTableViewFilledDelegate *delegate = [ASTableViewFilledDelegate new]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = delegate; + tableView.asyncDataSource = dataSource; + + [tableView reloadData]; + [tableView waitUntilAllUpdatesAreCommitted]; + [tableView setNeedsLayout]; + [tableView layoutIfNeeded]; + + CGFloat separatorHeight = 1.0 / ASScreenScale(); + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + CGRect rect = [tableView rectForRowAtIndexPath:indexPath]; + XCTAssertEqual(rect.size.width, 100); // specified width should be ignored for table + XCTAssertEqual(rect.size.height, 42 + separatorHeight); + } + } +} + +// TODO: Convert this to ARC. +- (void)DISABLED_testTableViewDoesNotRetainItselfAndDelegate +{ + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectZero style:UITableViewStylePlain]; + + __block BOOL tableViewDidDealloc = NO; + tableView.willDeallocBlock = ^(ASTableView *v){ + tableViewDidDealloc = YES; + }; + + ASTableViewTestDelegate *delegate = [[ASTableViewTestDelegate alloc] init]; + + __block BOOL delegateDidDealloc = NO; + delegate.willDeallocBlock = ^(ASTableViewTestDelegate *d){ + delegateDidDealloc = YES; + }; + + tableView.asyncDataSource = delegate; + tableView.asyncDelegate = delegate; + +// [delegate release]; + XCTAssertTrue(delegateDidDealloc, @"unexpected delegate lifetime:%@", delegate); + +// XCTAssertNoThrow([tableView release], @"unexpected exception when deallocating table view:%@", tableView); + XCTAssertTrue(tableViewDidDealloc, @"unexpected table view lifetime:%@", tableView); +} + +- (NSIndexSet *)randomIndexSet +{ + NSInteger randA = arc4random_uniform(NumberOfSections - 1); + NSInteger randB = arc4random_uniform(NumberOfSections - 1); + + return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))]; +} + +- (NSArray *)randomIndexPathsExisting:(BOOL)existing rowCount:(NSInteger)rowCount +{ + NSMutableArray *indexPaths = [NSMutableArray array]; + [[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = (existing ? 0 : rowCount); i < (existing ? rowCount : rowCount * 2); i++) { + // Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows + if (existing && arc4random_uniform(2) == 0) { + continue; + } + + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + } + }]; + return indexPaths; +} + +- (void)DISABLED_testReloadData +{ + // Keep the viewport moderately sized so that new cells are loaded on scrolling + ASTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, 100, 500) + style:UITableViewStylePlain]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; + + [tableView reloadDataWithCompletion:^{ + NSLog(@"*** Reload Complete ***"); + [reloadDataExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; + + for (int i = 0; i < NumberOfReloadIterations; ++i) { + UITableViewRowAnimation rowAnimation = (arc4random_uniform(2) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); + BOOL animatedScroll = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL reloadRowsInsteadOfSections = (arc4random_uniform(2) == 0 ? YES : NO); + NSTimeInterval runLoopDelay = ((arc4random_uniform(2) == 0) ? (1.0 / (1 + arc4random_uniform(500))) : 0); + BOOL useBeginEndUpdates = (arc4random_uniform(3) == 0 ? YES : NO); + + // instrument our instrumentation ;) + //NSLog(@"Iteration %03d: %@|%@|%@|%@|%g", i, (rowAnimation == UITableViewRowAnimationNone) ? @"NONE " : @"MIDDLE", animatedScroll ? @"ASCR" : @" ", reloadRowsInsteadOfSections ? @"ROWS" : @"SECS", useBeginEndUpdates ? @"BEGEND" : @" ", runLoopDelay); + + if (useBeginEndUpdates) { + [tableView beginUpdates]; + } + + if (reloadRowsInsteadOfSections) { + NSArray *indexPaths = [self randomIndexPathsExisting:YES rowCount:dataSource.rowsPerSection]; + //NSLog(@"reloading rows: %@", indexPaths); + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:rowAnimation]; + } else { + NSIndexSet *sections = [self randomIndexSet]; + //NSLog(@"reloading sections: %@", sections); + [tableView reloadSections:sections withRowAnimation:rowAnimation]; + } + + [tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animatedScroll]; + + if (runLoopDelay > 0) { + // Run other stuff on the main queue for between 2ms and 1000ms. + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:runLoopDelay]]; + } + + if (useBeginEndUpdates) { + [tableView endUpdates]; + } + } +} + +- (void)testRelayoutAllNodesWithNonZeroSizeInitially +{ + // Initial width of the table view is non-zero and all nodes are measured with this size. + // Any subsequence size change must trigger a relayout. + CGSize tableViewFinalSize = CGSizeMake(100, 500); + // Width and height are swapped so that a later size change will simulate a rotation + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width) + style:UITableViewStylePlain]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + [tableView layoutIfNeeded]; + + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 0); + [self triggerSizeChangeAndAssertRelayoutAllNodesForTableView:tableView newSize:tableViewFinalSize]; +} + +- (void)testRelayoutVisibleRowsWhenEditingModeIsChanged +{ + CGSize tableViewSize = CGSizeMake(100, 500); + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + // Currently this test requires that the text in the cell node fills the + // visible width, so we use the long description for the index path. + dataSource.nodeBlockForItem = ^(NSIndexPath *indexPath) { + return (ASCellNodeBlock)^{ + ASTestTextCellNode *textCellNode = [[ASTestTextCellNode alloc] init]; + textCellNode.text = indexPath.description; + return textCellNode; + }; + }; + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + [self triggerFirstLayoutMeasurementForTableView:tableView]; + + NSArray *visibleNodes = [tableView visibleNodes]; + XCTAssertGreaterThan(visibleNodes.count, 0); + + // Cause table view to enter editing mode. + // Visibile nodes should be re-measured on main thread with the new (smaller) content view width. + // Other nodes are untouched. + XCTestExpectation *relayoutAfterEnablingEditingExpectation = [self expectationWithDescription:@"relayoutAfterEnablingEditing"]; + [tableView beginUpdates]; + [tableView setEditing:YES]; + [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < dataSource.rowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + if ([visibleNodes containsObject:node]) { + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertLessThan(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } else { + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } + } + } + [relayoutAfterEnablingEditingExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; + + // Cause table view to leave editing mode. + // Visibile nodes should be re-measured again. + // All nodes should have max constrained width equals to the table view width. + XCTestExpectation *relayoutAfterDisablingEditingExpectation = [self expectationWithDescription:@"relayoutAfterDisablingEditing"]; + [tableView beginUpdates]; + [tableView setEditing:NO]; + [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < dataSource.rowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + BOOL visible = [visibleNodes containsObject:node]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, visible ? 2: 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } + } + [relayoutAfterDisablingEditingExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +- (void)DISABLED_testRelayoutRowsAfterEditingModeIsChangedAndTheyBecomeVisible +{ + CGSize tableViewSize = CGSizeMake(100, 500); + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + [self triggerFirstLayoutMeasurementForTableView:tableView]; + + // Cause table view to enter editing mode and then scroll to the bottom. + // The last node should be re-measured on main thread with the new (smaller) content view width. + NSIndexPath *lastRowIndexPath = [NSIndexPath indexPathForRow:(dataSource.rowsPerSection - 1) inSection:(NumberOfSections - 1)]; + XCTestExpectation *relayoutExpectation = [self expectationWithDescription:@"relayout"]; + [tableView beginUpdates]; + [tableView setEditing:YES]; + [tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX) animated:YES]; + [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:lastRowIndexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertLessThan(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + [relayoutExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +- (void)testIndexPathForNode +{ + CGSize tableViewSize = CGSizeMake(100, 500); + ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + [tableView reloadDataWithCompletion:^{ + for (NSUInteger i = 0; i < NumberOfSections; i++) { + for (NSUInteger j = 0; j < dataSource.rowsPerSection; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:j inSection:i]; + ASCellNode *cellNode = [tableView nodeForRowAtIndexPath:indexPath]; + NSIndexPath *reportedIndexPath = [tableView indexPathForNode:cellNode]; + XCTAssertEqual(indexPath.row, reportedIndexPath.row); + } + } + self.testTableView = nil; + }]; +} + +- (void)triggerFirstLayoutMeasurementForTableView:(ASTableView *)tableView{ + XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; + [tableView reloadDataWithCompletion:^{ + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableView.frame.size.width); + } + } + [reloadDataExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; + [tableView setNeedsLayout]; + [tableView layoutIfNeeded]; + [tableView waitUntilAllUpdatesAreCommitted]; +} + +- (void)triggerSizeChangeAndAssertRelayoutAllNodesForTableView:(ASTestTableView *)tableView newSize:(CGSize)newSize +{ + XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSize"]; + + [tableView beginUpdates]; + + CGRect frame = tableView.frame; + frame.size = newSize; + tableView.frame = frame; + [tableView layoutIfNeeded]; + + [tableView endUpdatesAnimated:NO completion:^(BOOL completed) { + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 1); + + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertLessThanOrEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, newSize.width); + } + } + [nodesMeasuredUsingNewConstrainedSizeExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +/** + * This may seem silly, but we had issues where the runtime sometimes wouldn't correctly report + * conformances declared on categories. + */ +- (void)testThatTableNodeConformsToExpectedProtocols +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + XCTAssert([node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]); +} + +- (void)testThatInitialDataLoadHappensInOneShot +{ + NSMutableArray *selectors = [NSMutableArray array]; + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + node.frame = CGRectMake(0, 0, 100, 100); + + node.dataSource = dataSource; + node.delegate = dataSource; + + [UITableView as_recordEditingCallsIntoArray:selectors]; + XCTAssertGreaterThan(node.numberOfSections, 0); + [node waitUntilAllUpdatesAreProcessed]; + XCTAssertGreaterThan(node.view.numberOfSections, 0); + + // The first reloadData call helps prevent UITableView from calling it multiple times while ASDataController is working. + // The second reloadData call is the real one. + NSArray *expectedSelectors = @[ NSStringFromSelector(@selector(reloadData)), + NSStringFromSelector(@selector(reloadData)) ]; + XCTAssertEqualObjects(selectors, expectedSelectors); + + [UITableView deswizzleAllInstanceMethods]; +} + +- (void)testThatReloadDataHappensInOneShot +{ + NSMutableArray *selectors = [NSMutableArray array]; + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + node.frame = CGRectMake(0, 0, 100, 100); + + node.dataSource = dataSource; + node.delegate = dataSource; + + // Load initial data. + XCTAssertGreaterThan(node.numberOfSections, 0); + [node waitUntilAllUpdatesAreProcessed]; + XCTAssertGreaterThan(node.view.numberOfSections, 0); + + // Reload data. + [UITableView as_recordEditingCallsIntoArray:selectors]; + [node reloadData]; + [node waitUntilAllUpdatesAreProcessed]; + + // Assert that the beginning of the call pattern is correct. + // There is currently noise that comes after that we will allow for this test. + NSArray *expectedSelectors = @[ NSStringFromSelector(@selector(reloadData)) ]; + XCTAssertEqualObjects(selectors, expectedSelectors); + + [UITableView deswizzleAllInstanceMethods]; +} + +/** + * This tests an issue where, if the table is loaded before the first layout pass, + * the nodes are first measured with a constrained width of 0 which isn't ideal. + */ +- (void)testThatNodeConstrainedSizesAreCorrectIfReloadIsPreempted +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + CGFloat cellWidth = 320; + node.frame = CGRectMake(0, 0, cellWidth, 480); + + node.dataSource = dataSource; + node.delegate = dataSource; + + // Trigger data load BEFORE first layout pass, to ensure constrained size is correct. + XCTAssertGreaterThan(node.numberOfSections, 0); + [node waitUntilAllUpdatesAreProcessed]; + + ASSizeRange expectedSizeRange = ASSizeRangeMake(CGSizeMake(cellWidth, 0)); + expectedSizeRange.max.height = CGFLOAT_MAX; + + for (NSInteger i = 0; i < node.numberOfSections; i++) { + for (NSInteger j = 0; j < [node numberOfRowsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASTestTextCellNode *cellNode = (id)[node nodeForRowAtIndexPath:indexPath]; + ASXCTAssertEqualSizeRanges(cellNode.constrainedSizeForCalculatedLayout, expectedSizeRange); + XCTAssertEqual(cellNode.numberOfLayoutsOnMainThread, 0); + } + } +} + +- (void)testSectionIndexHandling +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + dataSource.usesSectionIndex = YES; + node.frame = CGRectMake(0, 0, 320, 480); + + node.dataSource = dataSource; + node.delegate = dataSource; + + // Trigger data load + XCTAssertGreaterThan(node.numberOfSections, 0); + XCTAssertGreaterThan([node numberOfRowsInSection:0], 0); + + // UITableView's section index view is added only after some rows were inserted to the table. + // All nodes loaded and measured during the initial reloadData used an outdated constrained width (i.e full width: 320). + // So we need to force a new layout pass so that the table will pick up a new constrained size and apply to its node. + [node setNeedsLayout]; + [node.view layoutIfNeeded]; + [node waitUntilAllUpdatesAreProcessed]; + + UITableViewCell *cell = [node.view cellForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + XCTAssertNotNil(cell); + + CGFloat cellWidth = cell.contentView.frame.size.width; + XCTAssert(cellWidth > 0 && cellWidth < 320, @"Expected cell width to be about 305. Width: %@", @(cellWidth)); + + ASSizeRange expectedSizeRange = ASSizeRangeMake(CGSizeMake(cellWidth, 0)); + expectedSizeRange.max.height = CGFLOAT_MAX; + + for (NSInteger i = 0; i < node.numberOfSections; i++) { + for (NSInteger j = 0; j < [node numberOfRowsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASTestTextCellNode *cellNode = (id)[node nodeForRowAtIndexPath:indexPath]; + ASXCTAssertEqualSizeRanges(cellNode.constrainedSizeForCalculatedLayout, expectedSizeRange); + // We will have to accept a relayout on main thread, since the index bar won't show + // up until some of the cells are inserted. + XCTAssertLessThanOrEqual(cellNode.numberOfLayoutsOnMainThread, 1); + } + } +} + +- (void)testThatNilBatchUpdatesCanBeSubmitted +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + + // Passing nil blocks should not crash + [node performBatchUpdates:nil completion:nil]; + [node performBatchAnimated:NO updates:nil completion:nil]; +} + +// https://github.com/facebook/AsyncDisplayKit/issues/2252#issuecomment-263689979 +- (void)testIssue2252 +{ + // Hard-code an iPhone 7 screen. There's something particular about this geometry that causes the issue to repro. + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 375, 667)]; + + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + node.frame = window.bounds; + ASTableViewTestDelegate *del = [[ASTableViewTestDelegate alloc] init]; + del.headerHeight = 32; + del.footerHeight = 0.01; + node.delegate = del; + ASTableViewFilledDataSource *ds = [[ASTableViewFilledDataSource alloc] init]; + ds.rowsPerSection = 1; + node.dataSource = ds; + ASViewController *vc = [[ASViewController alloc] initWithNode:node]; + UITabBarController *tabCtrl = [[UITabBarController alloc] init]; + tabCtrl.viewControllers = @[ vc ]; + tabCtrl.tabBar.translucent = NO; + window.rootViewController = tabCtrl; + [window makeKeyAndVisible]; + + [window layoutIfNeeded]; + [node waitUntilAllUpdatesAreProcessed]; + XCTAssertEqual(node.view.numberOfSections, NumberOfSections); + ASXCTAssertEqualRects(CGRectMake(0, 32, 375, 44), [node rectForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]], @"This text requires very specific geometry. The rect for the first row should match up."); + + __unused XCTestExpectation *e = [self expectationWithDescription:@"Did a bunch of rounds of updates."]; + NSInteger totalCount = 20; + __block NSInteger count = 0; + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC, 0.01 * NSEC_PER_SEC); + dispatch_source_set_event_handler(timer, ^{ + [node performBatchUpdates:^{ + [node reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, NumberOfSections)] withRowAnimation:UITableViewRowAnimationNone]; + } completion:^(BOOL finished) { + if (++count == totalCount) { + dispatch_cancel(timer); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [e fulfill]; + }); + } + }]; + }); + dispatch_resume(timer); + [self waitForExpectationsWithTimeout:60 handler:nil]; +} + +- (void)testThatInvalidUpdateExceptionReasonContainsDataSourceClassName +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + node.bounds = CGRectMake(0, 0, 100, 100); + ASTableViewFilledDataSource *ds = [[ASTableViewFilledDataSource alloc] init]; + node.dataSource = ds; + + // Force node to load initial data. + [node.view layoutIfNeeded]; + + // Submit an invalid update, ensure exception name matches and that data source is included in the reason. + @try { + [node deleteSections:[NSIndexSet indexSetWithIndex:1000] withRowAnimation:UITableViewRowAnimationNone]; + XCTFail(@"Expected validation to fail."); + } @catch (NSException *e) { + XCTAssertEqual(e.name, ASCollectionInvalidUpdateException); + XCTAssert([e.reason containsString:NSStringFromClass([ds class])], @"Expected validation reason to contain the data source class name. Got:\n%@", e.reason); + } +} + +- (void)testAutomaticallyAdjustingContentOffset +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + node.automaticallyAdjustsContentOffset = YES; + node.bounds = CGRectMake(0, 0, 100, 100); + ASTableViewFilledDataSource *ds = [[ASTableViewFilledDataSource alloc] init]; + node.dataSource = ds; + + [node.view layoutIfNeeded]; + [node waitUntilAllUpdatesAreProcessed]; + CGFloat rowHeight = [node.view rectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]].size.height; + // Scroll to row (0,1) + 10pt + node.contentOffset = CGPointMake(0, rowHeight + 10); + + [node performBatchAnimated:NO updates:^{ + // Delete row 0 from all sections. + // This is silly but it's a consequence of how ASTableViewFilledDataSource is built. + ds.rowsPerSection -= 1; + for (NSInteger i = 0; i < NumberOfSections; i++) { + [node deleteRowsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:i]] withRowAnimation:UITableViewRowAnimationAutomatic]; + } + } completion:nil]; + [node waitUntilAllUpdatesAreProcessed]; + + // Now that row (0,0) is deleted, we should have slid up to be at just 10 + // i.e. we should have subtracted the deleted row height from our content offset. + XCTAssertEqual(node.contentOffset.y, 10); +} + +- (void)testTableViewReloadDoesReloadIfEditableTextNodeIsFirstResponder +{ + ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 375, 667)]; + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + node.frame = window.bounds; + [window addSubnode:node]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + dataSource.rowsPerSection = 1; + dataSource.numberOfSections = 1; + // Currently this test requires that the text in the cell node fills the + // visible width, so we use the long description for the index path. + dataSource.nodeBlockForItem = ^(NSIndexPath *indexPath) { + return (ASCellNodeBlock)^{ + ASCellNode *cellNode = [[ASCellNode alloc] init]; + cellNode.automaticallyManagesSubnodes = YES; + cellNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) child:editableTextNode]; + }; + return cellNode; + }; + }; + node.delegate = dataSource; + node.dataSource = dataSource; + + // Reload the data for the initial load + [node reloadData]; + [node waitUntilAllUpdatesAreProcessed]; + [node setNeedsLayout]; + [node layoutIfNeeded]; + + // Set the textView as first responder + [editableTextNode.textView becomeFirstResponder]; + + // Change data source count and try to reload a second time + dataSource.rowsPerSection = 2; + [node reloadData]; + [node waitUntilAllUpdatesAreProcessed]; + + // Check that numberOfRows in section 0 is 2 + XCTAssertEqual([node numberOfRowsInSection:0], 2); + XCTAssertEqual([node.view numberOfRowsInSection:0], 2); +} + +@end + +@implementation UITableView (Testing) + ++ (void)as_recordEditingCallsIntoArray:(NSMutableArray *)selectors +{ + [UITableView swizzleInstanceMethod:@selector(reloadData) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *) { + JGOriginalImplementation(void); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; + [UITableView swizzleInstanceMethod:@selector(beginUpdates) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *) { + JGOriginalImplementation(void); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; + [UITableView swizzleInstanceMethod:@selector(endUpdates) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *) { + JGOriginalImplementation(void); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; + [UITableView swizzleInstanceMethod:@selector(insertRowsAtIndexPaths:withRowAnimation:) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *, NSArray *indexPaths, UITableViewRowAnimation anim) { + JGOriginalImplementation(void, indexPaths, anim); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; + [UITableView swizzleInstanceMethod:@selector(deleteRowsAtIndexPaths:withRowAnimation:) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *, NSArray *indexPaths, UITableViewRowAnimation anim) { + JGOriginalImplementation(void, indexPaths, anim); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; + [UITableView swizzleInstanceMethod:@selector(insertSections:withRowAnimation:) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *, NSIndexSet *indexes, UITableViewRowAnimation anim) { + JGOriginalImplementation(void, indexes, anim); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; + [UITableView swizzleInstanceMethod:@selector(deleteSections:withRowAnimation:) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, UITableView *, NSIndexSet *indexes, UITableViewRowAnimation anim) { + JGOriginalImplementation(void, indexes, anim); + [selectors addObject:NSStringFromSelector(_cmd)]; + }; + }]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTableViewThrashTests.mm b/submodules/AsyncDisplayKit/Tests/ASTableViewThrashTests.mm new file mode 100644 index 0000000000..8d7064cd00 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTableViewThrashTests.mm @@ -0,0 +1,154 @@ +// +// ASTableViewThrashTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import + +#import "ASThrashUtility.h" + +@interface ASTableViewThrashTests: XCTestCase +@end + +@implementation ASTableViewThrashTests +{ + // The current update, which will be logged in case of a failure. + ASThrashUpdate *_update; + BOOL _failed; +} + +#pragma mark Overrides + +- (void)tearDown +{ + if (_failed && _update != nil) { + NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); + } + _failed = NO; + _update = nil; +} + +// NOTE: Despite the documentation, this is not always called if an exception is caught. +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected +{ + _failed = YES; + [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; +} + +#pragma mark Test Methods + +// Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. +- (void)testInitialDataRead +{ + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; + [self verifyDataSource:ds]; +} + +/// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file +- (void)testRecordedThrashCase +{ + NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; + + _update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (_update == nil) { + return; + } + + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:_update.oldData]; + ds.tableView.test_enableSuperUpdateCallLogging = YES; + [self applyUpdate:_update toDataSource:ds]; + [self verifyDataSource:ds]; +} + +// Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. +- (void)testThrashingWildly +{ + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { + [self setUp]; + @autoreleasepool { + NSArray *sections = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _update = [[ASThrashUpdate alloc] initWithData:sections]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:sections]; + + [self applyUpdate:_update toDataSource:ds]; + [self verifyDataSource:ds]; + [self expectationForPredicate:[ds predicateForDeallocatedHierarchy] evaluatedWithObject:(id)kCFNull handler:nil]; + } + [self waitForExpectationsWithTimeout:3 handler:nil]; + + [self tearDown]; + } +} + +#pragma mark Helpers + +- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource +{ + TableView *tableView = dataSource.tableView; + + [tableView beginUpdates]; + dataSource.data = update.data; + + [tableView insertSections:update.insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:update.deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView reloadSections:update.replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [update.insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [update.deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [update.replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + @try { + [tableView endUpdatesAnimated:NO completion:nil]; +#if !USE_UIKIT_REFERENCE + [tableView waitUntilAllUpdatesAreCommitted]; +#endif + } @catch (NSException *exception) { + _failed = YES; + @throw exception; + } +} + +- (void)verifyDataSource:(ASThrashDataSource *)ds +{ + TableView *tableView = ds.tableView; + NSArray *data = [ds data]; + XCTAssertEqual(data.count, tableView.numberOfSections); + for (NSInteger i = 0; i < tableView.numberOfSections; i++) { + XCTAssertEqual([tableView numberOfRowsInSection:i], data[i].items.count); + XCTAssertEqual([tableView rectForHeaderInSection:i].size.height, data[i].headerHeight); + + for (NSInteger j = 0; j < [tableView numberOfRowsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASThrashTestItem *item = data[i].items[j]; +#if USE_UIKIT_REFERENCE + XCTAssertEqual([tableView rectForRowAtIndexPath:indexPath].size.height, item.rowHeight); +#else + ASThrashTestNode *node = (ASThrashTestNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqualObjects(node.item, item, @"Wrong node at index path %@", indexPath); +#endif + } + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTextKitCoreTextAdditionsTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextKitCoreTextAdditionsTests.mm new file mode 100644 index 0000000000..8b0c47be8d --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextKitCoreTextAdditionsTests.mm @@ -0,0 +1,74 @@ +// +// ASTextKitCoreTextAdditionsTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +#if AS_ENABLE_TEXTNODE + +BOOL floatsCloseEnough(CGFloat float1, CGFloat float2) { + CGFloat epsilon = 0.00001; + return (fabs(float1 - float2) < epsilon); +} + +@interface ASTextKitCoreTextAdditionsTests : XCTestCase + +@end + +@implementation ASTextKitCoreTextAdditionsTests + +- (void)testAttributeCleansing +{ + UIFont *font = [UIFont systemFontOfSize:12.0]; + NSMutableAttributedString *testString = [[NSMutableAttributedString alloc] initWithString:@"Test" attributes:@{NSFontAttributeName:font}]; + CFRange cfRange = CFRangeMake(0, testString.length); + CGColorRef blueColor = CGColorRetain([UIColor blueColor].CGColor); + CFAttributedStringSetAttribute((CFMutableAttributedStringRef)testString, + cfRange, + kCTForegroundColorAttributeName, + blueColor); + UIColor *color = [UIColor colorWithCGColor:blueColor]; + + NSAttributedString *actualCleansedString = ASCleanseAttributedStringOfCoreTextAttributes(testString); + XCTAssertTrue([[actualCleansedString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:NULL] isEqual:color], @"Expected the %@ core text attribute to be cleansed from the string %@\n Should match %@", kCTForegroundColorFromContextAttributeName, actualCleansedString, color); + CGColorRelease(blueColor); +} + +- (void)testNoAttributeCleansing +{ + NSMutableAttributedString *testString = [[NSMutableAttributedString alloc] initWithString:@"Test" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12.0], + NSForegroundColorAttributeName : [UIColor blueColor]}]; + + NSAttributedString *actualCleansedString = ASCleanseAttributedStringOfCoreTextAttributes(testString); + XCTAssertTrue([testString isEqualToAttributedString:actualCleansedString], @"Expected the output string %@ to be the same as the input %@ if there are no core text attributes", actualCleansedString, testString); +} + +- (void)testNSParagraphStyleNoCleansing +{ + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.lineSpacing = 10.0; + + //NSUnderlineStyleAttributeName flags the unsupported CT attribute check + NSDictionary *attributes = @{NSParagraphStyleAttributeName:paragraphStyle, + NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)}; + + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:@"Test" attributes:attributes]; + NSAttributedString *cleansedString = ASCleanseAttributedStringOfCoreTextAttributes(attributedString); + + NSParagraphStyle *cleansedParagraphStyle = [cleansedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL]; + + XCTAssertTrue(floatsCloseEnough(cleansedParagraphStyle.lineSpacing, paragraphStyle.lineSpacing), @"Expected the output line spacing: %f to be equal to the input line spacing: %f", cleansedParagraphStyle.lineSpacing, paragraphStyle.lineSpacing); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Tests/ASTextKitFontSizeAdjusterTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextKitFontSizeAdjusterTests.mm new file mode 100644 index 0000000000..c91ff99a49 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextKitFontSizeAdjusterTests.mm @@ -0,0 +1,51 @@ +// +// ASTextKitFontSizeAdjusterTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_ENABLE_TEXTNODE + +@interface ASFontSizeAdjusterTests : XCTestCase + +@end + +@implementation ASFontSizeAdjusterTests + +- (void)testFontSizeAdjusterAttributes +{ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineHeightMultiple = 2.0; + paragraphStyle.lineSpacing = 2.0; + paragraphStyle.paragraphSpacing = 4.0; + paragraphStyle.firstLineHeadIndent = 6.0; + paragraphStyle.headIndent = 8.0; + paragraphStyle.tailIndent = 10.0; + paragraphStyle.minimumLineHeight = 12.0; + paragraphStyle.maximumLineHeight = 14.0; + + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet" + attributes:@{ NSParagraphStyleAttributeName: paragraphStyle }]; + + [ASTextKitFontSizeAdjuster adjustFontSizeForAttributeString:string withScaleFactor:0.5]; + + NSParagraphStyle *adjustedParagraphStyle = [string attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil]; + + XCTAssertEqual(adjustedParagraphStyle.lineHeightMultiple, 2.0); + XCTAssertEqual(adjustedParagraphStyle.lineSpacing, 1.0); + XCTAssertEqual(adjustedParagraphStyle.paragraphSpacing, 2.0); + XCTAssertEqual(adjustedParagraphStyle.firstLineHeadIndent, 3.0); + XCTAssertEqual(adjustedParagraphStyle.headIndent, 4.0); + XCTAssertEqual(adjustedParagraphStyle.tailIndent, 5.0); + XCTAssertEqual(adjustedParagraphStyle.minimumLineHeight, 6.0); + XCTAssertEqual(adjustedParagraphStyle.maximumLineHeight, 7.0); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Tests/ASTextKitTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextKitTests.mm new file mode 100644 index 0000000000..efd93cf809 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextKitTests.mm @@ -0,0 +1,231 @@ +// +// ASTextKitTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" +#import +#pragma clang diagnostic pop + +#import + +#if AS_ENABLE_TEXTNODE + +#import +#import +#import +#import + +#import + +@interface ASTextKitTests : XCTestCase + +@end + +static UITextView *UITextViewWithAttributes(const ASTextKitAttributes &attributes, + const CGSize constrainedSize, + NSDictionary *linkTextAttributes) +{ + UITextView *textView = [[UITextView alloc] initWithFrame:{ .size = constrainedSize }]; + textView.backgroundColor = [UIColor clearColor]; + textView.textContainer.lineBreakMode = attributes.lineBreakMode; + textView.textContainer.lineFragmentPadding = 0.f; + textView.textContainer.maximumNumberOfLines = attributes.maximumNumberOfLines; + textView.textContainerInset = UIEdgeInsetsZero; + textView.layoutManager.usesFontLeading = NO; + textView.attributedText = attributes.attributedString; + textView.linkTextAttributes = linkTextAttributes; + return textView; +} + +static UIImage *UITextViewImageWithAttributes(const ASTextKitAttributes &attributes, + const CGSize constrainedSize, + NSDictionary *linkTextAttributes) +{ + UITextView *textView = UITextViewWithAttributes(attributes, constrainedSize, linkTextAttributes); + UIGraphicsBeginImageContextWithOptions(constrainedSize, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSaveGState(context); + { + [textView.layer renderInContext:context]; + } + CGContextRestoreGState(context); + + UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return snapshot; +} + +static UIImage *ASTextKitImageWithAttributes(const ASTextKitAttributes &attributes, const CGSize constrainedSize) +{ + ASTextKitRenderer *renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes + constrainedSize:constrainedSize]; + UIGraphicsBeginImageContextWithOptions(constrainedSize, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSaveGState(context); + { + [renderer drawInContext:context bounds:{.size = constrainedSize}]; + } + CGContextRestoreGState(context); + + UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return snapshot; +} + +// linkTextAttributes are only applied to UITextView +static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize constrainedSize, NSDictionary *linkTextAttributes) +{ + FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] init]; + UIImage *labelImage = UITextViewImageWithAttributes(attributes, constrainedSize, linkTextAttributes); + UIImage *textKitImage = ASTextKitImageWithAttributes(attributes, constrainedSize); + return [controller compareReferenceImage:labelImage toImage:textKitImage tolerance:0.0 error:nil]; +} + +@implementation ASTextKitTests + +- (void)testSimpleStrings +{ + ASTextKitAttributes attributes { + .attributedString = [[NSAttributedString alloc] initWithString:@"hello" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}] + }; + XCTAssert(checkAttributes(attributes, { 100, 100 }, nil)); +} + +- (void)testChangingAPropertyChangesHash +{ + NSAttributedString *as = [[NSAttributedString alloc] initWithString:@"hello" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}]; + + ASTextKitAttributes attrib1 { + .attributedString = as, + .lineBreakMode = NSLineBreakByClipping, + }; + ASTextKitAttributes attrib2 { + .attributedString = as, + }; + + XCTAssertNotEqual(attrib1.hash(), attrib2.hash(), @"Hashes should differ when NSLineBreakByClipping changes."); +} + +- (void)testSameStringHashesSame +{ + ASTextKitAttributes attrib1 { + .attributedString = [[NSAttributedString alloc] initWithString:@"hello" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}], + }; + ASTextKitAttributes attrib2 { + .attributedString = [[NSAttributedString alloc] initWithString:@"hello" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}], + }; + + XCTAssertEqual(attrib1.hash(), attrib2.hash(), @"Hashes should be the same!"); +} + + +- (void)testStringsWithVariableAttributes +{ + NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:@"hello" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}]; + for (int i = 0; i < attrStr.length; i++) { + // Color each character something different + CGFloat factor = ((CGFloat)i) / ((CGFloat)attrStr.length); + [attrStr addAttribute:NSForegroundColorAttributeName + value:[UIColor colorWithRed:factor + green:1.0 - factor + blue:0.0 + alpha:1.0] + range:NSMakeRange(i, 1)]; + } + ASTextKitAttributes attributes { + .attributedString = attrStr + }; + XCTAssert(checkAttributes(attributes, { 100, 100 }, nil)); +} + +- (void)testLinkInTextUsesForegroundColor +{ + NSDictionary *linkTextAttributes = @{ NSForegroundColorAttributeName : [UIColor redColor], + // UITextView adds underline by default and we can't get rid of it + // so we have to choose a style and color and match it in the text kit version + // for this test + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle), + NSUnderlineColorAttributeName: [UIColor blueColor], + }; + NSDictionary *textAttributes = @{NSFontAttributeName : [UIFont systemFontOfSize:12], + }; + + NSString *prefixString = @"click "; + NSString *linkString = @"this link"; + NSString *textString = [prefixString stringByAppendingString:linkString]; + + NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:textString attributes:textAttributes]; + NSURL *linkURL = [NSURL URLWithString:@"https://github.com/facebook/AsyncDisplayKit/issues/967"]; + NSRange selectedRange = (NSRange){prefixString.length, linkString.length}; + + [attrStr addAttribute:NSLinkAttributeName value:linkURL range:selectedRange]; + + for (NSString *attributeName in linkTextAttributes.keyEnumerator) { + [attrStr addAttribute:attributeName + value:linkTextAttributes[attributeName] + range:selectedRange]; + } + + ASTextKitAttributes textKitattributes { + .attributedString = attrStr + }; + + XCTAssert(checkAttributes(textKitattributes, { 100, 100 }, linkTextAttributes)); +} + +- (void)testRectsForRangeBeyondTruncationSizeReturnsNonZeroNumberOfRects +{ + NSAttributedString *attributedString = + [[NSAttributedString alloc] + initWithString:@"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers Odd Future master cleanse tattooed four dollar toast small batch kale chips leggings meh photo booth occupy irony. " attributes:@{ASTextKitEntityAttributeName : [[ASTextKitEntityAttribute alloc] initWithEntity:@"entity"]}]; + ASTextKitRenderer *renderer = + [[ASTextKitRenderer alloc] + initWithTextKitAttributes:{ + .attributedString = attributedString, + .maximumNumberOfLines = 1, + .truncationAttributedString = [[NSAttributedString alloc] initWithString:@"... Continue Reading"] + } + constrainedSize:{ 100, 100 }]; + XCTAssert([renderer rectsForTextRange:NSMakeRange(0, attributedString.length) measureOption:ASTextKitRendererMeasureOptionBlock].count > 0); +} + +- (void)testTextKitComponentsCanCalculateSizeInBackground +{ + NSAttributedString *attributedString = + [[NSAttributedString alloc] + initWithString:@"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers Odd Future master cleanse tattooed four dollar toast small batch kale chips leggings meh photo booth occupy irony. " attributes:@{ASTextKitEntityAttributeName : [[ASTextKitEntityAttribute alloc] initWithEntity:@"entity"]}]; + ASTextKitComponents *components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:CGSizeZero]; + components.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:components.textContainer]; + components.textView.frame = CGRectMake(0, 0, 20, 1000); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Components deallocated in background"]; + + ASPerformBlockOnBackgroundThread(^{ + // Use an autorelease pool here to ensure temporary components are (and can be) released in background + @autoreleasepool { + [components sizeForConstrainedWidth:100]; + [components sizeForConstrainedWidth:50 forMaxNumberOfLines:5]; + } + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Tests/ASTextKitTruncationTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextKitTruncationTests.mm new file mode 100644 index 0000000000..a9d5544c3e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextKitTruncationTests.mm @@ -0,0 +1,165 @@ +// +// ASTextKitTruncationTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import + +#if AS_ENABLE_TEXTNODE + +#import + +@interface ASTextKitTruncationTests : XCTestCase + +@end + +@implementation ASTextKitTruncationTests + +- (NSString *)_sentenceString +{ + return @"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers Odd Future master cleanse tattooed four dollar toast small batch kale chips leggings meh photo booth occupy irony."; +} + +- (NSAttributedString *)_sentenceAttributedString +{ + return [[NSAttributedString alloc] initWithString:[self _sentenceString] attributes:@{}]; +} + +- (NSAttributedString *)_simpleTruncationAttributedString +{ + return [[NSAttributedString alloc] initWithString:@"..." attributes:@{}]; +} + +- (void)testEmptyTruncationStringSameAsStraightTextKitTailTruncation +{ + CGSize constrainedSize = CGSizeMake(100, 50); + NSAttributedString *attributedString = [self _sentenceAttributedString]; + ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString + lineBreakMode:NSLineBreakByWordWrapping + maximumNumberOfLines:0 + exclusionPaths:nil + constrainedSize:constrainedSize]; + __block NSRange textKitVisibleRange; + [context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + textKitVisibleRange = [layoutManager characterRangeForGlyphRange:[layoutManager glyphRangeForTextContainer:textContainer] + actualGlyphRange:NULL]; + }]; + ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context + truncationAttributedString:nil + avoidTailTruncationSet:nil]; + [tailTruncater truncate]; + XCTAssert(NSEqualRanges(textKitVisibleRange, tailTruncater.visibleRanges[0])); + XCTAssert(NSEqualRanges(textKitVisibleRange, tailTruncater.firstVisibleRange)); +} + +- (void)testSimpleTailTruncation +{ + CGSize constrainedSize = CGSizeMake(100, 60); + NSAttributedString *attributedString = [self _sentenceAttributedString]; + ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString + lineBreakMode:NSLineBreakByWordWrapping + maximumNumberOfLines:0 + exclusionPaths:nil + constrainedSize:constrainedSize]; + ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context + truncationAttributedString:[self _simpleTruncationAttributedString] + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@""]]; + [tailTruncater truncate]; + __block NSString *drawnString; + [context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + drawnString = textStorage.string; + }]; + NSString *expectedString = @"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers..."; + XCTAssertEqualObjects(expectedString, drawnString); + XCTAssert(NSEqualRanges(NSMakeRange(0, 62), tailTruncater.visibleRanges[0])); + XCTAssert(NSEqualRanges(NSMakeRange(0, 62), tailTruncater.firstVisibleRange)); +} + +- (void)testAvoidedCharTailWordBoundaryTruncation +{ + CGSize constrainedSize = CGSizeMake(100, 50); + NSAttributedString *attributedString = [self _sentenceAttributedString]; + ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString + lineBreakMode:NSLineBreakByWordWrapping + maximumNumberOfLines:0 + exclusionPaths:nil + constrainedSize:constrainedSize]; + ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context + truncationAttributedString:[self _simpleTruncationAttributedString] + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]; + [tailTruncater truncate]; + __block NSString *drawnString; + [context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + drawnString = textStorage.string; + }]; + // This should have removed the additional "." in the string right after Carles. + NSString *expectedString = @"90's cray photo booth tote bag bespoke Carles..."; + XCTAssertEqualObjects(expectedString, drawnString); +} + +- (void)testAvoidedCharTailCharBoundaryTruncation +{ + CGSize constrainedSize = CGSizeMake(50, 50); + NSAttributedString *attributedString = [self _sentenceAttributedString]; + ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString + lineBreakMode:NSLineBreakByCharWrapping + maximumNumberOfLines:0 + exclusionPaths:nil + constrainedSize:constrainedSize]; + ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context + truncationAttributedString:[self _simpleTruncationAttributedString] + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]; + [tailTruncater truncate]; + __block NSString *drawnString; + [context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + drawnString = textStorage.string; + }]; + // This should have removed the additional "." in the string right after Carles. + NSString *expectedString = @"90's cray photo booth t..."; + XCTAssertEqualObjects(expectedString, drawnString); +} + +- (void)testHandleZeroSizeConstrainedSize +{ + CGSize constrainedSize = CGSizeZero; + NSAttributedString *attributedString = [self _sentenceAttributedString]; + + ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString + lineBreakMode:NSLineBreakByWordWrapping + maximumNumberOfLines:0 + exclusionPaths:nil + constrainedSize:constrainedSize]; + ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context + truncationAttributedString:[self _simpleTruncationAttributedString] + avoidTailTruncationSet:nil]; + XCTAssertNoThrow([tailTruncater truncate]); + XCTAssert(tailTruncater.visibleRanges.size() == 0); + NSEqualRanges(NSMakeRange(0, 0), tailTruncater.firstVisibleRange); +} + +- (void)testHandleZeroHeightConstrainedSize +{ + CGSize constrainedSize = CGSizeMake(50, 0); + NSAttributedString *attributedString = [self _sentenceAttributedString]; + ASTextKitContext *context = [[ASTextKitContext alloc] initWithAttributedString:attributedString + lineBreakMode:NSLineBreakByCharWrapping + maximumNumberOfLines:0 + exclusionPaths:nil + constrainedSize:constrainedSize]; + + ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context + truncationAttributedString:[self _simpleTruncationAttributedString] + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]; + XCTAssertNoThrow([tailTruncater truncate]); +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Tests/ASTextNode2SnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextNode2SnapshotTests.mm new file mode 100644 index 0000000000..9b48a82e4a --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextNode2SnapshotTests.mm @@ -0,0 +1,301 @@ +// +// ASTextNode2SnapshotTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import "ASTestCase.h" +#import "ASSnapshotTestCase.h" +#import + +@interface ASTextNode2SnapshotTests : ASSnapshotTestCase + +@end + +@interface LineBreakConfig : NSObject + +@property (nonatomic, assign) NSUInteger numberOfLines; +@property (nonatomic, assign) NSLineBreakMode lineBreakMode; + ++ (NSArray *)configs; + +- (instancetype)initWithNumberOfLines:(NSUInteger)numberOfLines lineBreakMode:(NSLineBreakMode)lineBreakMode; +- (NSString *)breakModeDescription; + +@end + +@implementation LineBreakConfig + ++ (NSArray *)configs +{ + static dispatch_once_t init_predicate; + static NSArray *allConfigs = nil; + + dispatch_once(&init_predicate, ^{ + NSMutableArray *setup = [NSMutableArray new]; + for (int i = 0; i <= 3; i++) { + for (int j = NSLineBreakByWordWrapping; j <= NSLineBreakByTruncatingMiddle; j++) { + if (j == NSLineBreakByClipping) continue; + [setup addObject:[[LineBreakConfig alloc] initWithNumberOfLines:i lineBreakMode:(NSLineBreakMode) j]]; + } + + allConfigs = [NSArray arrayWithArray:setup]; + } + }); + return allConfigs; +} + +- (instancetype)initWithNumberOfLines:(NSUInteger)numberOfLines lineBreakMode:(NSLineBreakMode)lineBreakMode +{ + self = [super init]; + if (self != nil) { + _numberOfLines = numberOfLines; + _lineBreakMode = lineBreakMode; + + return self; + } + return nil; +} + +- (NSString *)breakModeDescription { + NSString *lineBreak = nil; + switch (self.lineBreakMode) { + case NSLineBreakByTruncatingHead: + lineBreak = @"NSLineBreakByTruncatingHead"; + break; + case NSLineBreakByCharWrapping: + lineBreak = @"NSLineBreakByCharWrapping"; + break; + case NSLineBreakByClipping: + lineBreak = @"NSLineBreakByClipping"; + break; + case NSLineBreakByWordWrapping: + lineBreak = @"NSLineBreakByWordWrapping"; + break; + case NSLineBreakByTruncatingTail: + lineBreak = @"NSLineBreakByTruncatingTail"; + break; + case NSLineBreakByTruncatingMiddle: + lineBreak = @"NSLineBreakByTruncatingMiddle"; + break; + default: + lineBreak = @"Unknown?"; + break; + } + return lineBreak; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"numberOfLines: %lu\nlineBreakMode: %@", (unsigned long) self.numberOfLines, [self breakModeDescription]]; +} + +@end + +@implementation ASTextNode2SnapshotTests + +- (void)setUp +{ + [super setUp]; + + // This will use ASTextNode2 for snapshot tests. + // All tests are duplicated from ASTextNodeSnapshotTests. + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; +#if AS_ENABLE_TEXTNODE + config.experimentalFeatures = ASExperimentalTextNode; +#endif + [ASConfigurationManager test_resetWithConfiguration:config]; + + self.recordMode = NO; +} + +- (void)tearDown +{ + [super tearDown]; + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = kNilOptions; + [ASConfigurationManager test_resetWithConfiguration:config]; +} + +- (void)testTextContainerInset_ASTextNode2 +{ + // trivial test case to ensure ASSnapshotTestCase works + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar" + attributes:@{NSFontAttributeName: [UIFont italicSystemFontOfSize:24]}]; + textNode.textContainerInset = UIEdgeInsetsMake(0, 2, 0, 2); + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testTextTruncationModes_ASTextNode2 +{ + UIView *container = [[UIView alloc] initWithFrame:(CGRect) {CGPointZero, (CGSize) {375.0f, 667.0f}}]; + + UILabel *textNodeLabel = [[UILabel alloc] init]; + UILabel *uiLabelLabel = [[UILabel alloc] init]; + UILabel *description = [[UILabel alloc] init]; + textNodeLabel.text = @"ASTextNode2:"; + textNodeLabel.font = [UIFont boldSystemFontOfSize:16.0]; + textNodeLabel.textColor = [UIColor colorWithRed:0.0 green:0.7 blue:0.0 alpha:1.0]; + uiLabelLabel.text = @"UILabel:"; + uiLabelLabel.font = [UIFont boldSystemFontOfSize:16.0]; + uiLabelLabel.textColor = [UIColor colorWithRed:0.0 green:0.7 blue:0.0 alpha:1.0]; + + description.text = @""; + description.font = [UIFont italicSystemFontOfSize:16.0]; + description.numberOfLines = 0; + + uiLabelLabel.textColor = [UIColor colorWithRed:0.0 green:0.7 blue:0.0 alpha:1.0]; + + UILabel *reference = [[UILabel alloc] init]; + ASTextNode *textNode = [[ASTextNode alloc] init]; // ASTextNode2 + + NSMutableAttributedString *refString = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:18.0f] }]; + NSMutableAttributedString *asString = [refString mutableCopy]; + + reference.attributedText = refString; + textNode.attributedText = asString; + + CGSize size = (CGSize) {container.bounds.size.width, 120.0}; + CGPoint origin = (CGPoint) {CGRectGetWidth(container.bounds) / 2 - size.width / 2, CGRectGetHeight(container.bounds) / 2 - size.height / 2}; // center + + textNode.frame = (CGRect) {origin, size}; + reference.frame = CGRectOffset(textNode.frame, 0, -160.0f); + + textNodeLabel.bounds = (CGRect) {CGPointZero, (CGSize) {container.bounds.size.width, textNodeLabel.font.lineHeight}}; + origin = (CGPoint) {textNode.frame.origin.x, textNode.frame.origin.y - textNodeLabel.bounds.size.height}; + textNodeLabel.frame = (CGRect) {origin, textNodeLabel.bounds.size}; + + uiLabelLabel.bounds = (CGRect) {CGPointZero, (CGSize) {container.bounds.size.width, uiLabelLabel.font.lineHeight}}; + origin = (CGPoint) {reference.frame.origin.x, reference.frame.origin.y - uiLabelLabel.bounds.size.height}; + uiLabelLabel.frame = (CGRect) {origin, uiLabelLabel.bounds.size}; + + uiLabelLabel.bounds = (CGRect) {CGPointZero, (CGSize) {container.bounds.size.width, uiLabelLabel.font.lineHeight}}; + origin = (CGPoint) {textNode.frame.origin.x, textNode.frame.origin.y - uiLabelLabel.bounds.size.height}; + uiLabelLabel.frame = (CGRect) {origin, uiLabelLabel.bounds.size}; + + uiLabelLabel.bounds = (CGRect) {CGPointZero, (CGSize) {container.bounds.size.width, uiLabelLabel.font.lineHeight}}; + origin = (CGPoint) {reference.frame.origin.x, reference.frame.origin.y - uiLabelLabel.bounds.size.height}; + uiLabelLabel.frame = (CGRect) {origin, uiLabelLabel.bounds.size}; + + description.bounds = textNode.bounds; + description.frame = (CGRect) {(CGPoint) {0, container.bounds.size.height * 0.8}, description.bounds.size}; + + [container addSubview:reference]; + [container addSubview:textNode.view]; + [container addSubview:textNodeLabel]; + [container addSubview:uiLabelLabel]; + [container addSubview:description]; + + NSArray *c = [LineBreakConfig configs]; + for (LineBreakConfig *config in c) { + reference.lineBreakMode = textNode.truncationMode = config.lineBreakMode; + reference.numberOfLines = textNode.maximumNumberOfLines = config.numberOfLines; + description.text = config.description; + [container setNeedsLayout]; + NSString *identifier = [NSString stringWithFormat:@"%@_%luLines", [config breakModeDescription], (unsigned long)config.numberOfLines]; + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; + ASSnapshotVerifyViewWithTolerance(container, identifier, 0.01); + } +} + +- (void)testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2 +{ + UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + backgroundView.layer.as_allowsHighlightDrawing = YES; + + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar judar judar judar judar judar" + attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }]; + + textNode.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); + + ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 80))]; + textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height); + + [backgroundView addSubview:textNode.view]; + backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50)); + + textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length); + + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; + ASSnapshotVerifyLayer(backgroundView.layer, nil); +} + +- (void)testTextContainerInsetHighlight_ASTextNode2 +{ + UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + backgroundView.layer.as_allowsHighlightDrawing = YES; + + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"yolo" + attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }]; + + textNode.textContainerInset = UIEdgeInsetsMake(5, 10, 10, 5); + ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))]; + textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height); + + [backgroundView addSubview:textNode.view]; + backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50)); + + textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length); + + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; + ASSnapshotVerifyView(backgroundView, nil); +} + +// This test is disabled because the fast-path is disabled. +- (void)DISABLED_testThatFastPathTruncationWorks_ASTextNode2 +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))]; + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testThatSlowPathTruncationWorks_ASTextNode2 +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; + // Set exclusion paths to trigger slow path + textNode.exclusionPaths = @[ [UIBezierPath bezierPath] ]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))); + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testShadowing_ASTextNode2 +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important"]; + textNode.shadowColor = [UIColor blackColor].CGColor; + textNode.shadowOpacity = 0.3; + textNode.shadowRadius = 3; + textNode.shadowOffset = CGSizeMake(0, 1); + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + +/** + * https://github.com/TextureGroup/Texture/issues/822 + */ +- (void)DISABLED_testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode_ASTextNode2 +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(20, 80); + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] initWithString:@"Quality is an important "]; + [mas appendAttributedString:[[NSAttributedString alloc] initWithString:@"thing" attributes:@{ NSBackgroundColorAttributeName : UIColor.yellowColor}]]; + textNode.attributedText = mas; + textNode.truncationMode = NSLineBreakByTruncatingTail; + + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:@{ NSBackgroundColorAttributeName: UIColor.greenColor }]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTextNode2Tests.mm b/submodules/AsyncDisplayKit/Tests/ASTextNode2Tests.mm new file mode 100644 index 0000000000..b8f08ed451 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextNode2Tests.mm @@ -0,0 +1,94 @@ +// +// ASTextNode2Tests.mm +// TextureTests +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import +#import +#import + +#import "ASTestCase.h" + +@interface ASTextNode2Tests : XCTestCase + +@property(nonatomic) ASTextNode2 *textNode; +@property(nonatomic, copy) NSAttributedString *attributedText; + +@end + +@implementation ASTextNode2Tests + +- (void)setUp +{ + [super setUp]; + _textNode = [[ASTextNode2 alloc] init]; + + UIFontDescriptor *desc = [UIFontDescriptor fontDescriptorWithName:@"Didot" size:18]; + NSArray *arr = @[ @{ + UIFontFeatureTypeIdentifierKey : @(kLetterCaseType), + UIFontFeatureSelectorIdentifierKey : @(kSmallCapsSelector) + } ]; + desc = [desc fontDescriptorByAddingAttributes:@{UIFontDescriptorFeatureSettingsAttribute : arr}]; + UIFont *f = [UIFont fontWithDescriptor:desc size:0]; + NSDictionary *d = @{NSFontAttributeName : f}; + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] + initWithString: + @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " + @"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " + @"exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + @"dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + @"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + @"mollit anim id est laborum." + attributes:d]; + NSMutableParagraphStyle *para = [NSMutableParagraphStyle new]; + para.alignment = NSTextAlignmentCenter; + para.lineSpacing = 1.0; + [mas addAttribute:NSParagraphStyleAttributeName value:para range:NSMakeRange(0, mas.length - 1)]; + + // Vary the linespacing on the last line + NSMutableParagraphStyle *lastLinePara = [NSMutableParagraphStyle new]; + lastLinePara.alignment = para.alignment; + lastLinePara.lineSpacing = 5.0; + [mas addAttribute:NSParagraphStyleAttributeName + value:lastLinePara + range:NSMakeRange(mas.length - 1, 1)]; + + _attributedText = mas; + _textNode.attributedText = _attributedText; +} + +- (void)testTruncation +{ + XCTAssertTrue([(ASTextNode *)_textNode shouldTruncateForConstrainedSize:ASSizeRangeMake(CGSizeMake(100, 100))], @"Text Node should truncate"); + + _textNode.frame = CGRectMake(0, 0, 100, 100); + XCTAssertTrue(_textNode.isTruncated, @"Text Node should be truncated"); +} + +- (void)testAccessibility +{ + XCTAssertTrue(_textNode.isAccessibilityElement, @"Should be an accessibility element"); + XCTAssertTrue(_textNode.accessibilityTraits == UIAccessibilityTraitStaticText, + @"Should have static text accessibility trait, instead has %llu", + _textNode.accessibilityTraits); + XCTAssertTrue(_textNode.defaultAccessibilityTraits == UIAccessibilityTraitStaticText, + @"Default accessibility traits should return static text accessibility trait, " + @"instead returns %llu", + _textNode.defaultAccessibilityTraits); + + XCTAssertTrue([_textNode.accessibilityLabel isEqualToString:_attributedText.string], + @"Accessibility label is incorrectly set to \n%@\n when it should be \n%@\n", + _textNode.accessibilityLabel, _attributedText.string); + XCTAssertTrue([_textNode.defaultAccessibilityLabel isEqualToString:_attributedText.string], + @"Default accessibility label incorrectly returns \n%@\n when it should be \n%@\n", + _textNode.defaultAccessibilityLabel, _attributedText.string); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTextNodePerformanceTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextNodePerformanceTests.mm new file mode 100644 index 0000000000..961d192aa7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextNodePerformanceTests.mm @@ -0,0 +1,234 @@ +// +// ASTextNodePerformanceTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASPerformanceTestContext.h" +#import +#import +#import +#import + +#import "ASXCTExtensions.h" + +/** + * NOTE: This test case is not run during the "test" action. You have to run it manually (click the little diamond.) + */ + +@interface ASTextNodePerformanceTests : XCTestCase + +@end + +@implementation ASTextNodePerformanceTests + +#pragma mark Performance Tests + +static NSString *const kTestCaseUIKit = @"UIKit"; +static NSString *const kTestCaseASDK = @"ASDK"; +static NSString *const kTestCaseUIKitPrivateCaching = @"UIKitPrivateCaching"; +static NSString *const kTestCaseUIKitWithNoContext = @"UIKitNoContext"; +static NSString *const kTestCaseUIKitWithFreshContext = @"UIKitFreshContext"; +static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; + ++ (NSArray *)realisticDataSet +{ + static NSArray *array; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *file = [[NSBundle bundleForClass:self] pathForResource:@"AttributedStringsFixture0" ofType:@"plist" inDirectory:@"TestResources"]; + if (file != nil) { + array = [NSKeyedUnarchiver unarchiveObjectWithFile:file]; + } + NSAssert([array isKindOfClass:[NSArray class]], nil); + NSSet *unique = [NSSet setWithArray:array]; + NSLog(@"Loaded realistic text data set with %d attributed strings, %d unique.", (int)array.count, (int)unique.count); + }); + return array; +} + +- (void)testPerformance_RealisticData +{ + NSArray *data = [self.class realisticDataSet]; + + CGSize maxSize = CGSizeMake(355, CGFLOAT_MAX); + CGSize __block uiKitSize, __block asdkSize; + + ASPerformanceTestContext *ctx = [[ASPerformanceTestContext alloc] init]; + [ctx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + NSAttributedString *text = data[i % data.count]; + startMeasuring(); + uiKitSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil].size; + stopMeasuring(); + }]; + uiKitSize.width = ASCeilPixelValue(uiKitSize.width); + uiKitSize.height = ASCeilPixelValue(uiKitSize.height); + ctx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uiKitSize); + + [ctx addCaseWithName:kTestCaseASDK block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + ASTextNode *node = [[ASTextNode alloc] init]; + NSAttributedString *text = data[i % data.count]; + startMeasuring(); + node.attributedText = text; + asdkSize = [node layoutThatFits:ASSizeRangeMake(CGSizeZero, maxSize)].size; + stopMeasuring(); + }]; + ctx.results[kTestCaseASDK].userInfo[@"size"] = NSStringFromCGSize(asdkSize); + + ASXCTAssertEqualSizes(uiKitSize, asdkSize); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.2, 0.5); +} + +- (void)testPerformance_TwoParagraphLatinNoTruncation +{ + NSAttributedString *text = [ASTextNodePerformanceTests twoParagraphLatinText]; + + CGSize maxSize = CGSizeMake(355, CGFLOAT_MAX); + CGSize __block uiKitSize, __block asdkSize; + + ASPerformanceTestContext *ctx = [[ASPerformanceTestContext alloc] init]; + [ctx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + startMeasuring(); + uiKitSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil].size; + stopMeasuring(); + }]; + uiKitSize.width = ASCeilPixelValue(uiKitSize.width); + uiKitSize.height = ASCeilPixelValue(uiKitSize.height); + ctx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uiKitSize); + + [ctx addCaseWithName:kTestCaseASDK block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + ASTextNode *node = [[ASTextNode alloc] init]; + startMeasuring(); + node.attributedText = text; + asdkSize = [node layoutThatFits:ASSizeRangeMake(CGSizeZero, maxSize)].size; + stopMeasuring(); + }]; + ctx.results[kTestCaseASDK].userInfo[@"size"] = NSStringFromCGSize(asdkSize); + + ASXCTAssertEqualSizes(uiKitSize, asdkSize); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.5, 0.9); +} + +- (void)testPerformance_OneParagraphLatinWithTruncation +{ + NSAttributedString *text = [ASTextNodePerformanceTests oneParagraphLatinText]; + + CGSize maxSize = CGSizeMake(355, 150); + CGSize __block uiKitSize, __block asdkSize; + + ASPerformanceTestContext *testCtx = [[ASPerformanceTestContext alloc] init]; + [testCtx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + startMeasuring(); + uiKitSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil].size; + stopMeasuring(); + }]; + uiKitSize.width = ASCeilPixelValue(uiKitSize.width); + uiKitSize.height = ASCeilPixelValue(uiKitSize.height); + testCtx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uiKitSize); + + [testCtx addCaseWithName:kTestCaseASDK block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + ASTextNode *node = [[ASTextNode alloc] init]; + startMeasuring(); + node.attributedText = text; + asdkSize = [node layoutThatFits:ASSizeRangeMake(CGSizeZero, maxSize)].size; + stopMeasuring(); + }]; + testCtx.results[kTestCaseASDK].userInfo[@"size"] = NSStringFromCGSize(asdkSize); + + XCTAssert(CGSizeEqualToSizeWithIn(uiKitSize, asdkSize, 5)); + ASXCTAssertRelativePerformanceInRange(testCtx, kTestCaseASDK, 0.1, 0.3); +} + +- (void)testThatNotUsingAStringDrawingContextHasSimilarPerformanceToHavingOne +{ + ASPerformanceTestContext *ctx = [[ASPerformanceTestContext alloc] init]; + + NSAttributedString *text = [ASTextNodePerformanceTests oneParagraphLatinText]; + CGSize maxSize = CGSizeMake(355, 150); + NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine; + __block CGSize size; + // nil context + [ctx addCaseWithName:kTestCaseUIKitWithNoContext block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + startMeasuring(); + size = [text boundingRectWithSize:maxSize options:options context:nil].size; + stopMeasuring(); + }]; + ctx.results[kTestCaseUIKitWithNoContext].userInfo[@"size"] = NSStringFromCGSize(size); + + // Fresh context + [ctx addCaseWithName:kTestCaseUIKitWithFreshContext block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + NSStringDrawingContext *stringDrawingCtx = [[NSStringDrawingContext alloc] init]; + startMeasuring(); + size = [text boundingRectWithSize:maxSize options:options context:stringDrawingCtx].size; + stopMeasuring(); + }]; + ctx.results[kTestCaseUIKitWithFreshContext].userInfo[@"size"] = NSStringFromCGSize(size); + + // Reused context + NSStringDrawingContext *stringDrawingCtx = [[NSStringDrawingContext alloc] init]; + [ctx addCaseWithName:kTestCaseUIKitWithReusedContext block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + startMeasuring(); + size = [text boundingRectWithSize:maxSize options:options context:stringDrawingCtx].size; + stopMeasuring(); + }]; + ctx.results[kTestCaseUIKitWithReusedContext].userInfo[@"size"] = NSStringFromCGSize(size); + + XCTAssertTrue([ctx areAllUserInfosEqual]); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseUIKitWithReusedContext, 0.8, 1.2); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseUIKitWithFreshContext, 0.8, 1.2); +} + +- (void)testThatUIKitPrivateLayoutCachingIsAwesome +{ + NSAttributedString *text = [ASTextNodePerformanceTests oneParagraphLatinText]; + ASPerformanceTestContext *ctx = [[ASPerformanceTestContext alloc] init]; + CGSize maxSize = CGSizeMake(355, 150); + __block CGSize uncachedSize, cachedSize; + NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine; + + // No caching, reused ctx + NSStringDrawingContext *defaultCtx = [[NSStringDrawingContext alloc] init]; + XCTAssertFalse([[defaultCtx valueForKey:@"cachesLayout"] boolValue]); + [ctx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + startMeasuring(); + uncachedSize = [text boundingRectWithSize:maxSize options:options context:defaultCtx].size; + stopMeasuring(); + }]; + XCTAssertFalse([[defaultCtx valueForKey:@"cachesLayout"] boolValue]); + ctx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uncachedSize); + + // Caching + NSStringDrawingContext *cachingCtx = [[NSStringDrawingContext alloc] init]; + [cachingCtx setValue:@YES forKey:@"cachesLayout"]; + [ctx addCaseWithName:kTestCaseUIKitPrivateCaching block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + startMeasuring(); + cachedSize = [text boundingRectWithSize:maxSize options:options context:cachingCtx].size; + stopMeasuring(); + }]; + ctx.results[kTestCaseUIKitPrivateCaching].userInfo[@"size"] = NSStringFromCGSize(cachedSize); + + XCTAssertTrue([ctx areAllUserInfosEqual]); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseUIKitPrivateCaching, 1.2, FLT_MAX); +} + +#pragma mark Fixture Data + ++ (NSMutableAttributedString *)oneParagraphLatinText +{ + NSDictionary *attributes = @{ + NSFontAttributeName: [UIFont systemFontOfSize:14] + }; + return [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam gravida, metus non tincidunt tincidunt, arcu quam vulputate magna, nec semper libero mi in lorem. Quisque turpis erat, congue sit amet eros at, gravida gravida lacus. Maecenas maximus lectus in efficitur pulvinar. Nam elementum massa eget luctus condimentum. Curabitur egestas mauris urna. Fusce lacus ante, laoreet vitae leo quis, mattis aliquam est. Donec bibendum augue at elit lacinia lobortis. Cras imperdiet ac justo eget sollicitudin. Pellentesque malesuada nec tellus vitae dictum. Proin vestibulum tempus odio in condimentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis vel turpis at velit dignissim rutrum. Nunc lorem felis, molestie eget ornare id, luctus at nunc. Maecenas suscipit nisi sit amet nulla cursus, id eleifend odio laoreet." attributes:attributes]; +} + ++ (NSMutableAttributedString *)twoParagraphLatinText +{ + NSDictionary *attributes = @{ + NSFontAttributeName: [UIFont systemFontOfSize:14] + }; + return [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam gravida, metus non tincidunt tincidunt, arcu quam vulputate magna, nec semper libero mi in lorem. Quisque turpis erat, congue sit amet eros at, gravida gravida lacus. Maecenas maximus lectus in efficitur pulvinar. Nam elementum massa eget luctus condimentum. Curabitur egestas mauris urna. Fusce lacus ante, laoreet vitae leo quis, mattis aliquam est. Donec bibendum augue at elit lacinia lobortis. Cras imperdiet ac justo eget sollicitudin. Pellentesque malesuada nec tellus vitae dictum. Proin vestibulum tempus odio in condimentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis vel turpis at velit dignissim rutrum. Nunc lorem felis, molestie eget ornare id, luctus at nunc. Maecenas suscipit nisi sit amet nulla cursus, id eleifend odio laoreet.\n\nPellentesque auctor pulvinar velit, venenatis elementum ex tempus eu. Vestibulum iaculis hendrerit tortor quis sagittis. Pellentesque quam sem, varius ac orci nec, tincidunt ultricies mauris. Aliquam est nunc, eleifend et posuere sed, vestibulum eu elit. Pellentesque pharetra bibendum finibus. Aliquam interdum metus ac feugiat congue. Donec suscipit neque quis mauris volutpat, at molestie tortor aliquam. Aenean posuere nulla a ex posuere finibus. Integer tincidunt quam urna, et vulputate enim tempor sit amet. Nullam ut tellus ac arcu fringilla cursus." attributes:attributes]; +} +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTextNodeSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextNodeSnapshotTests.mm new file mode 100644 index 0000000000..1b21ac35f2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextNodeSnapshotTests.mm @@ -0,0 +1,147 @@ +// +// ASTextNodeSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASSnapshotTestCase.h" +#import + +@interface ASTextNodeSnapshotTests : ASSnapshotTestCase + +@end + +@implementation ASTextNodeSnapshotTests + +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; +} + +- (void)testTextContainerInset +{ + // trivial test case to ensure ASSnapshotTestCase works + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar" + attributes:@{NSFontAttributeName : [UIFont italicSystemFontOfSize:24]}]; + textNode.textContainerInset = UIEdgeInsetsMake(0, 2, 0, 2); + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testTextContainerInsetIsIncludedWithSmallerConstrainedSize +{ + UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + backgroundView.layer.as_allowsHighlightDrawing = YES; + + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar judar judar judar judar judar" + attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }]; + + textNode.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); + + ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 80))]; + textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height); + + [backgroundView addSubview:textNode.view]; + backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50)); + + textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length); + + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; + ASSnapshotVerifyLayer(backgroundView.layer, nil); +} + +- (void)testTextContainerInsetHighlight +{ + UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + backgroundView.layer.as_allowsHighlightDrawing = YES; + + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"yolo" + attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }]; + + textNode.textContainerInset = UIEdgeInsetsMake(5, 10, 10, 5); + ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))]; + textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height); + + [backgroundView addSubview:textNode.view]; + backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50)); + + textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length); + + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; + ASSnapshotVerifyView(backgroundView, nil); +} + +// This test is disabled because the fast-path is disabled. +- (void)DISABLED_testThatFastPathTruncationWorks +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))]; + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testThatSlowPathTruncationWorks +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; + // Set exclusion paths to trigger slow path + textNode.exclusionPaths = @[ [UIBezierPath bezierPath] ]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))); + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testShadowing +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important"]; + textNode.shadowColor = [UIColor blackColor].CGColor; + textNode.shadowOpacity = 0.3; + textNode.shadowRadius = 3; + textNode.shadowOffset = CGSizeMake(0, 1); + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + +/** + * https://github.com/TextureGroup/Texture/issues/822 + */ +- (void)DISABLED_testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(20, 80); + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] initWithString:@"Quality is an important "]; + [mas appendAttributedString:[[NSAttributedString alloc] initWithString:@"thing" attributes:@{ NSBackgroundColorAttributeName : UIColor.yellowColor}]]; + textNode.attributedText = mas; + textNode.truncationMode = NSLineBreakByTruncatingTail; + + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:@{ NSBackgroundColorAttributeName: UIColor.greenColor }]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testFontPointSizeScaling +{ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineHeightMultiple = 0.5; + paragraphStyle.lineSpacing = 2.0; + + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(60, 80); + textNode.pointSizeScaleFactors = @[@0.5]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is an important thing" + attributes:@{ NSParagraphStyleAttributeName: paragraphStyle }]; + + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTextNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextNodeTests.mm new file mode 100644 index 0000000000..dd6ae4c942 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextNodeTests.mm @@ -0,0 +1,324 @@ +// +// ASTextNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import +#import +#import + +#import "ASTestCase.h" + + + +@interface ASTextNodeTestDelegate : NSObject + +@property (nonatomic, copy, readonly) NSString *tappedLinkAttribute; +@property (nonatomic, readonly) id tappedLinkValue; + +@end +@interface ASTextNodeSubclass : ASTextNode +@end +@interface ASTextNodeSecondSubclass : ASTextNodeSubclass +@end + +@implementation ASTextNodeTestDelegate + +- (void)textNode:(ASTextNode *)textNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + _tappedLinkAttribute = attribute; + _tappedLinkValue = value; +} + +- (BOOL)textNode:(ASTextNode *)textNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + return YES; +} + +@end + +@interface ASTextNodeTests : XCTestCase + +@property (nonatomic) ASTextNode *textNode; +@property (nonatomic, copy) NSAttributedString *attributedText; +@property (nonatomic) NSMutableArray *textNodeBucket; + +@end + +@implementation ASTextNodeTests + +- (void)setUp +{ + [super setUp]; + _textNode = [[ASTextNode alloc] init]; + _textNodeBucket = [[NSMutableArray alloc] init]; + + UIFontDescriptor *desc = + [UIFontDescriptor fontDescriptorWithName:@"Didot" size:18]; + NSArray *arr = + @[@{UIFontFeatureTypeIdentifierKey:@(kLetterCaseType), + UIFontFeatureSelectorIdentifierKey:@(kSmallCapsSelector)}]; + desc = + [desc fontDescriptorByAddingAttributes: + @{UIFontDescriptorFeatureSettingsAttribute:arr}]; + UIFont *f = [UIFont fontWithDescriptor:desc size:0]; + NSDictionary *d = @{NSFontAttributeName: f}; + NSMutableAttributedString *mas = + [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." attributes:d]; + NSMutableParagraphStyle *para = [NSMutableParagraphStyle new]; + para.alignment = NSTextAlignmentCenter; + para.lineSpacing = 1.0; + [mas addAttribute:NSParagraphStyleAttributeName value:para + range:NSMakeRange(0, mas.length - 1)]; + + // Vary the linespacing on the last line + NSMutableParagraphStyle *lastLinePara = [NSMutableParagraphStyle new]; + lastLinePara.alignment = para.alignment; + lastLinePara.lineSpacing = 5.0; + [mas addAttribute:NSParagraphStyleAttributeName value:lastLinePara + range:NSMakeRange(mas.length - 1, 1)]; + + _attributedText = mas; + _textNode.attributedText = _attributedText; +} + +#pragma mark - ASTextNode + +- (void)testAllocASTextNode +{ + ASTextNode *node = [[ASTextNode alloc] init]; + XCTAssertTrue([[node class] isSubclassOfClass:[ASTextNode class]], @"ASTextNode alloc should return an instance of ASTextNode, instead returned %@", [node class]); +} + +#pragma mark - ASTextNode + +- (void)testTruncation +{ + XCTAssertTrue([_textNode shouldTruncateForConstrainedSize:ASSizeRangeMake(CGSizeMake(100, 100))], @""); + + _textNode.frame = CGRectMake(0, 0, 100, 100); + XCTAssertTrue(_textNode.isTruncated, @"Text Node should be truncated"); +} + +- (void)testSettingTruncationMessage +{ + NSAttributedString *truncation = [[NSAttributedString alloc] initWithString:@"..." attributes:nil]; + _textNode.truncationAttributedText = truncation; + XCTAssertTrue([_textNode.truncationAttributedText isEqualToAttributedString:truncation], @"Failed to set truncation message"); +} + +- (void)testSettingAdditionalTruncationMessage +{ + NSAttributedString *additionalTruncationMessage = [[NSAttributedString alloc] initWithString:@"read more" attributes:nil]; + _textNode.additionalTruncationMessage = additionalTruncationMessage; + XCTAssertTrue([_textNode.additionalTruncationMessage isEqualToAttributedString:additionalTruncationMessage], @"Failed to set additionalTruncationMessage message"); +} + +- (void)testCalculatedSizeIsGreaterThanOrEqualToConstrainedSize +{ + for (NSInteger i = 10; i < 500; i += 50) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + XCTAssertTrue(calculatedSize.width <= constrainedSize.width, @"Calculated width (%f) should be less than or equal to constrained width (%f)", calculatedSize.width, constrainedSize.width); + XCTAssertTrue(calculatedSize.height <= constrainedSize.height, @"Calculated height (%f) should be less than or equal to constrained height (%f)", calculatedSize.height, constrainedSize.height); + } +} + +- (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedSize +{ + for (NSInteger i = 10; i < 500; i += 50) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + CGSize recalculatedSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 4.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); + } +} + +- (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedFloatingPointSize +{ + for (CGFloat i = 10; i < 500; i *= 1.3) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + CGSize recalculatedSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + XCTAssertTrue(CGSizeEqualToSizeWithIn(calculatedSize, recalculatedSize, 11.0), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); + } +} + +- (void)testMeasureWithZeroSizeAndPlaceholder +{ + _textNode.placeholderEnabled = YES; + + XCTAssertNoThrow([_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeZero)], @"Measure with zero size and placeholder enabled should not throw an exception"); + XCTAssertNoThrow([_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(0, 100))], @"Measure with zero width and placeholder enabled should not throw an exception"); + XCTAssertNoThrow([_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 0))], @"Measure with zero height and placeholder enabled should not throw an exception"); +} + +- (void)testAccessibility +{ + _textNode.attributedText = _attributedText; + XCTAssertTrue(_textNode.isAccessibilityElement, @"Should be an accessibility element"); + XCTAssertTrue(_textNode.accessibilityTraits == UIAccessibilityTraitStaticText, @"Should have static text accessibility trait, instead has %llu", _textNode.accessibilityTraits); + + XCTAssertTrue([_textNode.accessibilityLabel isEqualToString:_attributedText.string], @"Accessibility label is incorrectly set to \n%@\n when it should be \n%@\n", _textNode.accessibilityLabel, _attributedText.string); +} + +- (void)testLinkAttribute +{ + NSString *linkAttributeName = @"MockLinkAttributeName"; + NSString *linkAttributeValue = @"MockLinkAttributeValue"; + NSString *linkString = @"Link"; + NSRange linkRange = NSMakeRange(0, linkString.length); + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:linkString attributes:@{ linkAttributeName : linkAttributeValue}]; + _textNode.attributedText = attributedString; + _textNode.linkAttributeNames = @[linkAttributeName]; + + ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; + _textNode.delegate = delegate; + + ASLayout *layout = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + _textNode.frame = CGRectMake(0, 0, layout.size.width, layout.size.height); + + NSRange returnedLinkRange; + NSString *returnedAttributeName; + NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:CGPointMake(3, 3) attributeName:&returnedAttributeName range:&returnedLinkRange]; + XCTAssertTrue([linkAttributeName isEqualToString:returnedAttributeName], @"Expecting a link attribute name of %@, returned %@", linkAttributeName, returnedAttributeName); + XCTAssertTrue([linkAttributeValue isEqualToString:returnedLinkAttributeValue], @"Expecting a link attribute value of %@, returned %@", linkAttributeValue, returnedLinkAttributeValue); + XCTAssertTrue(NSEqualRanges(linkRange, returnedLinkRange), @"Expected a range of %@, got a link range of %@", NSStringFromRange(linkRange), NSStringFromRange(returnedLinkRange)); +} + +- (void)testTapNotOnALinkAttribute +{ + NSString *linkAttributeName = @"MockLinkAttributeName"; + NSString *linkAttributeValue = @"MockLinkAttributeValue"; + NSString *linkString = @"Link notalink"; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:linkString]; + [attributedString addAttribute:linkAttributeName value:linkAttributeValue range:NSMakeRange(0, 4)]; + _textNode.attributedText = attributedString; + _textNode.linkAttributeNames = @[linkAttributeName]; + + ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; + _textNode.delegate = delegate; + + CGSize calculatedSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))].size; + NSRange returnedLinkRange = NSMakeRange(NSNotFound, 0); + NSRange expectedRange = NSMakeRange(NSNotFound, 0); + NSString *returnedAttributeName; + CGPoint pointNearEndOfString = CGPointMake(calculatedSize.width - 3, calculatedSize.height / 2); + NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:pointNearEndOfString attributeName:&returnedAttributeName range:&returnedLinkRange]; + XCTAssertFalse(returnedAttributeName, @"Expecting no link attribute name, returned %@", returnedAttributeName); + XCTAssertFalse(returnedLinkAttributeValue, @"Expecting no link attribute value, returned %@", returnedLinkAttributeValue); + XCTAssertTrue(NSEqualRanges(expectedRange, returnedLinkRange), @"Expected a range of %@, got a link range of %@", NSStringFromRange(expectedRange), NSStringFromRange(returnedLinkRange)); + + XCTAssertFalse(delegate.tappedLinkAttribute, @"Expected the delegate to be told that %@ was tapped, instead it thinks the tapped attribute is %@", linkAttributeName, delegate.tappedLinkAttribute); + XCTAssertFalse(delegate.tappedLinkValue, @"Expected the delegate to be told that the value %@ was tapped, instead it thinks the tapped attribute value is %@", linkAttributeValue, delegate.tappedLinkValue); +} + +#pragma mark exclusion Paths + +- (void)testSettingExclusionPaths +{ + NSArray *exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(10, 20, 30, 40)]]; + _textNode.exclusionPaths = exclusionPaths; + XCTAssertTrue([_textNode.exclusionPaths isEqualToArray:exclusionPaths], @"Failed to set exclusion paths"); +} + +- (void)testAddingExclusionPathsShouldInvalidateAndIncreaseTheSize +{ + CGSize constrainedSize = CGSizeMake(100, CGFLOAT_MAX); + CGSize sizeWithoutExclusionPaths = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + _textNode.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(50, 20, 30, 40)]]; + CGSize sizeWithExclusionPaths = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + XCTAssertGreaterThan(sizeWithExclusionPaths.height, sizeWithoutExclusionPaths.height, @"Setting exclusions paths should invalidate the calculated size and return a greater size"); +} + +#if AS_ENABLE_TEXTNODE +- (void)testThatTheExperimentWorksCorrectly +{ + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTextNode; + [ASConfigurationManager test_resetWithConfiguration:config]; + + ASTextNode *plainTextNode = [[ASTextNode alloc] init]; + XCTAssertEqualObjects(plainTextNode.class, [ASTextNode2 class]); + + ASTextNodeSecondSubclass *sc2 = [[ASTextNodeSecondSubclass alloc] init]; + XCTAssertEqualObjects([ASTextNodeSubclass superclass], [ASTextNode2 class]); + XCTAssertEqualObjects(sc2.superclass, [ASTextNodeSubclass class]); +} + +- (void)testTextNodeSwitchWorksInMultiThreadEnvironment +{ + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTextNode; + [ASConfigurationManager test_resetWithConfiguration:config]; + XCTestExpectation *exp = [self expectationWithDescription:@"wait for full bucket"]; + + dispatch_queue_t queue = dispatch_queue_create("com.texture.AsyncDisplayKit.ASTextNodeTestsQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_group_t g = dispatch_group_create(); + for (int i = 0; i < 20; i++) { + dispatch_group_async(g, queue, ^{ + ASTextNode *textNode = [[ASTextNodeSecondSubclass alloc] init]; + XCTAssert([textNode isKindOfClass:[ASTextNode2 class]]); + @synchronized(self.textNodeBucket) { + [self.textNodeBucket addObject:textNode]; + if (self.textNodeBucket.count == 20) { + [exp fulfill]; + } + } + }); + } + [self waitForExpectations:@[exp] timeout:3]; + exp = nil; + [self.textNodeBucket removeAllObjects]; +} + +- (void)testTextNodeSwitchWorksInMultiThreadEnvironment2 +{ + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTextNode; + [ASConfigurationManager test_resetWithConfiguration:config]; + XCTestExpectation *exp = [self expectationWithDescription:@"wait for full bucket"]; + + NSLock *lock = [[NSLock alloc] init]; + NSMutableArray *textNodeBucket = [[NSMutableArray alloc] init]; + + dispatch_queue_t queue = dispatch_queue_create("com.texture.AsyncDisplayKit.ASTextNodeTestsQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_group_t g = dispatch_group_create(); + for (int i = 0; i < 20; i++) { + dispatch_group_async(g, queue, ^{ + ASTextNode *textNode = [[ASTextNodeSecondSubclass alloc] init]; + XCTAssert([textNode isKindOfClass:[ASTextNode2 class]]); + [lock lock]; + [textNodeBucket addObject:textNode]; + if (textNodeBucket.count == 20) { + [exp fulfill]; + } + [lock unlock]; + }); + } + [self waitForExpectations:@[exp] timeout:3]; + exp = nil; + [textNodeBucket removeAllObjects]; +} +#endif + +@end + +@implementation ASTextNodeSubclass +@end +@implementation ASTextNodeSecondSubclass +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASTextNodeWordKernerTests.mm b/submodules/AsyncDisplayKit/Tests/ASTextNodeWordKernerTests.mm new file mode 100644 index 0000000000..7b9773574b --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASTextNodeWordKernerTests.mm @@ -0,0 +1,149 @@ +// +// ASTextNodeWordKernerTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +#pragma mark - Tests + +@interface ASTextNodeWordKernerTests : XCTestCase + +@property (nonatomic) ASTextNodeWordKerner *layoutManagerDelegate; +@property (nonatomic) ASTextKitComponents *components; +@property (nonatomic, copy) NSAttributedString *attributedString; + +@end + +@implementation ASTextNodeWordKernerTests + +- (void)setUp +{ + [super setUp]; + _layoutManagerDelegate = [[ASTextNodeWordKerner alloc] init]; + _components.layoutManager.delegate = _layoutManagerDelegate; +} + +- (void)setupTextKitComponentsWithoutWordKerning +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = nil; + NSString *seedString = @"Hello world"; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + _components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:size]; +} + +- (void)setupTextKitComponentsWithWordKerning +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5"}; + NSString *seedString = @"Hello world"; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + _components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:size]; +} + +- (void)setupTextKitComponentsWithWordKerningDifferentFontSizes +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5"}; + NSString *seedString = @" "; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + UIFont *bigFont = [UIFont systemFontOfSize:36]; + UIFont *normalFont = [UIFont systemFontOfSize:12]; + [attributedString addAttribute:NSFontAttributeName value:bigFont range:NSMakeRange(0, 1)]; + [attributedString addAttribute:NSFontAttributeName value:normalFont range:NSMakeRange(1, 1)]; + _components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:size]; +} + +- (void)testSomeGlyphsToChangeIfWordKerning +{ + [self setupTextKitComponentsWithWordKerning]; + + NSInteger glyphsToChange = [self _layoutManagerShouldGenerateGlyphs]; + XCTAssertTrue(glyphsToChange > 0, @"Should have changed the properties on some glyphs"); +} + +- (void)testSpaceBoundingBoxForNoWordKerning +{ + CGSize size = CGSizeMake(200, 200); + UIFont *font = [UIFont systemFontOfSize:12.0]; + NSDictionary *attributes = @{NSFontAttributeName : font}; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@" " attributes:attributes]; + _components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:size]; + CGFloat expectedWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width; + + CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:CGPointZero characterIndex:0]; + + XCTAssertEqualWithAccuracy(boundingBox.size.width, expectedWidth, FLT_EPSILON, @"Word kerning shouldn't alter the default width of %f. Encountered space width was %f", expectedWidth, boundingBox.size.width); +} + +- (void)testSpaceBoundingBoxForWordKerning +{ + CGSize size = CGSizeMake(200, 200); + UIFont *font = [UIFont systemFontOfSize:12]; + + CGFloat kernValue = 0.5; + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @(kernValue), + NSFontAttributeName : font}; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@" " attributes:attributes]; + _components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:size]; + CGFloat expectedWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width + kernValue; + + CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:CGPointZero characterIndex:0]; + XCTAssertEqualWithAccuracy(boundingBox.size.width, expectedWidth, FLT_EPSILON, @"Word kerning shouldn't alter the default width of %f. Encountered space width was %f", expectedWidth, boundingBox.size.width); +} + +- (NSInteger)_layoutManagerShouldGenerateGlyphs +{ + NSRange stringRange = NSMakeRange(0, _components.textStorage.length); + NSRange glyphRange = [_components.layoutManager glyphRangeForCharacterRange:stringRange actualCharacterRange:NULL]; + NSInteger glyphCount = glyphRange.length; + NSUInteger *characterIndexes = (NSUInteger *)malloc(sizeof(NSUInteger) * glyphCount); + for (NSUInteger i=0; i < stringRange.length; i++) { + characterIndexes[i] = i; + } + NSGlyphProperty *glyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount); + CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * glyphCount); + NSInteger glyphsToChange = [_layoutManagerDelegate layoutManager:_components.layoutManager shouldGenerateGlyphs:glyphs properties:glyphProperties characterIndexes:characterIndexes font:[UIFont systemFontOfSize:12.0] forGlyphRange:stringRange]; + free(characterIndexes); + free(glyphProperties); + free(glyphs); + return glyphsToChange; +} + +- (void)testPerCharacterWordKerning +{ + [self setupTextKitComponentsWithWordKerningDifferentFontSizes]; + CGPoint glyphPosition = CGPointZero; + NSUInteger bigSpaceIndex = 0; + NSUInteger normalSpaceIndex = 1; + CGRect bigBoundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:bigSpaceIndex forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:glyphPosition characterIndex:bigSpaceIndex]; + CGRect normalBoundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:normalSpaceIndex forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:glyphPosition characterIndex:normalSpaceIndex]; + XCTAssertTrue(bigBoundingBox.size.width > normalBoundingBox.size.width, @"Unbolded and bolded spaces should have different kerning"); +} + +- (void)testWordKerningDoesNotAlterGlyphOrigin +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5"}; + NSString *seedString = @" "; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + UIFont *normalFont = [UIFont systemFontOfSize:12]; + [attributedString addAttribute:NSFontAttributeName value:normalFont range:NSMakeRange(0, 1)]; + _components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:size]; + + CGPoint glyphPosition = CGPointMake(42, 54); + + CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:glyphPosition characterIndex:0]; + XCTAssertTrue(CGPointEqualToPoint(glyphPosition, boundingBox.origin), @"Word kerning shouldn't alter the origin point of a glyph"); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASThrashUtility.h b/submodules/AsyncDisplayKit/Tests/ASThrashUtility.h new file mode 100644 index 0000000000..09e7847750 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASThrashUtility.h @@ -0,0 +1,111 @@ +// +// Tests/ASThrashUtility.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#define kInitialSectionCount 10 +#define kInitialItemCount 10 +#define kMinimumItemCount 5 +#define kMinimumSectionCount 3 +#define kFickleness 0.1 +#define kThrashingIterationCount 10 + +// Set to 1 to use UITableView and see if the issue still exists. +#define USE_UIKIT_REFERENCE 0 + +#if USE_UIKIT_REFERENCE +#define TableView UITableView +#define CollectionView UICollectionView +#define kCellReuseID @"ASThrashTestCellReuseID" +#else +#define TableView ASTableView +#define CollectionView ASCollectionNode +#endif + +static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; + +@class ASThrashTestSection; +static atomic_uint ASThrashTestItemNextID; +@interface ASThrashTestItem: NSObject +@property (nonatomic, readonly) NSInteger itemID; + ++ (NSMutableArray *)itemsWithCount:(NSInteger)count; + +- (CGFloat)rowHeight; +@end + + +@interface ASThrashTestSection: NSObject +@property (nonatomic, readonly) NSMutableArray *items; +@property (nonatomic, readonly) NSInteger sectionID; + ++ (NSMutableArray *)sectionsWithCount:(NSInteger)count; + +- (instancetype)initWithCount:(NSInteger)count; +- (CGFloat)headerHeight; +@end + +@interface ASThrashDataSource: NSObject +#if USE_UIKIT_REFERENCE + +#else + +#endif + +@property (nonatomic, readonly) UIWindow *window; +@property (nonatomic, readonly) TableView *tableView; +@property (nonatomic, readonly) CollectionView *collectionView; +@property (nonatomic) NSArray *data; +// Only access on main +@property (nonatomic) ASWeakSet *allNodes; + +- (instancetype)initTableViewDataSourceWithData:(NSArray *)data; +- (instancetype)initCollectionViewDataSourceWithData:(NSArray * _Nullable)data; +- (NSPredicate *)predicateForDeallocatedHierarchy; +@end + +@interface NSIndexSet (ASThrashHelpers) +- (NSArray *)indexPathsInSection:(NSInteger)section; +/// `insertMode` means that for each index selected, the max goes up by one. ++ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode; +@end + +#if !USE_UIKIT_REFERENCE +@interface ASThrashTestNode: ASCellNode +@property (nonatomic) ASThrashTestItem *item; +@end +#endif + +@interface ASThrashUpdate : NSObject +@property (nonatomic, readonly) NSArray *oldData; +@property (nonatomic, readonly) NSMutableArray *data; +@property (nonatomic, readonly) NSMutableIndexSet *deletedSectionIndexes; +@property (nonatomic, readonly) NSMutableIndexSet *replacedSectionIndexes; +/// The sections used to replace the replaced sections. +@property (nonatomic, readonly) NSMutableArray *replacingSections; +@property (nonatomic, readonly) NSMutableIndexSet *insertedSectionIndexes; +@property (nonatomic, readonly) NSMutableArray *insertedSections; +@property (nonatomic, readonly) NSMutableArray *deletedItemIndexes; +@property (nonatomic, readonly) NSMutableArray *replacedItemIndexes; +/// The items used to replace the replaced items. +@property (nonatomic, readonly) NSMutableArray *> *replacingItems; +@property (nonatomic, readonly) NSMutableArray *insertedItemIndexes; +@property (nonatomic, readonly) NSMutableArray *> *insertedItems; + +- (instancetype)initWithData:(NSArray *)data; + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; +- (NSString *)base64Representation; +- (NSString *)logFriendlyBase64Representation; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Tests/ASThrashUtility.m b/submodules/AsyncDisplayKit/Tests/ASThrashUtility.m new file mode 100644 index 0000000000..c555d3f776 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASThrashUtility.m @@ -0,0 +1,467 @@ +// +// ASTableViewThrashTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASThrashUtility.h" +#import +#import + +static NSString *ASThrashArrayDescription(NSArray *array) +{ + NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; + NSInteger i = 0; + for (id obj in array) { + [str appendFormat:@"\t[%ld]: \"%@\",\n", (long)i, obj]; + i += 1; + } + [str appendString:@")"]; + return str; +} + +@implementation ASThrashTestItem + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + _itemID = atomic_fetch_add(&ASThrashTestItemNextID, 1); + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) { + _itemID = [aDecoder decodeIntegerForKey:@"itemID"]; + NSAssert(_itemID > 0, @"Failed to decode %@", self); + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInteger:_itemID forKey:@"itemID"]; +} + ++ (NSMutableArray *)itemsWithCount:(NSInteger)count +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; + for (NSInteger i = 0; i < count; i += 1) { + [result addObject:[[ASThrashTestItem alloc] init]]; + } + return result; +} + +- (CGFloat)rowHeight +{ + return (self.itemID % 400) ?: 44; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", (unsigned long)_itemID]; +} + +@end + +static atomic_uint ASThrashTestSectionNextID = 1; +@implementation ASThrashTestSection + +/// Create an array of sections with the given count ++ (NSMutableArray *)sectionsWithCount:(NSInteger)count +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; + for (NSInteger i = 0; i < count; i += 1) { + [result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]]; + } + return result; +} + +- (instancetype)initWithCount:(NSInteger)count +{ + self = [super init]; + if (self != nil) { + _sectionID = atomic_fetch_add(&ASThrashTestSectionNextID, 1); + _items = [ASThrashTestItem itemsWithCount:count]; + } + return self; +} + +- (instancetype)init +{ + return [self initWithCount:0]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) { + _items = [aDecoder decodeObjectOfClass:[NSArray class] forKey:@"items"]; + _sectionID = [aDecoder decodeIntegerForKey:@"sectionID"]; + NSAssert(_sectionID > 0, @"Failed to decode %@", self); + } + return self; +} + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:_items forKey:@"items"]; + [aCoder encodeInteger:_sectionID forKey:@"sectionID"]; +} + +- (CGFloat)headerHeight +{ + return self.sectionID % 400 ?: 44; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"
", (unsigned long)_sectionID, (unsigned long)self.items.count, ASThrashArrayDescription(self.items)]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + ASThrashTestSection *copy = [[ASThrashTestSection alloc] init]; + copy->_sectionID = _sectionID; + copy->_items = [_items mutableCopy]; + return copy; +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[ASThrashTestSection class]]) { + return [(ASThrashTestSection *)object sectionID] == _sectionID; + } else { + return NO; + } +} + +@end + +@implementation NSIndexSet (ASThrashHelpers) + +- (NSArray *)indexPathsInSection:(NSInteger)section +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [result addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; + }]; + return result; +} + +/// `insertMode` means that for each index selected, the max goes up by one. ++ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode +{ + NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; + u_int32_t cutoff = probability * 100; + for (NSInteger i = 0; i < max; i++) { + if (arc4random_uniform(100) < cutoff) { + [indexes addIndex:i]; + if (insertMode) { + max += 1; + } + } + } + return indexes; +} + +@end + +@implementation ASThrashDataSource + +- (instancetype)initTableViewDataSourceWithData:(NSArray *)data +{ + self = [super init]; + if (self != nil) { + _data = [[NSArray alloc] initWithArray:data copyItems:YES]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _tableView = [[TableView alloc] initWithFrame:_window.bounds style:UITableViewStylePlain]; + _allNodes = [[ASWeakSet alloc] init]; + [_window addSubview:_tableView]; +#if USE_UIKIT_REFERENCE + _tableView.dataSource = self; + _tableView.delegate = self; + [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; +#else + _tableView.asyncDelegate = self; + _tableView.asyncDataSource = self; + [_tableView reloadData]; + [_tableView waitUntilAllUpdatesAreCommitted]; +#endif + [_tableView layoutIfNeeded]; + } + return self; +} + +- (instancetype)initCollectionViewDataSourceWithData:(NSArray *)data +{ + self = [super init]; + if (self != nil) { + _data = data != nil ? [[NSArray alloc] initWithArray:data copyItems:YES] : [[NSArray alloc] init]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _collectionView = [[CollectionView alloc] initWithCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init]]; + _allNodes = [[ASWeakSet alloc] init]; + [_window addSubview:_tableView]; + _collectionView.delegate = self; + _collectionView.dataSource = self; +#if USE_UIKIT_REFERENCE + [_collectionView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; +#else + [_collectionView reloadData]; + [_collectionView waitUntilAllUpdatesAreProcessed]; +#endif + [_collectionView layoutIfNeeded]; + } + return self; +} + +- (void)setData:(NSArray *)data +{ + _data = data; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.data[section].items.count; +} + + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return self.data.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return self.data[section].headerHeight; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return self.data[section].items.count; +} + + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ + return self.data.count; +} + +/// Object passed into predicate is ignored. +- (NSPredicate *)predicateForDeallocatedHierarchy +{ + ASWeakSet *allNodes = self.allNodes; + __weak UIWindow *window = _window; + __weak ASTableView *view = _tableView; + return [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + return window == nil && view == nil && allNodes.isEmpty; + }]; +} + +#if USE_UIKIT_REFERENCE + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return [tableView dequeueReusableCellWithIdentifier:kCellReuseID forIndexPath:indexPath]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; + return item.rowHeight; +} + +#else + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; + node.item = self.data[indexPath.section].items[indexPath.row]; + [self.allNodes addObject:node]; + return node; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; + node.item = self.data[indexPath.section].items[indexPath.item]; + [self.allNodes addObject:node]; + return node; +} + +#endif + +@end + +#if !USE_UIKIT_REFERENCE +@implementation ASThrashTestNode + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNodeAssertFalse(isinf(constrainedSize.width)); + return CGSizeMake(constrainedSize.width, 44); +} + +@end +#endif + +@implementation ASThrashUpdate + +- (instancetype)initWithData:(NSArray *)data +{ + self = [super init]; + if (self != nil) { + _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; + _oldData = [[NSArray alloc] initWithArray:data copyItems:YES]; + + _deletedItemIndexes = [NSMutableArray array]; + _replacedItemIndexes = [NSMutableArray array]; + _insertedItemIndexes = [NSMutableArray array]; + _replacingItems = [NSMutableArray array]; + _insertedItems = [NSMutableArray array]; + + // Randomly reload some items + for (ASThrashTestSection *section in _data) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [_replacingItems addObject:newItems]; + [_replacedItemIndexes addObject:indexes]; + } + + // Randomly replace some sections + _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO]; + _replacingSections = [ASThrashTestSection sectionsWithCount:_replacedSectionIndexes.count]; + [_data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; + + // Randomly delete some items + [_data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { + if (section.items.count >= kMinimumItemCount) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; + + /// Cannot reload & delete the same item. + [indexes removeIndexes:_replacedItemIndexes[idx]]; + + [section.items removeObjectsAtIndexes:indexes]; + [_deletedItemIndexes addObject:indexes]; + } else { + [_deletedItemIndexes addObject:[NSMutableIndexSet indexSet]]; + } + }]; + + // Randomly delete some sections + if (_data.count >= kMinimumSectionCount) { + _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO]; + } else { + _deletedSectionIndexes = [NSMutableIndexSet indexSet]; + } + // Cannot replace & delete the same section. + [_deletedSectionIndexes removeIndexes:_replacedSectionIndexes]; + + // Cannot delete/replace item in deleted/replaced section + [_deletedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_replacedItemIndexes[idx] removeAllIndexes]; + [_deletedItemIndexes[idx] removeAllIndexes]; + }]; + [_replacedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_replacedItemIndexes[idx] removeAllIndexes]; + [_deletedItemIndexes[idx] removeAllIndexes]; + }]; + [_data removeObjectsAtIndexes:_deletedSectionIndexes]; + + // Randomly insert some sections + _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(_data.count + 1) probability:kFickleness insertMode:YES]; + _insertedSections = [ASThrashTestSection sectionsWithCount:_insertedSectionIndexes.count]; + [_data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; + + // Randomly insert some items + for (ASThrashTestSection *section in _data) { + // Only insert items into the old sections – not replaced/inserted sections. + if ([_oldData containsObject:section]) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:(section.items.count + 1) probability:kFickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [_insertedItems addObject:newItems]; + [_insertedItemIndexes addObject:indexes]; + } else { + [_insertedItems addObject:@[]]; + [_insertedItemIndexes addObject:[NSMutableIndexSet indexSet]]; + } + } + } + return self; +} + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64 +{ + return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:base64 options:kNilOptions]]; +} + +- (NSString *)base64Representation +{ + return [[NSKeyedArchiver archivedDataWithRootObject:self] base64EncodedStringWithOptions:kNilOptions]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + NSDictionary *dict = [self dictionaryWithValuesForKeys:@[ + @"oldData", + @"data", + @"deletedSectionIndexes", + @"replacedSectionIndexes", + @"replacingSections", + @"insertedSectionIndexes", + @"insertedSections", + @"deletedItemIndexes", + @"replacedItemIndexes", + @"replacingItems", + @"insertedItemIndexes", + @"insertedItems" + ]]; + [aCoder encodeObject:dict forKey:@"_dict"]; + [aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) { + NSAssert(ASThrashUpdateCurrentSerializationVersion == [aDecoder decodeIntegerForKey:@"_version"], @"This thrash update was archived from a different version and can't be read. Sorry."); + NSDictionary *dict = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:@"_dict"]; + [self setValuesForKeysWithDictionary:dict]; + } + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, ASThrashArrayDescription(_oldData), ASThrashArrayDescription(_deletedItemIndexes), _deletedSectionIndexes, ASThrashArrayDescription(_replacedItemIndexes), _replacedSectionIndexes, ASThrashArrayDescription(_insertedItemIndexes), _insertedSectionIndexes, ASThrashArrayDescription(_data)]; +} + +- (NSString *)logFriendlyBase64Representation +{ + return [NSString stringWithFormat:@"\n\n**********\nBase64 Representation:\n**********\n%@\n**********\nEnd Base64 Representation\n**********", self.base64Representation]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASUICollectionViewTests.mm b/submodules/AsyncDisplayKit/Tests/ASUICollectionViewTests.mm new file mode 100644 index 0000000000..6f99590a84 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASUICollectionViewTests.mm @@ -0,0 +1,141 @@ +// +// ASUICollectionViewTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import "NSInvocation+ASTestHelpers.h" + +@interface ASUICollectionViewTests : XCTestCase + +@end + +@implementation ASUICollectionViewTests + +/// Test normal item-affiliated supplementary node +- (void)testNormalTwoIndexSupplementaryElement +{ + [self _testSupplementaryNodeAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] sectionCount:2 expectException:NO]; +} + +/// If your supp is indexPathForItem:inSection:, the section index must be in bounds +- (void)testThatSupplementariesWithItemIndexesMustBeWithinNormalSections +{ + [self _testSupplementaryNodeAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:3] sectionCount:2 expectException:YES]; +} + +/// If your supp is indexPathWithIndex:, that's OK even if that section is out of bounds! +- (void)testThatSupplementariesWithOneIndexAreOKOutOfSectionBounds +{ + [self _testSupplementaryNodeAtIndexPath:[NSIndexPath indexPathWithIndex:3] sectionCount:2 expectException:NO]; +} + +- (void)testThatNestedBatchCompletionsAreCalledInOrder +{ + UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init]; + id layoutMock = [OCMockObject partialMockForObject:layout]; + + UICollectionView *cv = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layoutMock]; + id dataSource = [OCMockObject niceMockForProtocol:@protocol(UICollectionViewDataSource)]; + + cv.dataSource = dataSource; + + XCTestExpectation *inner0 = [self expectationWithDescription:@"Inner completion 0 is called"]; + XCTestExpectation *inner1 = [self expectationWithDescription:@"Inner completion 1 is called"]; + XCTestExpectation *outer = [self expectationWithDescription:@"Outer completion is called"]; + + NSMutableArray *completions = [NSMutableArray array]; + + [cv performBatchUpdates:^{ + [cv performBatchUpdates:^{ + + } completion:^(BOOL finished) { + [completions addObject:inner0]; + [inner0 fulfill]; + }]; + [cv performBatchUpdates:^{ + + } completion:^(BOOL finished) { + [completions addObject:inner1]; + [inner1 fulfill]; + }]; + } completion:^(BOOL finished) { + [completions addObject:outer]; + [outer fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + XCTAssertEqualObjects(completions, (@[ outer, inner0, inner1 ]), @"Expected completion order to be correct"); +} + +- (void)_testSupplementaryNodeAtIndexPath:(NSIndexPath *)indexPath sectionCount:(NSInteger)sectionCount expectException:(BOOL)shouldFail +{ + UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:@"SuppKind" withIndexPath:indexPath]; + attr.frame = CGRectMake(0, 0, 20, 20); + UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init]; + id layoutMock = [OCMockObject partialMockForObject:layout]; + + [[[[layoutMock expect] ignoringNonObjectArgs] andReturn:@[ attr ]] layoutAttributesForElementsInRect:CGRectZero]; + UICollectionView *cv = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layoutMock]; + [cv registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:@"SuppKind" withReuseIdentifier:@"ReuseID"]; + + id dataSource = [OCMockObject niceMockForProtocol:@protocol(UICollectionViewDataSource)]; + __block id view = nil; + [[[dataSource expect] andDo:^(NSInvocation *invocation) { + NSIndexPath *indexPath = [invocation as_argumentAtIndexAsObject:4]; + view = [cv dequeueReusableSupplementaryViewOfKind:@"SuppKind" withReuseIdentifier:@"ReuseID" forIndexPath:indexPath]; + [invocation setReturnValue:&view]; + }] collectionView:cv viewForSupplementaryElementOfKind:@"SuppKind" atIndexPath:indexPath]; + [[[dataSource expect] andReturnValue:[NSNumber numberWithInteger:sectionCount]] numberOfSectionsInCollectionView:cv]; + + cv.dataSource = dataSource; + if (shouldFail) { + XCTAssertThrowsSpecificNamed([cv layoutIfNeeded], NSException, NSInternalInconsistencyException); + // Early return because behavior after exception is thrown is undefined. + return; + } + + [cv layoutIfNeeded]; + XCTAssertEqualObjects(attr, [cv layoutAttributesForSupplementaryElementOfKind:@"SuppKind" atIndexPath:indexPath]); + XCTAssertEqual(view, [cv supplementaryViewForElementKind:@"SuppKind" atIndexPath:indexPath]); + [dataSource verify]; + [layoutMock verify]; +} + +- (void)testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + UICollectionView *cv = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; + id dataSource = [OCMockObject niceMockForProtocol:@protocol(UICollectionViewDataSource)]; + + // Setup empty data source – 0 sections, 0 items + [[[dataSource stub] andDo:^(NSInvocation *invocation) { + NSIndexPath *indexPath = [invocation as_argumentAtIndexAsObject:3]; + __autoreleasing UICollectionViewCell *view = [cv dequeueReusableCellWithReuseIdentifier:@"CellID" forIndexPath:indexPath]; + [invocation setReturnValue:&view]; + }] collectionView:cv cellForItemAtIndexPath:OCMOCK_ANY]; + [[[dataSource stub] andReturnValue:[NSNumber numberWithInteger:0]] numberOfSectionsInCollectionView:cv]; + [[[dataSource stub] andReturnValue:[NSNumber numberWithInteger:0]] collectionView:cv numberOfItemsInSection:0]; + [cv registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CellID"]; + cv.dataSource = dataSource; + + // Update data source – 1 section, 0 items + [[[dataSource stub] andReturnValue:[NSNumber numberWithInteger:1]] numberOfSectionsInCollectionView:cv]; + + /** + * Inform collection view – insert section 0 + * Throws exception because collection view never saw the data source have 0 sections. + * so it's going to read "oldSectionCount" now and get 1. It will also read + * "newSectionCount" and get 1. Then it'll throw because "oldSectionCount(1) + insertedCount(1) != newSectionCount(1)". + * To workaround this, you could add `[cv numberOfSections]` before the data source is updated to + * trigger the collection view to read oldSectionCount=0. + */ + XCTAssertThrowsSpecificNamed([cv insertSections:[NSIndexSet indexSetWithIndex:0]], NSException, NSInternalInconsistencyException); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASVideoNodeTests.mm b/submodules/AsyncDisplayKit/Tests/ASVideoNodeTests.mm new file mode 100644 index 0000000000..60c7f89c42 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASVideoNodeTests.mm @@ -0,0 +1,428 @@ +// +// ASVideoNodeTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import + +#import + +#import "ASDisplayNodeTestsHelper.h" + +#if AS_USE_VIDEO +@interface ASVideoNodeTests : XCTestCase +{ + ASVideoNode *_videoNode; + AVURLAsset *_firstAsset; + AVAsset *_secondAsset; + NSURL *_url; + NSArray *_requestedKeys; +} +@end + +@interface ASVideoNode () { + ASDisplayNode *_playerNode; + AVPlayer *_player; +} + + +@property ASInterfaceState interfaceState; +@property (readonly) ASDisplayNode *spinner; +@property ASDisplayNode *playerNode; +@property AVPlayer *player; +@property BOOL shouldBePlaying; + +- (void)setVideoPlaceholderImage:(UIImage *)image; +- (void)prepareToPlayAsset:(AVAsset *)asset withKeys:(NSArray *)requestedKeys; + +@end + +@implementation ASVideoNodeTests + +- (void)setUp +{ + _videoNode = [[ASVideoNode alloc] init]; + _firstAsset = [AVURLAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; + _secondAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; + _url = [NSURL URLWithString:@"testURL"]; + _requestedKeys = @[ @"playable" ]; +} + +- (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode +{ + _videoNode.asset = _firstAsset; + [self doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl]; +} + +- (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl]; +} + +- (void)doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl +{ + _videoNode.interfaceState = ASInterfaceStatePreload; + [_videoNode play]; +} + + +- (void)testOnPauseSpinnerIsPausedIfPresent +{ + _videoNode.asset = _firstAsset; + [self doOnPauseSpinnerIsPausedIfPresentWithURL]; +} + +- (void)testOnPauseSpinnerIsPausedIfPresentWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doOnPauseSpinnerIsPausedIfPresentWithURL]; +} + +- (void)doOnPauseSpinnerIsPausedIfPresentWithURL +{ + _videoNode.interfaceState = ASInterfaceStatePreload; + + [_videoNode play]; + [_videoNode pause]; + +} + + +- (void)testOnVideoReadySpinnerIsStoppedAndRemoved +{ + _videoNode.asset = _firstAsset; + [self doOnVideoReadySpinnerIsStoppedAndRemovedWithURL]; +} + +- (void)testOnVideoReadySpinnerIsStoppedAndRemovedWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doOnVideoReadySpinnerIsStoppedAndRemovedWithURL]; +} + +- (void)doOnVideoReadySpinnerIsStoppedAndRemovedWithURL +{ + _videoNode.interfaceState = ASInterfaceStatePreload; + + [_videoNode play]; + [_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{NSKeyValueChangeNewKey : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; +} + + +- (void)testPlayerDefaultsToNil +{ + _videoNode.asset = _firstAsset; + XCTAssertNil(_videoNode.player); +} + +- (void)testPlayerDefaultsToNilWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + XCTAssertNil(_videoNode.player); +} + +- (void)testPlayerIsCreatedAsynchronouslyInPreload +{ + AVAsset *asset = _firstAsset; + + id assetMock = [OCMockObject partialMockForObject:asset]; + id videoNodeMock = [OCMockObject partialMockForObject:_videoNode]; + + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + + _videoNode.asset = assetMock; + _videoNode.interfaceState = ASInterfaceStatePreload; + + [videoNodeMock verifyWithDelay:1.0f]; + + XCTAssertNotNil(_videoNode.player); +} + +- (void)testPlayerIsCreatedAsynchronouslyInPreloadWithURL +{ + AVAsset *asset = [AVAsset assetWithURL:_url]; + + id assetMock = [OCMockObject partialMockForObject:asset]; + id videoNodeMock = [OCMockObject partialMockForObject:_videoNode]; + + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + + _videoNode.asset = assetMock; + _videoNode.interfaceState = ASInterfaceStatePreload; + + [videoNodeMock verifyWithDelay:1.0f]; + + XCTAssertNotNil(_videoNode.player); +} + +- (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlaying +{ + _videoNode.asset = _firstAsset; + [self doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL]; +} + +- (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL]; +} + +- (void)doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL +{ + [_videoNode setInterfaceState:ASInterfaceStateNone]; + [_videoNode didLoad]; + + XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]); +} + + +- (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying +{ + _videoNode.asset = _firstAsset; + [self doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying]; +} + +- (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlayingWithUrl +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying]; +} + +- (void)doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying +{ + [_videoNode pause]; + [_videoNode layer]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay]; + + XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]); +} + + +- (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay +{ + _videoNode.asset = _firstAsset; + [self doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay]; +} + +- (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplayWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay]; +} + +- (void)doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay +{ + _videoNode.shouldAutoplay = YES; + _videoNode.playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ + AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; + return playerLayer; + }]; + _videoNode.playerNode.layer.frame = CGRectZero; + + [_videoNode layer]; + [_videoNode didEnterVisibleState]; + + XCTAssertTrue(_videoNode.shouldBePlaying); +} + +- (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater +{ + _videoNode.asset = _firstAsset; + [self doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater]; +} + +- (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLaterWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater]; +} + +- (void)doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater +{ + [_videoNode play]; + + [_videoNode interfaceStateDidChange:ASInterfaceStateNone fromState:ASInterfaceStateVisible]; + + XCTAssertFalse(_videoNode.isPlaying); + XCTAssertTrue(_videoNode.shouldBePlaying); +} + + +- (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack +{ + _videoNode.asset = _firstAsset; + [self doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack]; +} + +- (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBackWithURL +{ + _videoNode.asset = [AVAsset assetWithURL:_url]; + [self doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack]; +} + +- (void)doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack +{ + [_videoNode play]; + + [_videoNode interfaceStateDidChange:ASInterfaceStateVisible fromState:ASInterfaceStateNone]; + [_videoNode interfaceStateDidChange:ASInterfaceStateNone fromState:ASInterfaceStateVisible]; + + XCTAssertTrue(_videoNode.shouldBePlaying); +} + +- (void)testMutingShouldMutePlayer +{ + [_videoNode setPlayer:[[AVPlayer alloc] init]]; + + _videoNode.muted = YES; + + XCTAssertTrue(_videoNode.player.muted); +} + +- (void)testUnMutingShouldUnMutePlayer +{ + [_videoNode setPlayer:[[AVPlayer alloc] init]]; + + _videoNode.muted = YES; + _videoNode.muted = NO; + + XCTAssertFalse(_videoNode.player.muted); +} + +- (void)testVideoThatDoesNotAutorepeatsShouldPauseOnPlaybackEnd +{ + id assetMock = [OCMockObject partialMockForObject:_firstAsset]; + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + + _videoNode.asset = assetMock; + _videoNode.shouldAutorepeat = NO; + + [_videoNode layer]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStatePreload]; + [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + [_videoNode play]; + + XCTAssertTrue(_videoNode.isPlaying); + + [[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem]; + + XCTAssertFalse(_videoNode.isPlaying); + XCTAssertEqual(0, CMTimeGetSeconds(_videoNode.player.currentTime)); +} + +- (void)testVideoThatAutorepeatsShouldRepeatOnPlaybackEnd +{ + id assetMock = [OCMockObject partialMockForObject:_firstAsset]; + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + + _videoNode.asset = assetMock; + _videoNode.shouldAutorepeat = YES; + + [_videoNode layer]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStatePreload]; + [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + [_videoNode play]; + + [[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem]; + + XCTAssertTrue(_videoNode.isPlaying); +} + +- (void)testVideoResumedWhenBufferIsLikelyToKeepUp +{ + id assetMock = [OCMockObject partialMockForObject:_firstAsset]; + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + + _videoNode.asset = assetMock; + + [_videoNode layer]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStatePreload]; + [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + ASCATransactionQueueWait(nil); + [_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.contentMode); + + _videoNode.gravity = AVLayerVideoGravityResize; + XCTAssertEqual(UIViewContentModeScaleToFill, _videoNode.contentMode); + + _videoNode.gravity = AVLayerVideoGravityResizeAspect; + XCTAssertEqual(UIViewContentModeScaleAspectFit, _videoNode.contentMode); + + _videoNode.gravity = AVLayerVideoGravityResizeAspectFill; + XCTAssertEqual(UIViewContentModeScaleAspectFill, _videoNode.contentMode); +} + +- (void)testChangingAssetsChangesPlaceholderImage +{ + UIImage *firstImage = [[UIImage alloc] init]; + + _videoNode.asset = _firstAsset; + [_videoNode setVideoPlaceholderImage:firstImage]; + XCTAssertEqual(firstImage, _videoNode.image); + + _videoNode.asset = _secondAsset; + XCTAssertNotEqual(firstImage, _videoNode.image); +} + +- (void)testClearingPreloadedContentShouldClearAssetData +{ + AVAsset *asset = _firstAsset; + + id assetMock = [OCMockObject partialMockForObject:asset]; + id videoNodeMock = [OCMockObject partialMockForObject:_videoNode]; + + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + + _videoNode.asset = assetMock; + [_videoNode didEnterPreloadState]; + [_videoNode setVideoPlaceholderImage:[[UIImage alloc] init]]; + + [videoNodeMock verifyWithDelay:1.0f]; + + XCTAssertNotNil(_videoNode.player); + XCTAssertNotNil(_videoNode.currentItem); + XCTAssertNotNil(_videoNode.image); + + [_videoNode didExitPreloadState]; + XCTAssertNil(_videoNode.player); + XCTAssertNil(_videoNode.currentItem); +} + +- (void)testDelegateProperlySetForClassHierarchy +{ + _videoNode.delegate = self; + + XCTAssertTrue([_videoNode.delegate conformsToProtocol:@protocol(ASVideoNodeDelegate)]); + XCTAssertTrue([_videoNode.delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]); + XCTAssertTrue([((ASNetworkImageNode*)_videoNode).delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]); + + XCTAssertEqual(_videoNode.delegate, self); + XCTAssertEqual(((ASNetworkImageNode*)_videoNode).delegate, self); +} + +@end + +#endif \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Tests/ASViewControllerTests.mm b/submodules/AsyncDisplayKit/Tests/ASViewControllerTests.mm new file mode 100644 index 0000000000..f6b26e9756 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASViewControllerTests.mm @@ -0,0 +1,88 @@ +// +// ASViewControllerTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#import + +#import "NSInvocation+ASTestHelpers.h" + +@interface ASViewControllerTests : XCTestCase + +@end + +@implementation ASViewControllerTests + +- (void)testThatAutomaticSubnodeManagementScrollViewInsetsAreApplied +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + ASScrollNode *scrollNode = [[ASScrollNode alloc] init]; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange constrainedSize){ + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:scrollNode]; + }; + ASViewController *vc = [[ASViewController alloc] initWithNode:node]; + window.rootViewController = [[UINavigationController alloc] initWithRootViewController:vc]; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); + XCTAssertNotEqual(scrollNode.view.contentInset.top, 0); +} + +- (void)testThatViewControllerFrameIsRightAfterCustomTransitionWithNonextendedEdges +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + ASViewController *vc = [[ASViewController alloc] initWithNode:node]; + vc.node.backgroundColor = [UIColor greenColor]; + vc.edgesForExtendedLayout = UIRectEdgeNone; + + UIViewController * oldVC = [[UIViewController alloc] init]; + UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:oldVC]; + id navDelegate = [OCMockObject niceMockForProtocol:@protocol(UINavigationControllerDelegate)]; + id animator = [OCMockObject niceMockForProtocol:@protocol(UIViewControllerAnimatedTransitioning)]; + [[[[navDelegate expect] ignoringNonObjectArgs] andReturn:animator] navigationController:[OCMArg any] animationControllerForOperation:UINavigationControllerOperationPush fromViewController:[OCMArg any] toViewController:[OCMArg any]]; + [[[animator expect] andReturnValue:@0.3] transitionDuration:[OCMArg any]]; + XCTestExpectation *e = [self expectationWithDescription:@"Transition completed"]; + [[[animator expect] andDo:^(NSInvocation *invocation) { + id ctx = [invocation as_argumentAtIndexAsObject:2]; + UIView *container = [ctx containerView]; + [container addSubview:vc.view]; + vc.view.alpha = 0; + vc.view.frame = [ctx finalFrameForViewController:vc]; + [UIView animateWithDuration:0.3 animations:^{ + vc.view.alpha = 1; + oldVC.view.alpha = 0; + } completion:^(BOOL finished) { + [oldVC.view removeFromSuperview]; + [ctx completeTransition:finished]; + [e fulfill]; + }]; + }] animateTransition:[OCMArg any]]; + nav.delegate = navDelegate; + window.rootViewController = nav; + [window makeKeyAndVisible]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]]; + [nav pushViewController:vc animated:YES]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; + + CGFloat navHeight = CGRectGetMaxY([nav.navigationBar convertRect:nav.navigationBar.bounds toView:window]); + CGRect expectedRect, slice; + CGRectDivide(window.bounds, &slice, &expectedRect, navHeight, CGRectMinYEdge); + XCTAssertEqualObjects(NSStringFromCGRect(expectedRect), NSStringFromCGRect(node.frame)); + [navDelegate verify]; + [animator verify]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASWeakMapTests.mm b/submodules/AsyncDisplayKit/Tests/ASWeakMapTests.mm new file mode 100644 index 0000000000..fd0c0835be --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASWeakMapTests.mm @@ -0,0 +1,54 @@ +// +// ASWeakMapTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASWeakMapTests : XCTestCase + +@end + +@implementation ASWeakMapTests + +- (void)testKeyAndValueAreReleasedWhenEntryIsReleased +{ + ASWeakMap *weakMap = [[ASWeakMap alloc] init]; + + __weak NSObject *weakKey; + __weak NSObject *weakValue; + @autoreleasepool { + NSObject *key = [[NSObject alloc] init]; + NSObject *value = [[NSObject alloc] init]; + ASWeakMapEntry *entry = [weakMap setObject:value forKey:key]; + XCTAssertEqual([weakMap entryForKey:key], entry); + + weakKey = key; + weakValue = value; +} + XCTAssertNil(weakKey); + XCTAssertNil(weakValue); +} + +- (void)testKeyEquality +{ + ASWeakMap *weakMap = [[ASWeakMap alloc] init]; + NSString *keyA = @"key"; + NSString *keyB = [keyA copy]; // `isEqual` but not pointer equal + NSObject *value = [[NSObject alloc] init]; + + ASWeakMapEntry *entryA = [weakMap setObject:value forKey:keyA]; + ASWeakMapEntry *entryB = [weakMap entryForKey:keyB]; + XCTAssertEqual(entryA, entryB); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Tests/ASWeakSetTests.mm b/submodules/AsyncDisplayKit/Tests/ASWeakSetTests.mm new file mode 100644 index 0000000000..31af91de0f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASWeakSetTests.mm @@ -0,0 +1,135 @@ +// +// ASWeakSetTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASWeakSetTests : XCTestCase + +@end + +@implementation ASWeakSetTests + +- (void)testAddingACoupleRetainedObjects +{ + ASWeakSet *weakSet = [ASWeakSet new]; + NSString *hello = @"hello"; + NSString *world = @"hello"; + [weakSet addObject:hello]; + [weakSet addObject:world]; + XCTAssert([weakSet containsObject:hello]); + XCTAssert([weakSet containsObject:world]); + XCTAssert(![weakSet containsObject:@"apple"]); +} + +- (void)testThatCountIncorporatesDeallocatedObjects +{ + ASWeakSet *weakSet = [ASWeakSet new]; + XCTAssertEqual(weakSet.count, 0); + NSObject *a = [NSObject new]; + NSObject *b = [NSObject new]; + [weakSet addObject:a]; + [weakSet addObject:b]; + XCTAssertEqual(weakSet.count, 2); + + @autoreleasepool { + NSObject *doomedObject = [NSObject new]; + [weakSet addObject:doomedObject]; + XCTAssertEqual(weakSet.count, 3); + } + + XCTAssertEqual(weakSet.count, 2); +} + +- (void)testThatIsEmptyIncorporatesDeallocatedObjects +{ + ASWeakSet *weakSet = [ASWeakSet new]; + XCTAssertTrue(weakSet.isEmpty); + @autoreleasepool { + NSObject *doomedObject = [NSObject new]; + [weakSet addObject:doomedObject]; + XCTAssertFalse(weakSet.isEmpty); + } + XCTAssertTrue(weakSet.isEmpty); +} + +- (void)testThatContainsObjectWorks +{ + ASWeakSet *weakSet = [ASWeakSet new]; + NSObject *a = [NSObject new]; + NSObject *b = [NSObject new]; + [weakSet addObject:a]; + XCTAssertTrue([weakSet containsObject:a]); + XCTAssertFalse([weakSet containsObject:b]); +} + +- (void)testThatRemoveObjectWorks +{ + ASWeakSet *weakSet = [ASWeakSet new]; + NSObject *a = [NSObject new]; + NSObject *b = [NSObject new]; + [weakSet addObject:a]; + [weakSet addObject:b]; + XCTAssertTrue([weakSet containsObject:a]); + XCTAssertTrue([weakSet containsObject:b]); + XCTAssertEqual(weakSet.count, 2); + + [weakSet removeObject:b]; + XCTAssertTrue([weakSet containsObject:a]); + XCTAssertFalse([weakSet containsObject:b]); + XCTAssertEqual(weakSet.count, 1); +} + +- (void)testThatFastEnumerationWorks +{ + ASWeakSet *weakSet = [ASWeakSet new]; + NSObject *a = [NSObject new]; + NSObject *b = [NSObject new]; + [weakSet addObject:a]; + [weakSet addObject:b]; + + @autoreleasepool { + NSObject *doomedObject = [NSObject new]; + [weakSet addObject:doomedObject]; + XCTAssertEqual(weakSet.count, 3); + } + + NSInteger i = 0; + NSMutableSet *awaitingObjects = [NSMutableSet setWithObjects:a, b, nil]; + for (NSObject *object in weakSet) { + XCTAssertTrue([awaitingObjects containsObject:object]); + [awaitingObjects removeObject:object]; + i += 1; + } + + XCTAssertEqual(i, 2); +} + +- (void)testThatRemoveAllObjectsWorks +{ + ASWeakSet *weakSet = [ASWeakSet new]; + NSObject *a = [NSObject new]; + NSObject *b = [NSObject new]; + [weakSet addObject:a]; + [weakSet addObject:b]; + XCTAssertEqual(weakSet.count, 2); + + [weakSet removeAllObjects]; + + XCTAssertEqual(weakSet.count, 0); + + NSInteger i = 0; + for (__unused NSObject *object in weakSet) { + i += 1; + } + + XCTAssertEqual(i, 0); +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ASWrapperSpecSnapshotTests.mm b/submodules/AsyncDisplayKit/Tests/ASWrapperSpecSnapshotTests.mm new file mode 100644 index 0000000000..91ddd45fe5 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ASWrapperSpecSnapshotTests.mm @@ -0,0 +1,52 @@ +// +// ASWrapperSpecSnapshotTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import "ASLayoutSpecSnapshotTestsHelper.h" +#import + +@interface ASWrapperSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASWrapperSpecSnapshotTests + +- (void)testWrapperSpecWithOneElementShouldSizeToElement +{ + ASDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}); + + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)); + [self testWithChildren:@[child] sizeRange:sizeRange identifier:nil]; +} + +- (void)testWrapperSpecWithMultipleElementsShouldSizeToLargestElement +{ + ASDisplayNode *firstChild = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50}); + ASDisplayNode *secondChild = ASDisplayNodeWithBackgroundColor([UIColor greenColor], {100, 100}); + + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)); + [self testWithChildren:@[secondChild, firstChild] sizeRange:sizeRange identifier:nil]; +} + +- (void)testWithChildren:(NSArray *)children sizeRange:(ASSizeRange)sizeRange identifier:(NSString *)identifier +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); + + NSMutableArray *subnodes = [NSMutableArray arrayWithArray:children]; + [subnodes insertObject:backgroundNode atIndex:0]; + + ASLayoutSpec *layoutSpec = + [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild: + [ASWrapperLayoutSpec + wrapperWithLayoutElements:children] + background:backgroundNode]; + + [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/ArrayDiffingTests.mm b/submodules/AsyncDisplayKit/Tests/ArrayDiffingTests.mm new file mode 100644 index 0000000000..5151449d79 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/ArrayDiffingTests.mm @@ -0,0 +1,311 @@ +// +// ArrayDiffingTests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +@interface NSArray (ArrayDiffingTests) +- (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison; +@end + +@interface ArrayDiffingTests : XCTestCase + +@end + +@implementation ArrayDiffingTests + +- (void)testDiffingCommonIndexes +{ + NSArray *tests = @[ + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice", @"dave", @"gary"], + @[@0, @1, @2] + ], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"gary", @"dave"], + @[@0, @2] + ], + @[ + @[@"bob", @"alice"], + @[@"gary", @"dave"], + @[], + ], + @[ + @[@"bob", @"alice", @"dave"], + @[], + @[], + ], + @[ + @[], + @[@"bob", @"alice", @"dave"], + @[], + ], + ]; + + for (NSArray *test in tests) { + NSIndexSet *indexSet = [test[0] _asdk_commonIndexesWithArray:test[1] compareBlock:^BOOL(id lhs, id rhs) { + return [lhs isEqual:rhs]; + }]; + NSMutableIndexSet *mutableIndexSet = [indexSet mutableCopy]; + + for (NSNumber *index in (NSArray *)test[2]) { + XCTAssert([indexSet containsIndex:[index integerValue]]); + [mutableIndexSet removeIndex:[index integerValue]]; + } + + XCTAssert([mutableIndexSet count] == 0, @"Unaccounted deletions: %@", mutableIndexSet); + } +} + +- (void)testDiffingInsertionsAndDeletions { + NSArray *tests = @[ + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice", @"dave", @"gary"], + @[@3], + @[], + ], + @[ + @[@"a", @"b", @"c", @"d"], + @[@"d", @"c", @"b", @"a"], + @[@1, @2, @3], + @[@0, @1, @2], + ], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"gary", @"alice", @"dave"], + @[@1], + @[], + ], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice"], + @[], + @[@2], + ], + @[ + @[@"bob", @"alice", @"dave"], + @[], + @[], + @[@0, @1, @2], + ], + @[ + @[@"bob", @"alice", @"dave"], + @[@"gary", @"alice", @"dave", @"jack"], + @[@0, @3], + @[@0], + ], + @[ + @[@"bob", @"alice", @"dave", @"judy", @"lynda", @"tony"], + @[@"gary", @"bob", @"suzy", @"tony"], + @[@0, @2], + @[@1, @2, @3, @4], + ], + @[ + @[@"bob", @"alice", @"dave", @"judy"], + @[@"judy", @"dave", @"alice", @"bob"], + @[@1, @2, @3], + @[@0, @1, @2], + ], + ]; + + long n = 0; + for (NSArray *test in tests) { + NSIndexSet *insertions, *deletions; + [test[0] asdk_diffWithArray:test[1] insertions:&insertions deletions:&deletions]; + NSMutableIndexSet *mutableInsertions = [insertions mutableCopy]; + NSMutableIndexSet *mutableDeletions = [deletions mutableCopy]; + + for (NSNumber *index in (NSArray *)test[2]) { + XCTAssert([mutableInsertions containsIndex:[index integerValue]], @"Test #%ld: insertions %@ does not contain %@", + n, insertions, index); + [mutableInsertions removeIndex:[index integerValue]]; + } + for (NSNumber *index in (NSArray *)test[3]) { + XCTAssert([mutableDeletions containsIndex:[index integerValue]], @"Test #%ld: deletions %@ does not contain %@", + n, deletions, index + ); + [mutableDeletions removeIndex:[index integerValue]]; + } + + XCTAssert([mutableInsertions count] == 0, @"Test #%ld: Unaccounted insertions: %@", n, mutableInsertions); + XCTAssert([mutableDeletions count] == 0, @"Test #%ld: Unaccounted deletions: %@", n, mutableDeletions); + n++; + } +} + +- (void)testDiffingInsertsDeletesAndMoves +{ + NSArray *tests = @[ + @[ + @[@"a", @"b"], + @[@"b", @"a"], + @[], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]) {1, 0} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]) {0, 1} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice", @"dave", @"gary"], + @[@3], + @[], + @[]], + @[ + @[@"a", @"b", @"c", @"d"], + @[@"d", @"c", @"b", @"a"], + @[], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]){3, 0} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){2, 1} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){1, 2} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){0, 3} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"gary", @"dave", @"alice"], + @[@1], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]) {1, 3} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice"], + @[], + @[@2], + @[]], + @[ + @[@"bob", @"alice", @"dave"], + @[], + @[], + @[@0, @1, @2], + @[]], + @[ + @[@"bob", @"alice", @"dave"], + @[@"gary", @"alice", @"dave", @"jack"], + @[@0, @3], + @[@0], + @[]], + @[ + @[@"bob", @"alice", @"dave", @"judy", @"lynda", @"tony"], + @[@"gary", @"bob", @"suzy", @"tony"], + @[@0, @2], + @[@1, @2, @3, @4], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0, 1} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){5, 3} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave", @"judy"], + @[@"judy", @"dave", @"alice", @"bob"], + @[], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]){3, 0} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){2, 1} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){1, 2} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){0, 3} length:2] + ]] + + ]; + + long n = 0; + for (NSArray *test in tests) { + NSIndexSet *insertions, *deletions; + NSArray *moves; + [test[0] asdk_diffWithArray:test[1] insertions:&insertions deletions:&deletions moves:&moves]; + NSMutableIndexSet *mutableInsertions = [insertions mutableCopy]; + NSMutableIndexSet *mutableDeletions = [deletions mutableCopy]; + + for (NSNumber *index in (NSArray *) test[2]) { + XCTAssert([mutableInsertions containsIndex:[index integerValue]], @"Test #%ld, insertions does not contain %ld", + n, (long)[index integerValue]); + [mutableInsertions removeIndex:(NSUInteger) [index integerValue]]; + } + for (NSNumber *index in (NSArray *) test[3]) { + XCTAssert([mutableDeletions containsIndex:[index integerValue]], @"Test #%ld, deletions does not contain %ld", + n, (long)[index integerValue]); + [mutableDeletions removeIndex:(NSUInteger) [index integerValue]]; + } + + XCTAssert([mutableInsertions count] == 0, @"Test #%ld, Unaccounted insertions: %@", n, mutableInsertions); + XCTAssert([mutableDeletions count] == 0, @"Test #%ld, Unaccounted deletions: %@", n, mutableDeletions); + + XCTAssert([moves isEqual:test[4]], @"Test #%ld, %@ !isEqual: %@", n, moves, test[4]); + n++; + } +} + +- (void)testArrayDiffingRebuildingWithRandomElements +{ + NSArray *original = @[]; + NSArray *pending = @[]; + + NSIndexSet *insertions = nil; + NSIndexSet *deletions = nil; + NSArray *moves; + + for (int testNumber = 0; testNumber <= 25; testNumber++) { + int len = arc4random_uniform(10); + for (int j = 0; j < len; j++) { + original = [original arrayByAddingObject:@(arc4random_uniform(25))]; + } + len = arc4random_uniform(10); + for (int j = 0; j < len; j++) { + pending = [pending arrayByAddingObject:@(arc4random_uniform(25))]; + } + // Some sequences that presented issues in the past: + if (testNumber == 0) { + original = @[@20, @11, @14, @2, @14, @5, @4, @18, @0]; + pending = @[@9, @18, @18, @19, @20, @18, @22, @10, @3]; + } + if (testNumber == 1) { + original = @[@5, @9, @21, @11, @5, @9, @8]; + pending = @[@2, @12, @17, @19, @9, @1, @8, @5, @21]; + } + if (testNumber == 2) { + original = @[@14, @14, @12, @8, @20, @4, @0, @10]; + pending = @[@14]; + } + + [original asdk_diffWithArray:pending insertions:&insertions deletions:&deletions moves:&moves]; + + NSMutableArray *deletionsList = [NSMutableArray new]; + [deletions enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + [deletionsList addObject:@(idx)]; + }]; + NSMutableArray *insertionsList = [NSMutableArray new]; + [insertions enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + [insertionsList addObject:@(idx)]; + }]; + + NSUInteger i = 0; + NSUInteger j = 0; + NSMutableArray *test = [NSMutableArray new]; + for (NSUInteger count = 0; count < [pending count]; count++) { + if (i < [insertionsList count] && [insertionsList[i] unsignedIntegerValue] == count) { + [test addObject:pending[[insertionsList[i] unsignedIntegerValue]]]; + i++; + } else if (j < [moves count] && [moves[j] indexAtPosition:1] == count) { + [test addObject:original[[moves[j] indexAtPosition:0]]]; + j++; + } else { + [test addObject:original[count]]; + } + } + + XCTAssert([test isEqualToArray:pending], @"Did not mutate to expected new array:\n [%@] -> [%@], actual: [%@]\ninsertions: %@\nmoves: %@\ndeletions: %@", + [original componentsJoinedByString:@","], [pending componentsJoinedByString:@","], [test componentsJoinedByString:@","], + insertions, moves, deletions); + original = @[]; + pending = @[]; + } +} +@end diff --git a/submodules/AsyncDisplayKit/Tests/AsyncDisplayKitTests-Info.plist b/submodules/AsyncDisplayKit/Tests/AsyncDisplayKitTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/AsyncDisplayKitTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AsyncDisplayKit/Tests/AsyncDisplayKitTests-Prefix.pch b/submodules/AsyncDisplayKit/Tests/AsyncDisplayKitTests-Prefix.pch new file mode 100644 index 0000000000..625be4d28b --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/AsyncDisplayKitTests-Prefix.pch @@ -0,0 +1,9 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/submodules/AsyncDisplayKit/Tests/Common/ASDisplayNode+OCMock.mm b/submodules/AsyncDisplayKit/Tests/Common/ASDisplayNode+OCMock.mm new file mode 100644 index 0000000000..8776fd7bde --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/ASDisplayNode+OCMock.mm @@ -0,0 +1,27 @@ +// +// ASDisplayNode+OCMock.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * For some reason, when creating partial mocks of nodes, OCMock fails to find + * these class methods that it swizzled! + */ +@implementation ASDisplayNode (OCMock) + ++ (Class)ocmock_replaced_viewClass +{ + return [_ASDisplayView class]; +} + ++ (Class)ocmock_replaced_layerClass +{ + return [_ASDisplayLayer class]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/Common/ASTestCase.h b/submodules/AsyncDisplayKit/Tests/Common/ASTestCase.h new file mode 100644 index 0000000000..72fcd259f9 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/ASTestCase.h @@ -0,0 +1,27 @@ +// +// ASTestCase.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +// Not strictly necessary, but convenient +#import + +#import + +#import "OCMockObject+ASAdditions.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ASTestCase : XCTestCase + +@property (class, nonatomic, nullable, readonly) ASTestCase *currentTestCase; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Tests/Common/ASTestCase.mm b/submodules/AsyncDisplayKit/Tests/Common/ASTestCase.mm new file mode 100644 index 0000000000..30b42bde4c --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/ASTestCase.mm @@ -0,0 +1,109 @@ +// +// ASTestCase.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import +#import +#import +#import "OCMockObject+ASAdditions.h" + +static __weak ASTestCase *currentTestCase; + +@implementation ASTestCase { + ASWeakSet *registeredMockObjects; +} + +- (void)setUp +{ + [super setUp]; + currentTestCase = self; + registeredMockObjects = [ASWeakSet new]; +} + +- (void)tearDown +{ + [ASConfigurationManager test_resetWithConfiguration:nil]; + + // Clear out all application windows. Note: the system will retain these sometimes on its + // own but we'll do our best. + for (UIWindow *window in [UIApplication sharedApplication].windows) { + [window resignKeyWindow]; + window.hidden = YES; + window.rootViewController = nil; + for (UIView *view in window.subviews) { + [view removeFromSuperview]; + } + } + + // Set nil for all our subclasses' ivars. Use setValue:forKey: so memory is managed correctly. + // This is important to do _inside_ the test-perform, so that we catch any issues caused by the + // deallocation, and so that we're inside the @autoreleasepool for the test invocation. + Class c = [self class]; + while (c != [ASTestCase class]) { + unsigned int ivarCount; + Ivar *ivars = class_copyIvarList(c, &ivarCount); + for (unsigned int i = 0; i < ivarCount; i++) { + Ivar ivar = ivars[i]; + NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding]; + if (OCMIsObjectType(ivar_getTypeEncoding(ivar))) { + [self setValue:nil forKey:key]; + } + } + if (ivars) { + free(ivars); + } + + c = [c superclass]; + } + + for (OCMockObject *mockObject in registeredMockObjects) { + OCMVerifyAll(mockObject); + [mockObject stopMocking]; + + // Invocations retain arguments, which may cause retain cycles. + // Manually clear them all out. + NSMutableArray *invocations = object_getIvar(mockObject, class_getInstanceVariable(OCMockObject.class, "invocations")); + [invocations removeAllObjects]; + } + + // Go ahead and spin the run loop before finishing, so the system + // unregisters/cleans up whatever possible. + [NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantPast]; + + [super tearDown]; +} + +- (void)invokeTest +{ + // This will call setup, run, then teardown. + @autoreleasepool { + [super invokeTest]; + } + + // Now that the autorelease pool is drained, drain the dealloc queue also. + [[ASDeallocQueue sharedDeallocationQueue] drain]; +} + ++ (ASTestCase *)currentTestCase +{ + return currentTestCase; +} + +@end + +@implementation ASTestCase (OCMockObjectRegistering) + +- (void)registerMockObject:(id)mockObject +{ + @synchronized (registeredMockObjects) { + [registeredMockObjects addObject:mockObject]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/Common/ASXCTExtensions.h b/submodules/AsyncDisplayKit/Tests/Common/ASXCTExtensions.h new file mode 100644 index 0000000000..2d882f922a --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/ASXCTExtensions.h @@ -0,0 +1,36 @@ +// +// ASXCTExtensions.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define ASXCTAssertEqualSizes(s0, s1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromCGSize(s0), @#s0, NSStringFromCGSize(s1), @#s1, __VA_ARGS__) + +#define ASXCTAssertNotEqualSizes(s0, s1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromCGSize(s0), @#s0, NSStringFromCGSize(s1), @#s1, __VA_ARGS__) + +#define ASXCTAssertEqualPoints(p0, p1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromCGPoint(p0), @#p0, NSStringFromCGPoint(p1), @#p1, __VA_ARGS__) + +#define ASXCTAssertNotEqualPoints(p0, p1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromCGPoint(p0), @#p0, NSStringFromCGPoint(p1), @#p1, __VA_ARGS__) + +#define ASXCTAssertEqualRects(r0, r1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromCGRect(r0), @#r0, NSStringFromCGRect(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertNotEqualRects(r0, r1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromCGRect(r0), @#r0, NSStringFromCGRect(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertEqualDimensions(r0, r1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromASDimension(r0), @#r0, NSStringFromASDimension(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertNotEqualDimensions(r0, r1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromASDimension(r0), @#r0, NSStringFromASDimension(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertEqualSizeRanges(r0, r1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromASSizeRange(r0), @#r0, NSStringFromASSizeRange(r1), @#r1, __VA_ARGS__) diff --git a/submodules/AsyncDisplayKit/Tests/Common/NSInvocation+ASTestHelpers.h b/submodules/AsyncDisplayKit/Tests/Common/NSInvocation+ASTestHelpers.h new file mode 100644 index 0000000000..3248cdd283 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/NSInvocation+ASTestHelpers.h @@ -0,0 +1,32 @@ +// +// NSInvocation+ASTestHelpers.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSInvocation (ASTestHelpers) + +/** + * Formats the argument at the given index as an object and returns it. + * + * Currently only supports arguments that are themselves objects, but handles + * getting the argument into ARC safely. + */ +- (nullable id)as_argumentAtIndexAsObject:(NSInteger)index; + +/** + * Sets the return value, simulating ARC behavior. + * + * Currently only supports invocations whose return values are already object types. + */ +- (void)as_setReturnValueWithObject:(nullable id)object; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Tests/Common/NSInvocation+ASTestHelpers.mm b/submodules/AsyncDisplayKit/Tests/Common/NSInvocation+ASTestHelpers.mm new file mode 100644 index 0000000000..dc5a4279f7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/NSInvocation+ASTestHelpers.mm @@ -0,0 +1,32 @@ +// +// NSInvocation+ASTestHelpers.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "NSInvocation+ASTestHelpers.h" + +@implementation NSInvocation (ASTestHelpers) + +- (id)as_argumentAtIndexAsObject:(NSInteger)index +{ + void *buf; + [self getArgument:&buf atIndex:index]; + return (__bridge id)buf; +} + +- (void)as_setReturnValueWithObject:(id)object +{ + if (object == nil) { + const void *fixedBuf = NULL; + [self setReturnValue:&fixedBuf]; + } else { + // Retain, then autorelease. + const void *fixedBuf = CFAutorelease((__bridge_retained void *)object); + [self setReturnValue:&fixedBuf]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Tests/Common/OCMockObject+ASAdditions.h b/submodules/AsyncDisplayKit/Tests/Common/OCMockObject+ASAdditions.h new file mode 100644 index 0000000000..2ad99e676e --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/OCMockObject+ASAdditions.h @@ -0,0 +1,56 @@ +// +// OCMockObject+ASAdditions.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface OCMockObject (ASAdditions) + +/** + * NOTE: All OCMockObjects created during an ASTestCase call OCMVerifyAll during -tearDown. + */ + +/** + * A method to manually specify which optional protocol methods should return YES + * from -respondsToSelector:. + * + * If you don't call this method, the default OCMock behavior is to + * "implement" all optional protocol methods, which makes it impossible to + * test scenarios where only a subset of optional protocol methods are implemented. + * + * You should only call this on protocol mocks. + */ +- (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... NS_REQUIRES_NIL_TERMINATION; + +/// An optional block to modify description text. Only used in OCClassMockObject currently. +@property NSString *(^modifyDescriptionBlock)(OCMockObject *object, NSString *baseDescription); + +@end + +/** + * Additional stub recorders useful in ASDK. + */ +@interface OCMStubRecorder (ASProperties) + +/** + * Add a debug-break side effect to this stub/expectation. + * + * You will usually need to jump to frame 12 "fr s 12" + */ +#define andDebugBreak() _andDebugBreak() +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDebugBreak)(void); + +#define ignoringNonObjectArgs() _ignoringNonObjectArgs() +@property (nonatomic, readonly) OCMStubRecorder *(^ _ignoringNonObjectArgs)(void); + +#define onMainThread() _onMainThread() +@property (nonatomic, readonly) OCMStubRecorder *(^ _onMainThread)(void); + +#define offMainThread() _offMainThread() +@property (nonatomic, readonly) OCMStubRecorder *(^ _offMainThread)(void); + +@end diff --git a/submodules/AsyncDisplayKit/Tests/Common/OCMockObject+ASAdditions.mm b/submodules/AsyncDisplayKit/Tests/Common/OCMockObject+ASAdditions.mm new file mode 100644 index 0000000000..e6d3f711d3 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/OCMockObject+ASAdditions.mm @@ -0,0 +1,237 @@ +// +// OCMockObject+ASAdditions.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OCMockObject+ASAdditions.h" + +#import +#import +#import "ASTestCase.h" +#import +#import "debugbreak.h" + +@interface ASTestCase (OCMockObjectRegistering) + +- (void)registerMockObject:(id)mockObject; + +@end + +@implementation OCMockObject (ASAdditions) + ++ (void)load +{ + // [OCProtocolMockObject respondsToSelector:] <-> [(self) swizzled_protocolMockRespondsToSelector:] + Method orig = class_getInstanceMethod(OCMockObject.protocolMockObjectClass, @selector(respondsToSelector:)); + Method newMethod = class_getInstanceMethod(self, @selector(swizzled_protocolMockRespondsToSelector:)); + method_exchangeImplementations(orig, newMethod); + + // init <-> swizzled_init + { + Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init)); + Method newInit = class_getInstanceMethod(self, @selector(swizzled_init)); + method_exchangeImplementations(origInit, newInit); + } + + // (class mock) description <-> swizzled_classMockDescription + { + Method orig = class_getInstanceMethod(OCMockObject.classMockObjectClass, @selector(description)); + Method newMethod = class_getInstanceMethod(self, @selector(swizzled_classMockDescription)); + method_exchangeImplementations(orig, newMethod); + } +} + +/// Since OCProtocolMockObject is private, use this method to get the class. ++ (Class)protocolMockObjectClass +{ + static Class c; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + c = NSClassFromString(@"OCProtocolMockObject"); + NSAssert(c != Nil, nil); + }); + return c; +} + +/// Since OCClassMockObject is private, use this method to get the class. ++ (Class)classMockObjectClass +{ + static Class c; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + c = NSClassFromString(@"OCClassMockObject"); + NSAssert(c != Nil, nil); + }); + return c; +} + +/// Whether the user has opted-in to specify which optional methods are implemented for this object. +- (BOOL)hasSpecifiedOptionalProtocolMethods +{ + return objc_getAssociatedObject(self, @selector(optionalImplementedMethods)) != nil; +} + +/// The optional protocol selectors the user has added via -addImplementedOptionalProtocolMethods: +- (NSMutableSet *)optionalImplementedMethods +{ + NSMutableSet *result = objc_getAssociatedObject(self, _cmd); + if (result == nil) { + result = [NSMutableSet set]; + objc_setAssociatedObject(self, _cmd, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return result; +} + +- (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... +{ + // Can't use isKindOfClass: since we're a proxy. + NSAssert(object_getClass(self) == OCMockObject.protocolMockObjectClass, @"Cannot call this method on non-protocol mocks."); + NSMutableSet *methods = self.optionalImplementedMethods; + + // First arg is not returned by va_arg, needs to be handled separately. + if (aSelector != NULL) { + [methods addObject:NSStringFromSelector(aSelector)]; + } + + va_list args; + va_start(args, aSelector); + SEL s; + while((s = va_arg(args, SEL))) + { + [methods addObject:NSStringFromSelector(s)]; + } + va_end(args); +} + +- (BOOL)implementsOptionalProtocolMethod:(SEL)aSelector +{ + NSAssert(self.hasSpecifiedOptionalProtocolMethods, @"Shouldn't call this method if the user hasn't opted-in to specifying optional protocol methods."); + + // Check our collection first. It'll be in here if they explicitly marked the method as implemented. + for (NSString *str in self.optionalImplementedMethods) { + if (sel_isEqual(NSSelectorFromString(str), aSelector)) { + return YES; + } + } + + // If they didn't explicitly mark it implemented, check if they stubbed/expected it. That counts too, but + // we still want them to have the option to declare that the method exists without + // stubbing it or making an expectation, so the rest of OCMock's mechanisms work as expected. + return [self handleSelector:aSelector]; +} + +- (BOOL)swizzled_protocolMockRespondsToSelector:(SEL)aSelector +{ + // Can't use isKindOfClass: since we're a proxy. + NSAssert(object_getClass(self) == OCMockObject.protocolMockObjectClass, @"Swizzled method should only ever be called for protocol mocks."); + + // If they haven't called our public method to opt-in, use the default behavior. + if (!self.hasSpecifiedOptionalProtocolMethods) { + return [self swizzled_protocolMockRespondsToSelector:aSelector]; + } + + Ivar i = class_getInstanceVariable([self class], "mockedProtocol"); + NSAssert(i != NULL, nil); + Protocol *mockedProtocol = object_getIvar(self, i); + NSAssert(mockedProtocol != NULL, nil); + + // Check if it's an optional protocol method. If not, just return the default implementation (which has now swapped). + struct objc_method_description methodDescription; + methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, NO, YES); + if (methodDescription.name == NULL) { + methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, NO, NO); + if (methodDescription.name == NULL) { + return [self swizzled_protocolMockRespondsToSelector:aSelector]; + } + } + + // It's an optional instance or class method. Override the return value. + return [self implementsOptionalProtocolMethod:aSelector]; +} + +// Whenever a mock object is initted, register it with the current test case +// so that it gets verified and its invocations are cleared during -tearDown. +- (instancetype)swizzled_init +{ + [self swizzled_init]; + [ASTestCase.currentTestCase registerMockObject:self]; + return self; +} + +- (NSString *)swizzled_classMockDescription +{ + NSString *orig = [self swizzled_classMockDescription]; + __auto_type block = self.modifyDescriptionBlock; + if (block) { + return block(self, orig); + } + return orig; +} + +- (void)setModifyDescriptionBlock:(NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock +{ + objc_setAssociatedObject(self, @selector(modifyDescriptionBlock), modifyDescriptionBlock, OBJC_ASSOCIATION_COPY); +} + +- (NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock +{ + return objc_getAssociatedObject(self, _cmd); +} + +@end + +@implementation OCMStubRecorder (ASProperties) + +@dynamic _ignoringNonObjectArgs; + +- (OCMStubRecorder *(^)(void))_ignoringNonObjectArgs +{ + id (^theBlock)(void) = ^ () + { + return [self ignoringNonObjectArgs]; + }; + return theBlock; +} + +@dynamic _onMainThread; + +- (OCMStubRecorder *(^)(void))_onMainThread +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + ASDisplayNodeAssertMainThread(); + }]; + }; + return theBlock; +} + +@dynamic _offMainThread; + +- (OCMStubRecorder *(^)(void))_offMainThread +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + ASDisplayNodeAssertNotMainThread(); + }]; + }; + return theBlock; +} + +@dynamic _andDebugBreak; + +- (OCMStubRecorder *(^)(void))_andDebugBreak +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + debug_break(); + }]; + }; + return theBlock; +} +@end diff --git a/submodules/AsyncDisplayKit/Tests/Common/debugbreak.h b/submodules/AsyncDisplayKit/Tests/Common/debugbreak.h new file mode 100644 index 0000000000..96f1e9e59a --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/Common/debugbreak.h @@ -0,0 +1,142 @@ +// +// debugbreak.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +/* Copyright (c) 2011-2015, Scott Tsai + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef DEBUG_BREAK_H +#define DEBUG_BREAK_H + +#ifdef _MSC_VER + +#define debug_break __debugbreak + +#else + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + /* gcc optimizers consider code after __builtin_trap() dead. + * Making __builtin_trap() unsuitable for breaking into the debugger */ + DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP = 0, +}; + +#if defined(__i386__) || defined(__x86_64__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + __asm__ volatile("int $0x03"); +} +#elif defined(__thumb__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +/* FIXME: handle __THUMB_INTERWORK__ */ +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'arm-linux-tdep.c' in GDB source. + * Both instruction sequences below work. */ +#if 1 + /* 'eabi_linux_thumb_le_breakpoint' */ + __asm__ volatile(".inst 0xde01"); +#else + /* 'eabi_linux_thumb2_le_breakpoint' */ + __asm__ volatile(".inst.w 0xf7f0a000"); +#endif + + /* Known problem: + * After a breakpoint hit, can't stepi, step, or continue in GDB. + * 'step' stuck on the same instruction. + * + * Workaround: a new GDB command, + * 'debugbreak-step' is defined in debugbreak-gdb.py + * that does: + * (gdb) set $instruction_len = 2 + * (gdb) tbreak *($pc + $instruction_len) + * (gdb) jump *($pc + $instruction_len) + */ +} +#elif defined(__arm__) && !defined(__thumb__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'arm-linux-tdep.c' in GDB source, + * 'eabi_linux_arm_le_breakpoint' */ + __asm__ volatile(".inst 0xe7f001f0"); + /* Has same known problem and workaround + * as Thumb mode */ +} +#elif defined(__aarch64__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'aarch64-tdep.c' in GDB source, + * 'aarch64_default_breakpoint' */ + __asm__ volatile(".inst 0xd4200000"); +} +#else +enum { HAVE_TRAP_INSTRUCTION = 0, }; +#endif + +__attribute__((gnu_inline, always_inline)) +__inline__ static void debug_break(void) +{ + if (HAVE_TRAP_INSTRUCTION) { + trap_instruction(); + } else if (DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP) { + /* raises SIGILL on Linux x86{,-64}, to continue in gdb: + * (gdb) handle SIGILL stop nopass + * */ + __builtin_trap(); + } else { + #ifdef _WIN32 + /* SIGTRAP available only on POSIX-compliant operating systems + * use builtin trap instead */ + __builtin_trap(); + #else + raise(SIGTRAP); + #endif + } +} + +#ifdef __cplusplus +} +#endif + +#endif + +#endif diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testChildrenMeasuredWithAutoMaxSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testChildrenMeasuredWithAutoMaxSize@2x.png new file mode 100644 index 0000000000..2b60cf2bcf Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testChildrenMeasuredWithAutoMaxSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_overflowChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_overflowChildren@2x.png new file mode 100644 index 0000000000..1687dc24f6 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_overflowChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_underflowChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_underflowChildren@2x.png new file mode 100644 index 0000000000..2233813979 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_underflowChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_wrappedChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_wrappedChildren@2x.png new file mode 100644 index 0000000000..3d76853c01 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASAbsoluteLayoutSpecSnapshotTests/testSizingBehaviour_wrappedChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASBackgroundLayoutSpecSnapshotTests/testBackground@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASBackgroundLayoutSpecSnapshotTests/testBackground@2x.png new file mode 100644 index 0000000000..198e5394cc Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASBackgroundLayoutSpecSnapshotTests/testBackground@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testMinimumSizeRangeIsGivenToChildWhenNotCentering@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testMinimumSizeRangeIsGivenToChildWhenNotCentering@2x.png new file mode 100644 index 0000000000..02717f8fdb Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testMinimumSizeRangeIsGivenToChildWhenNotCentering@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions@2x.png new file mode 100644 index 0000000000..50cb613c24 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringX@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringX@2x.png new file mode 100644 index 0000000000..69e7392384 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringX@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringXCenteringY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringXCenteringY@2x.png new file mode 100644 index 0000000000..311ef9ed32 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringXCenteringY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringY@2x.png new file mode 100644 index 0000000000..28036afad5 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithOptions_CenteringY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions@2x.png new file mode 100644 index 0000000000..50cb613c24 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumX@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumX@2x.png new file mode 100644 index 0000000000..270b15feb6 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumX@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumXSizingMinimumY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumXSizingMinimumY@2x.png new file mode 100644 index 0000000000..7fddbff94e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumXSizingMinimumY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumY@2x.png new file mode 100644 index 0000000000..b14c267b42 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASCenterLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASDisplayNodeSnapshotTests/testBasicHierarchySnapshotTesting@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASDisplayNodeSnapshotTests/testBasicHierarchySnapshotTesting@2x.png new file mode 100644 index 0000000000..e5f40f70fd Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASDisplayNodeSnapshotTests/testBasicHierarchySnapshotTesting@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testForcedScaling_first@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testForcedScaling_first@2x.png new file mode 100644 index 0000000000..896dc7abf0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testForcedScaling_first@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testForcedScaling_second@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testForcedScaling_second@2x.png new file mode 100644 index 0000000000..79d4c747d8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testForcedScaling_second@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testRenderLogoSquare@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testRenderLogoSquare@2x.png new file mode 100644 index 0000000000..896dc7abf0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testRenderLogoSquare@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testRoundedCornerBlock@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testRoundedCornerBlock@2x.png new file mode 100644 index 0000000000..b4c17dd803 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testRoundedCornerBlock@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testTintColorBlock@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testTintColorBlock@2x.png new file mode 100644 index 0000000000..0cc74697a6 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testTintColorBlock@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-10-10@2x.png new file mode 100644 index 0000000000..ec0a4cf3c7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-10-inf@2x.png new file mode 100644 index 0000000000..fb4775c0f5 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-inf-10@2x.png new file mode 100644 index 0000000000..993d4c0592 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-inf-inf@2x.png new file mode 100644 index 0000000000..d0b2cbd912 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-10-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-10-10@2x.png new file mode 100644 index 0000000000..537326b3db Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-10-inf@2x.png new file mode 100644 index 0000000000..b38c9ccb1c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-inf-10@2x.png new file mode 100644 index 0000000000..797e7ac410 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-inf-inf@2x.png new file mode 100644 index 0000000000..8c88149380 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_10-inf-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-10-10@2x.png new file mode 100644 index 0000000000..f9decf0282 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-10-inf@2x.png new file mode 100644 index 0000000000..e3efdda0a1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-inf-10@2x.png new file mode 100644 index 0000000000..f494863bc7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-inf-inf@2x.png new file mode 100644 index 0000000000..5653f7f6fc Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-10-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-10-10@2x.png new file mode 100644 index 0000000000..5fa79d8a92 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-10-inf@2x.png new file mode 100644 index 0000000000..bece9190a6 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-inf-10@2x.png new file mode 100644 index 0000000000..e8132c50b9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-inf-inf@2x.png new file mode 100644 index 0000000000..6da1b998c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithFixedSize_inf-inf-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-0-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-0-0@2x.png new file mode 100644 index 0000000000..5104ab98be Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-0-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-0-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-0-inf@2x.png new file mode 100644 index 0000000000..8ac22fa361 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-0-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-inf-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-inf-0@2x.png new file mode 100644 index 0000000000..2054fac7ea Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-inf-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-inf-inf@2x.png new file mode 100644 index 0000000000..d09eb720c8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-0-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-0-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-0-0@2x.png new file mode 100644 index 0000000000..8fe02d548d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-0-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-0-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-0-inf@2x.png new file mode 100644 index 0000000000..21f8ad6169 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-0-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-inf-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-inf-0@2x.png new file mode 100644 index 0000000000..a77ce6a0c9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-inf-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-inf-inf@2x.png new file mode 100644 index 0000000000..c4e1791769 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_0-inf-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-0-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-0-0@2x.png new file mode 100644 index 0000000000..19f457874e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-0-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-0-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-0-inf@2x.png new file mode 100644 index 0000000000..81a07b2570 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-0-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-inf-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-inf-0@2x.png new file mode 100644 index 0000000000..31422ea5d7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-inf-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-inf-inf@2x.png new file mode 100644 index 0000000000..63cff77a6f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-0-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-0-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-0-0@2x.png new file mode 100644 index 0000000000..1c55fe57ed Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-0-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-0-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-0-inf@2x.png new file mode 100644 index 0000000000..5d0bfee89e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-0-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-inf-0@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-inf-0@2x.png new file mode 100644 index 0000000000..2b6430c5b5 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-inf-0@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-inf-inf@2x.png new file mode 100644 index 0000000000..6da1b998c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithInfinityAndZeroInsetValue_inf-inf-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-10-10@2x.png new file mode 100644 index 0000000000..c9255d62e8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-10-inf@2x.png new file mode 100644 index 0000000000..db62499472 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-inf-10@2x.png new file mode 100644 index 0000000000..2b052c4e38 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-inf-inf@2x.png new file mode 100644 index 0000000000..d0b2cbd912 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-10-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-10-10@2x.png new file mode 100644 index 0000000000..69368ee671 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-10-inf@2x.png new file mode 100644 index 0000000000..55efcf5dba Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-inf-10@2x.png new file mode 100644 index 0000000000..797e7ac410 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-inf-inf@2x.png new file mode 100644 index 0000000000..8c88149380 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_10-inf-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-10-10@2x.png new file mode 100644 index 0000000000..01411e3f2c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-10-inf@2x.png new file mode 100644 index 0000000000..e3efdda0a1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-inf-10@2x.png new file mode 100644 index 0000000000..f0cd235628 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-inf-inf@2x.png new file mode 100644 index 0000000000..5653f7f6fc Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-10-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-10-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-10-10@2x.png new file mode 100644 index 0000000000..5fa79d8a92 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-10-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-10-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-10-inf@2x.png new file mode 100644 index 0000000000..bece9190a6 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-10-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-inf-10@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-inf-10@2x.png new file mode 100644 index 0000000000..e8132c50b9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-inf-10@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-inf-inf@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-inf-inf@2x.png new file mode 100644 index 0000000000..6da1b998c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASInsetLayoutSpecSnapshotTests/testInsetsWithVariableSize_inf-inf-inf-inf@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASOverlayLayoutSpecSnapshotTests/testOverlay@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASOverlayLayoutSpecSnapshotTests/testOverlay@2x.png new file mode 100644 index 0000000000..198e5394cc Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASOverlayLayoutSpecSnapshotTests/testOverlay@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_DoubleRatio@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_DoubleRatio@2x.png new file mode 100644 index 0000000000..a10ef96975 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_DoubleRatio@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_HalfRatio@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_HalfRatio@2x.png new file mode 100644 index 0000000000..0cbc831393 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_HalfRatio@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_SevenTimesRatio@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_SevenTimesRatio@2x.png new file mode 100644 index 0000000000..48be1e63a7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_SevenTimesRatio@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_TenTimesRatioWithItemTooBig@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_TenTimesRatioWithItemTooBig@2x.png new file mode 100644 index 0000000000..ce593f7ad7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRatioLayoutSpecSnapshotTests/testRatioLayout_TenTimesRatioWithItemTooBig@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testMinimumSizeRangeIsGivenToChildWhenNotPositioning@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testMinimumSizeRangeIsGivenToChildWhenNotPositioning@2x.png new file mode 100644 index 0000000000..02717f8fdb Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testMinimumSizeRangeIsGivenToChildWhenNotPositioning@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions@2x.png new file mode 100644 index 0000000000..1192d72e8d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterX@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterX@2x.png new file mode 100644 index 0000000000..e2c7e8266b Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterX@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterXCenterY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterXCenterY@2x.png new file mode 100644 index 0000000000..311ef9ed32 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterXCenterY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterXEndY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterXEndY@2x.png new file mode 100644 index 0000000000..385fc3e817 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterXEndY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterY@2x.png new file mode 100644 index 0000000000..94db1b131f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_CenterY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndX@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndX@2x.png new file mode 100644 index 0000000000..e464619571 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndX@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndXCenterY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndXCenterY@2x.png new file mode 100644 index 0000000000..04ee2d6115 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndXCenterY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndXEndY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndXEndY@2x.png new file mode 100644 index 0000000000..3d9d16b5d2 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndXEndY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndY@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndY@2x.png new file mode 100644 index 0000000000..8ba7270db2 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithOptions_EndY@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions@2x.png new file mode 100644 index 0000000000..1192d72e8d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumHeight@2x.png new file mode 100644 index 0000000000..467bb6fa8c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumWidth@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumWidth@2x.png new file mode 100644 index 0000000000..8e9c66caa1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumWidth@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumWidthSizingMinimumHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumWidthSizingMinimumHeight@2x.png new file mode 100644 index 0000000000..ddafb41db7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASRelativeLayoutSpecSnapshotTests/testWithSizingOptions_SizingMinimumWidthSizingMinimumHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignCenterWithFlexedMainDimension@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignCenterWithFlexedMainDimension@2x.png new file mode 100644 index 0000000000..ea22b081a8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignCenterWithFlexedMainDimension@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignCenterWithIndefiniteCrossDimension@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignCenterWithIndefiniteCrossDimension@2x.png new file mode 100644 index 0000000000..00b3bfb369 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignCenterWithIndefiniteCrossDimension@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..188cd6007e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..18f614cf47 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..188cd6007e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..20d303b586 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png new file mode 100644 index 0000000000..20d303b586 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..27b26426db Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..740100fce2 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..40bae9ef39 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..a60f63f145 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000..a89a941bf1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png new file mode 100644 index 0000000000..461c786922 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..032e9fac3a Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..6d08d18e4a Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..032e9fac3a Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..4df2a81184 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png new file mode 100644 index 0000000000..4df2a81184 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png new file mode 100644 index 0000000000..51d060062c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..fea7203a26 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..9e0bdad429 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..bb608622fd Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..17b8dbfbc4 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000..ab969dd638 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png new file mode 100644 index 0000000000..584aca77db Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png new file mode 100644 index 0000000000..12936781c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedCenter@2x.png new file mode 100644 index 0000000000..46b73e6a6e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedEnd@2x.png new file mode 100644 index 0000000000..d2131dd83f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStart@2x.png new file mode 100644 index 0000000000..5f9dc090e0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStretchNoChildExceedsMin@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStretchNoChildExceedsMin@2x.png new file mode 100644 index 0000000000..537b1f6f13 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStretchNoChildExceedsMin@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStretchOneChildExceedsMin@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStretchOneChildExceedsMin@2x.png new file mode 100644 index 0000000000..8fbd29f3b5 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignedStretchOneChildExceedsMin@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithSpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithSpaceBetween@2x.png new file mode 100644 index 0000000000..5174b92e27 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithSpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithStretchedItem@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithStretchedItem@2x.png new file mode 100644 index 0000000000..f3e20dbe00 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithStretchedItem@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineFirst@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineFirst@2x.png new file mode 100644 index 0000000000..e45ab35c68 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineFirst@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineLast@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineLast@2x.png new file mode 100644 index 0000000000..6cd4909902 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineLast@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingAfter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingAfter@2x.png new file mode 100644 index 0000000000..f253af0a20 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingAfter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingBalancedOut@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingBalancedOut@2x.png new file mode 100644 index 0000000000..60ffcd81bc Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingBalancedOut@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingBefore@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingBefore@2x.png new file mode 100644 index 0000000000..e74a5b5a28 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildSpacing_spacingBefore@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildThatChangesCrossSizeWhenMainSizeIsFlexed@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildThatChangesCrossSizeWhenMainSizeIsFlexed@2x.png new file mode 100644 index 0000000000..fcb8d934c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testChildThatChangesCrossSizeWhenMainSizeIsFlexed@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisSizeBehaviors_fixedHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisSizeBehaviors_fixedHeight@2x.png new file mode 100644 index 0000000000..435a4cf60e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisSizeBehaviors_fixedHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisSizeBehaviors_variableHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisSizeBehaviors_variableHeight@2x.png new file mode 100644 index 0000000000..718b60304f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisSizeBehaviors_variableHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisStretchingOccursAfterStackAxisFlexing@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisStretchingOccursAfterStackAxisFlexing@2x.png new file mode 100644 index 0000000000..cd7489cb20 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testCrossAxisStretchingOccursAfterStackAxisFlexing@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testEmptyStack@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testEmptyStack@2x.png new file mode 100644 index 0000000000..94dc9a5d2b Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testEmptyStack@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisAppliedWhenFlexingItems_overflow@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisAppliedWhenFlexingItems_overflow@2x.png new file mode 100644 index 0000000000..d224b220fd Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisAppliedWhenFlexingItems_overflow@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisAppliedWhenFlexingItems_underflow@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisAppliedWhenFlexingItems_underflow@2x.png new file mode 100644 index 0000000000..df55d754f1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisAppliedWhenFlexingItems_underflow@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisOverridesIntrinsicSizeForNonFlexingChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisOverridesIntrinsicSizeForNonFlexingChildren@2x.png new file mode 100644 index 0000000000..64048bbd05 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFixedFlexBasisOverridesIntrinsicSizeForNonFlexingChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWithUnequalIntrinsicSizes_overflow@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWithUnequalIntrinsicSizes_overflow@2x.png new file mode 100644 index 0000000000..eebc4bb9e4 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWithUnequalIntrinsicSizes_overflow@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWithUnequalIntrinsicSizes_underflow@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWithUnequalIntrinsicSizes_underflow@2x.png new file mode 100644 index 0000000000..54929c089c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWithUnequalIntrinsicSizes_underflow@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png new file mode 100644 index 0000000000..31df2f6199 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png new file mode 100644 index 0000000000..1824a25085 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFractionalFlexBasisResolvesAgainstParentSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFractionalFlexBasisResolvesAgainstParentSize@2x.png new file mode 100644 index 0000000000..eba4df4df3 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFractionalFlexBasisResolvesAgainstParentSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png new file mode 100644 index 0000000000..489764fe6e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png new file mode 100644 index 0000000000..eb0ea8c3da Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png new file mode 100644 index 0000000000..8864402768 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png new file mode 100644 index 0000000000..4e46d0f9dd Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png new file mode 100644 index 0000000000..a0b412886f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png new file mode 100644 index 0000000000..8277b97c00 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedCenterWithChildSpacing_variableHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedCenterWithChildSpacing_variableHeight@2x.png new file mode 100644 index 0000000000..46dab172c1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedCenterWithChildSpacing_variableHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png new file mode 100644 index 0000000000..da37b62262 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png new file mode 100644 index 0000000000..6de6f28efc Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png new file mode 100644 index 0000000000..88d0aaf203 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png new file mode 100644 index 0000000000..8ee77c8de5 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationAndFlexFactorIsNotRespected@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationAndFlexFactorIsNotRespected@2x.png new file mode 100644 index 0000000000..d5cdcfb602 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationAndFlexFactorIsNotRespected@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSize@2x.png new file mode 100644 index 0000000000..ebea9c1eaa Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenChildren@2x.png new file mode 100644 index 0000000000..5318559608 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..5318559608 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAmongMixedChildrenWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactor@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactor@2x.png new file mode 100644 index 0000000000..4dda03f985 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactor@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildren@2x.png new file mode 100644 index 0000000000..8792b39b8f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildrenArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildrenArbitraryFloats@2x.png new file mode 100644 index 0000000000..8792b39b8f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorAmongMixedChildrenArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZero@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZero@2x.png new file mode 100644 index 0000000000..405fa880d3 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZero@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZeroWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZeroWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..405fa880d3 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorDoesNotShrinkToZeroWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..4dda03f985 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeAndFlexFactorWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..ebea9c1eaa Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNegativeViolationIsDistributedBasedOnSizeWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNestedBaselineAlignments@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNestedBaselineAlignments@2x.png new file mode 100644 index 0000000000..67f32cb4df Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNestedBaselineAlignments@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNestedStackLayoutStretchDoesNotViolateWidth@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNestedStackLayoutStretchDoesNotViolateWidth@2x.png new file mode 100644 index 0000000000..2a5fd841b8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testNestedStackLayoutStretchDoesNotViolateWidth@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists@2x.png new file mode 100644 index 0000000000..0471d0cc08 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_flex@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_flex@2x.png new file mode 100644 index 0000000000..3c15de518f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_flex@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyCenter@2x.png new file mode 100644 index 0000000000..07bf66b685 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyEnd@2x.png new file mode 100644 index 0000000000..a68c624868 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyStart@2x.png new file mode 100644 index 0000000000..e078398440 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testOverflowBehaviors_justifyStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEqually@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEqually@2x.png new file mode 100644 index 0000000000..12a70c6a6d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEqually@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyAmongMixedChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyAmongMixedChildren@2x.png new file mode 100644 index 0000000000..5ab4b9fd6c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyAmongMixedChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyAmongMixedChildrenWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyAmongMixedChildrenWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..5ab4b9fd6c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyAmongMixedChildrenWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..12a70c6a6d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedEquallyWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionally@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionally@2x.png new file mode 100644 index 0000000000..262cf4c26c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionally@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyAmongMixedChildren@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyAmongMixedChildren@2x.png new file mode 100644 index 0000000000..584294ebcb Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyAmongMixedChildren@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyAmongMixedChildrenWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyAmongMixedChildrenWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..584294ebcb Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyAmongMixedChildrenWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..262cf4c26c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testPositiveViolationIsDistributedProportionallyWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testRemainingViolationIsAppliedProperlyToFirstFlexibleChild@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testRemainingViolationIsAppliedProperlyToFirstFlexibleChild@2x.png new file mode 100644 index 0000000000..c555157918 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testRemainingViolationIsAppliedProperlyToFirstFlexibleChild@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testRemainingViolationIsAppliedProperlyToFirstFlexibleChildWithArbitraryFloats@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testRemainingViolationIsAppliedProperlyToFirstFlexibleChildWithArbitraryFloats@2x.png new file mode 100644 index 0000000000..c555157918 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testRemainingViolationIsAppliedProperlyToFirstFlexibleChildWithArbitraryFloats@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testStackSpacingWithChildrenHavingNilObjects_variableHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testStackSpacingWithChildrenHavingNilObjects_variableHeight@2x.png new file mode 100644 index 0000000000..ca249a5daa Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testStackSpacingWithChildrenHavingNilObjects_variableHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testStackSpacing_variableHeight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testStackSpacing_variableHeight@2x.png new file mode 100644 index 0000000000..77d64be4cb Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testStackSpacing_variableHeight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_flex@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_flex@2x.png new file mode 100644 index 0000000000..89dac5e442 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_flex@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyCenter@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyCenter@2x.png new file mode 100644 index 0000000000..2dfbc80025 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyCenter@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyEnd@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyEnd@2x.png new file mode 100644 index 0000000000..6fb41b79f8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyEnd@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png new file mode 100644 index 0000000000..1d71834f70 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png new file mode 100644 index 0000000000..49943caaa8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyStart@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyStart@2x.png new file mode 100644 index 0000000000..4be46ae87f Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifyStart@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png new file mode 100644 index 0000000000..111bd50048 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testShadowing@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testShadowing@2x.png new file mode 100644 index 0000000000..edb68c14aa Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testShadowing@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png new file mode 100644 index 0000000000..7e6cac14b8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInsetHighlight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInsetHighlight@2x.png new file mode 100644 index 0000000000..4a2fa33448 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInsetHighlight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png new file mode 100644 index 0000000000..5e2062b8e7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png new file mode 100644 index 0000000000..d9f01fce31 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png new file mode 100644 index 0000000000..4d68f59813 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASWrapperSpecSnapshotTests/testWrapperSpecWithMultipleElementsShouldSizeToLargestElement@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASWrapperSpecSnapshotTests/testWrapperSpecWithMultipleElementsShouldSizeToLargestElement@2x.png new file mode 100644 index 0000000000..e34eef76c7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASWrapperSpecSnapshotTests/testWrapperSpecWithMultipleElementsShouldSizeToLargestElement@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASWrapperSpecSnapshotTests/testWrapperSpecWithOneElementShouldSizeToElement@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASWrapperSpecSnapshotTests/testWrapperSpecWithOneElementShouldSizeToElement@2x.png new file mode 100644 index 0000000000..d63856e6ec Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_64/ASWrapperSpecSnapshotTests/testWrapperSpecWithOneElementShouldSizeToElement@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_childSize@2x.png new file mode 100644 index 0000000000..3b78fb5e7d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_fullSize@2x.png new file mode 100644 index 0000000000..3b78fb5e7d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_childSize@2x.png new file mode 100644 index 0000000000..34851067b9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_fullSize@2x.png new file mode 100644 index 0000000000..34851067b9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_childSize@2x.png new file mode 100644 index 0000000000..aa4c3ee8d8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_fullSize@2x.png new file mode 100644 index 0000000000..aa4c3ee8d8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_childSize@2x.png new file mode 100644 index 0000000000..23082ede8c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_fullSize@2x.png new file mode 100644 index 0000000000..23082ede8c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_fullSize@2x.png new file mode 100644 index 0000000000..393143a214 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_fullSize@2x.png new file mode 100644 index 0000000000..12498681e0 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_fullSize@2x.png new file mode 100644 index 0000000000..dc4f1ab2b3 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_fullSize@2x.png new file mode 100644 index 0000000000..fa7e15a554 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_childSize@2x.png new file mode 100644 index 0000000000..90f411aff2 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_fullSize@2x.png new file mode 100644 index 0000000000..6d49323c17 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_childSize@2x.png new file mode 100644 index 0000000000..9d23e2b646 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_fullSize@2x.png new file mode 100644 index 0000000000..58257ffef6 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_childSize@2x.png new file mode 100644 index 0000000000..3503fd79dd Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_fullSize@2x.png new file mode 100644 index 0000000000..263f50d29c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_childSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_childSize@2x.png new file mode 100644 index 0000000000..492fc049b8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_childSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png new file mode 100644 index 0000000000..9e39a3c5c4 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithSpaceBetween@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithSpaceBetween@2x.png new file mode 100644 index 0000000000..259778c672 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithSpaceBetween@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithStretchedItem@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithStretchedItem@2x.png new file mode 100644 index 0000000000..9e8286a88c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignmentWithStretchedItem@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineFirst@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineFirst@2x.png new file mode 100644 index 0000000000..7f4045ee98 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineFirst@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineLast@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineLast@2x.png new file mode 100644 index 0000000000..c81f3e9ca4 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testBaselineAlignment_baselineLast@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testNestedBaselineAlignments@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testNestedBaselineAlignments@2x.png new file mode 100644 index 0000000000..679b98a52c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testNestedBaselineAlignments@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png new file mode 100644 index 0000000000..7a336db723 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png new file mode 100644 index 0000000000..37dfc5986d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png new file mode 100644 index 0000000000..dd3b0ccf4d Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInset_ASTextNode2@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInset_ASTextNode2@2x.png new file mode 100644 index 0000000000..d66bb3bce9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInset_ASTextNode2@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_0Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_0Lines@2x.png new file mode 100644 index 0000000000..e080782fa8 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_0Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_1Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_1Lines@2x.png new file mode 100644 index 0000000000..7139ae654a Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_1Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_2Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_2Lines@2x.png new file mode 100644 index 0000000000..7cab68aa7e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_2Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_3Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_3Lines@2x.png new file mode 100644 index 0000000000..79c7db1ee7 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByCharWrapping_3Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_0Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_0Lines@2x.png new file mode 100644 index 0000000000..4293eb9df1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_0Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_1Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_1Lines@2x.png new file mode 100644 index 0000000000..1422cb5b1b Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_1Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_2Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_2Lines@2x.png new file mode 100644 index 0000000000..e9472fc15b Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_2Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_3Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_3Lines@2x.png new file mode 100644 index 0000000000..85f8b9688c Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingHead_3Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_0Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_0Lines@2x.png new file mode 100644 index 0000000000..c1a4f2b499 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_0Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_1Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_1Lines@2x.png new file mode 100644 index 0000000000..d534022d55 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_1Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_2Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_2Lines@2x.png new file mode 100644 index 0000000000..c52a7e6dde Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_2Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_3Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_3Lines@2x.png new file mode 100644 index 0000000000..e5cde0ab00 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_3Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_0Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_0Lines@2x.png new file mode 100644 index 0000000000..387d0b8d47 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_0Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_1Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_1Lines@2x.png new file mode 100644 index 0000000000..fd36b638c1 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_1Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_2Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_2Lines@2x.png new file mode 100644 index 0000000000..ebfd109103 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_2Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_3Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_3Lines@2x.png new file mode 100644 index 0000000000..29e13b8d01 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingTail_3Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png new file mode 100644 index 0000000000..6202ed9874 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_1Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_1Lines@2x.png new file mode 100644 index 0000000000..6316fb81f9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_1Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_2Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_2Lines@2x.png new file mode 100644 index 0000000000..ffe6f9c3a2 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_2Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_3Lines@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_3Lines@2x.png new file mode 100644 index 0000000000..f9248c2ecb Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_3Lines@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testThatSlowPathTruncationWorks_ASTextNode2@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testThatSlowPathTruncationWorks_ASTextNode2@2x.png new file mode 100644 index 0000000000..b8123efd43 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testThatSlowPathTruncationWorks_ASTextNode2@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png new file mode 100644 index 0000000000..111bd50048 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testShadowing@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testShadowing@2x.png new file mode 100644 index 0000000000..edb68c14aa Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testShadowing@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInset2@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInset2@2x.png new file mode 100644 index 0000000000..d66bb3bce9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInset2@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInset@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInset@2x.png new file mode 100644 index 0000000000..d66bb3bce9 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInset@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetHighlight@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetHighlight@2x.png new file mode 100644 index 0000000000..01234cff97 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetHighlight@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png new file mode 100644 index 0000000000..65c801d4df Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png new file mode 100644 index 0000000000..37b5444efa Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png new file mode 100644 index 0000000000..18de8b27fe Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode@2x.png b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode@2x.png new file mode 100644 index 0000000000..40351ad62b Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/TestHost/AppDelegate.h b/submodules/AsyncDisplayKit/Tests/TestHost/AppDelegate.h new file mode 100644 index 0000000000..1cdc488736 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/TestHost/AppDelegate.h @@ -0,0 +1,14 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@end diff --git a/submodules/AsyncDisplayKit/Tests/TestHost/AppDelegate.mm b/submodules/AsyncDisplayKit/Tests/TestHost/AppDelegate.mm new file mode 100644 index 0000000000..91dc913326 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/TestHost/AppDelegate.mm @@ -0,0 +1,14 @@ +// +// AppDelegate.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +@implementation AppDelegate + +@end diff --git a/submodules/AsyncDisplayKit/Tests/TestHost/Default-568h@2x.png b/submodules/AsyncDisplayKit/Tests/TestHost/Default-568h@2x.png new file mode 100644 index 0000000000..1547a98454 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/TestHost/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/Tests/TestHost/Info.plist b/submodules/AsyncDisplayKit/Tests/TestHost/Info.plist new file mode 100644 index 0000000000..ed1c9acf9b --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/TestHost/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/Tests/TestHost/main.mm b/submodules/AsyncDisplayKit/Tests/TestHost/main.mm new file mode 100644 index 0000000000..50a9e3ad6a --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/TestHost/main.mm @@ -0,0 +1,17 @@ +// +// main.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/Tests/TestResources/ASThrashTestRecordedCase b/submodules/AsyncDisplayKit/Tests/TestResources/ASThrashTestRecordedCase new file mode 100644 index 0000000000..e9dc1e9cc0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/TestResources/ASThrashTestRecordedCase @@ -0,0 +1 @@ +YnBsaXN0MDDUAAEAAgADAAQABQAGAagBqVgkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRvcBIAAYagrxBrAAcACAAPAC0ALgAvADAAMQAyADMANAA1ADYANwA4ADkAPABEAEoATwBWAFkAXABeAGEAaQBsAG8AcgB1AHgAgACGAI4AkgCWAJkAnACfAKIApgCqALIAtQC4ALsAvgDBAMUAzQDQANMA1gDZANwA4ADoAOsA7gDxAPQA9wD7AQMBBgEJAQwBDwESARQBGwEeAScBKgEuATYBOQE8AT8BQgFFAUgBUAFTAVwBXwFhAWkBawFtAW8BcQFzAXsBfQF/AYEBgwGFAYwBkAGTAZYBmgGcAaABpFUkbnVsbNMACQAKAAsADAANAA5VX2RpY3RYX3ZlcnNpb25WJGNsYXNzgAIQAYBq0wAQABEACwASAB8ALFdOUy5rZXlzWk5TLm9iamVjdHOsABMAFAAVABYAFwAYABkAGgAbABwAHQAegAOABIAFgAaAB4AIgAmACoALgAyADYAOrAAgACEAIgAjACQAJQAmACcAKAApACoAK4APgBGAE4AYgB6ARYBVgFaAXIBigGeAaIBpXxAQaW5zZXJ0ZWRTZWN0aW9uc18QFnJlcGxhY2VkU2VjdGlvbkluZGV4ZXNfEBNpbnNlcnRlZEl0ZW1JbmRleGVzXnJlcGxhY2luZ0l0ZW1zV29sZERhdGFUZGF0YV8QFmluc2VydGVkU2VjdGlvbkluZGV4ZXNfEBJkZWxldGVkSXRlbUluZGV4ZXNfEBNyZXBsYWNlZEl0ZW1JbmRleGVzXWluc2VydGVkSXRlbXNfEBVkZWxldGVkU2VjdGlvbkluZGV4ZXNfEBFyZXBsYWNpbmdTZWN0aW9uc9IAEQALADoAO6CAENIAPQA+AD8AQFokY2xhc3NuYW1lWCRjbGFzc2VzXk5TTXV0YWJsZUFycmF5owBBAEIAQ15OU011dGFibGVBcnJheVdOU0FycmF5WE5TT2JqZWN01ABFAAsARgBHAEgASQANAA1aTlNMb2NhdGlvblxOU1JhbmdlQ291bnRYTlNMZW5ndGgQAoAS0gA9AD4ASwBMXxARTlNNdXRhYmxlSW5kZXhTZXSjAE0ATgBDXxARTlNNdXRhYmxlSW5kZXhTZXRaTlNJbmRleFNldNIAEQALAFAAO6QAUQBSAFMAVIAUgBWAFoAXgBDUAEUACwBGAEcAVwBJAA0ADRADgBLSAEYACwBaAEkQAIAS0gBGAAsAWgBJgBLUAEUACwBGAEcAXwBJAA0ADRAEgBLSABEACwBiADulAGMAZABlAGYAZ4AZgBqAG4AcgB2AENIAEQALAGoAO6CAENIAEQALAG0AO6CAENIAEQALAHAAO6CAENIAEQALAHMAO6CAENIAEQALAHYAO6CAENIAEQALAHkAf6UAegB7AHwAfQB+gB+AKIAvgDaAPYBE0wCBAIIACwCDAIQAhVVpdGVtc1lzZWN0aW9uSUSAIBEBhYAn0gARAAsAhwA7pQCIAIkAigCLAIyAIYAjgCSAJYAmgBDSAI8ACwCQAJFWaXRlbUlEEQJogCLSAD0APgCTAJRfEBBBU1RocmFzaFRlc3RJdGVtogCVAENfEBBBU1RocmFzaFRlc3RJdGVt0gCPAAsAlwCREQJpgCLSAI8ACwCaAJERAmqAItIAjwALAJ0AkRECa4Ai0gCPAAsAoACREQJsgCLSAD0APgCjAKRfEBNBU1RocmFzaFRlc3RTZWN0aW9uogClAENfEBNBU1RocmFzaFRlc3RTZWN0aW9u0wCBAIIACwCnAKgAhYApEQGGgCfSABEACwCrADulAKwArQCuAK8AsIAqgCuALIAtgC6AENIAjwALALMAkRECbYAi0gCPAAsAtgCREQJugCLSAI8ACwC5AJERAm+AItIAjwALALwAkRECcIAi0gCPAAsAvwCREQJxgCLTAIEAggALAMIAwwCFgDARAYeAJ9IAEQALAMYAO6UAxwDIAMkAygDLgDGAMoAzgDSANYAQ0gCPAAsAzgCREQJygCLSAI8ACwDRAJERAnOAItIAjwALANQAkRECdIAi0gCPAAsA1wCREQJ1gCLSAI8ACwDaAJERAnaAItMAgQCCAAsA3QDeAIWANxEBiIAn0gARAAsA4QA7pQDiAOMA5ADlAOaAOIA5gDqAO4A8gBDSAI8ACwDpAJERAneAItIAjwALAOwAkRECeIAi0gCPAAsA7wCREQJ5gCLSAI8ACwDyAJERAnqAItIAjwALAPUAkRECe4Ai0wCBAIIACwD4APkAhYA+EQGJgCfSABEACwD8ADulAP0A/gD/AQABAYA/gECAQYBCgEOAENIAjwALAQQAkRECfIAi0gCPAAsBBwCREQJ9gCLSAI8ACwEKAJERAn6AItIAjwALAQ0AkRECf4Ai0gCPAAsBEACREQKAgCLSAD0APgBCAROiAEIAQ9IAEQALARUAO6QBFgEXARgBGYBGgEmAUIBSgBDTAIEAggALARwAqACFgEeAJ9IAEQALAR8AO6YArACtAK4BIwCvALCAKoArgCyASIAtgC6AENIAjwALASgAkREChoAi0wCBAIIACwErASwAhYBKEQGZgCfSABEACwEvADulATABMQEyATMBNIBLgEyATYBOgE+AENIAjwALATcAkRECgYAi0gCPAAsBOgCREQKCgCLSAI8ACwE9AJERAoOAItIAjwALAUAAkREChIAi0gCPAAsBQwCREQKFgCLTAIEAggALAUYA3gCFgFGAJ9IAEQALAUkAO6UA4gDjAOQA5QDmgDiAOYA6gDuAPIAQ0wCBAIIACwFRAPkAhYBTgCfSABEACwFUADumAP0A/gD/AQABWQEBgD+AQIBBgEKAVIBDgBDSAI8ACwFdAJERAoeAItIARgALAFoASYAS0gARAAsBYgA7pQFjAWQBZQFmAWeAV4BYgFmAWoBbgBDSAEYACwBaAEmAEtIARgALAFoASYAS0gBGAAsAWgBJgBLSAEYACwBaAEmAEtIARgALAFoASYAS0gARAAsBdAA7pQF1AXYBdwF4AXmAXYBegF+AYIBhgBDSAEYACwBaAEmAEtIARgALAFoASYAS0gBGAAsAWgBJgBLSAEYACwBaAEmAEtIARgALAFoASYAS0gARAAsBhgA7pAGHAYgBiQGKgGOAZIBlgGaAENIAEQALAY0AO6EBI4BIgBDSABEACwGRAH+ggETSABEACwGUADuggBDSABEACwGXADuhAVmAVIAQ1ABFAAsARgBHAFoASQANAA2AEtIAEQALAZ0AO6EBF4BJgBDSAD0APgGhAaJcTlNEaWN0aW9uYXJ5ogGjAENcTlNEaWN0aW9uYXJ50gA9AD4BpQGmXkFTVGhyYXNoVXBkYXRlogGnAENeQVNUaHJhc2hVcGRhdGVfEA9OU0tleWVkQXJjaGl2ZXLRAaoBq1Ryb290gAEACAAZACIAKwA1ADoAPwEYAR4BKwExAToBQQFDAUUBRwFUAVwBZwGAAYIBhAGGAYgBigGMAY4BkAGSAZQBlgGYAbEBswG1AbcBuQG7Ab0BvwHBAcMBxQHHAckBywHeAfcCDQIcAiQCKQJCAlcCbQJ7ApMCpwKwArECswK8AscC0ALfAuYC9QL9AwYDFwMiAy8DOAM6AzwDRQNZA2ADdAN/A4gDkQOTA5UDlwOZA5sDrAOuA7ADuQO7A70DxgPIA9kD2wPdA+YD8QPzA/UD9wP5A/sD/QQGBAcECQQSBBMEFQQeBB8EIQQqBCsELQQ2BDcEOQRCBE0ETwRRBFMEVQRXBFkEZgRsBHYEeAR7BH0EhgSRBJMElQSXBJkEmwSdBKYErQSwBLIEuwTOBNME5gTvBPIE9AT9BQAFAgULBQ4FEAUZBRwFHgUnBT0FQgVYBWUFZwVqBWwFdQWABYIFhAWGBYgFigWMBZUFmAWaBaMFpgWoBbEFtAW2Bb8FwgXEBc0F0AXSBd8F4QXkBeYF7wX6BfwF/gYABgIGBAYGBg8GEgYUBh0GIAYiBisGLgYwBjkGPAY+BkcGSgZMBlkGWwZeBmAGaQZ0BnYGeAZ6BnwGfgaABokGjAaOBpcGmgacBqUGqAaqBrMGtga4BsEGxAbGBtMG1QbYBtoG4wbuBvAG8gb0BvYG+Ab6BwMHBgcIBxEHFAcWBx8HIgckBy0HMAcyBzsHPgdAB0kHTgdXB2AHYgdkB2YHaAdqB3cHeQd7B4QHkQeTB5UHlweZB5sHnQefB6gHqwetB7oHvAe/B8EHygfVB9cH2QfbB90H3wfhB+oH7QfvB/gH+wf9CAYICQgLCBQIFwgZCCIIJQgnCDQINgg4CEEITAhOCFAIUghUCFYIWAhlCGcIaQhyCH8IgQiDCIUIhwiJCIsIjQiWCJkImwikCKYIrwi6CLwIvgjACMIIxAjGCM8I0QjaCNwI5QjnCPAI8gj7CP0JBgkRCRMJFQkXCRkJGwkdCSYJKAkxCTMJPAk+CUcJSQlSCVQJXQlmCWgJaglsCW4JcAl5CXwJfgmACYkJigmMCZUJlgmYCaEJpAmmCagJuQm7CcQJxwnJCcsJ1AnhCeYJ8wn8CgsKEAofCjEKNgo7AAAAAAAAAgIAAAAAAAABrAAAAAAAAAAAAAAAAAAACj0= \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Tests/TestResources/AttributedStringsFixture0.plist b/submodules/AsyncDisplayKit/Tests/TestResources/AttributedStringsFixture0.plist new file mode 100644 index 0000000000..1d5554e472 Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/TestResources/AttributedStringsFixture0.plist differ diff --git a/submodules/AsyncDisplayKit/Tests/TestResources/logo-square.png b/submodules/AsyncDisplayKit/Tests/TestResources/logo-square.png new file mode 100755 index 0000000000..82ad66c69e Binary files /dev/null and b/submodules/AsyncDisplayKit/Tests/TestResources/logo-square.png differ diff --git a/submodules/AsyncDisplayKit/Tests/en.lproj/InfoPlist.strings b/submodules/AsyncDisplayKit/Tests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/submodules/AsyncDisplayKit/Tests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/submodules/AsyncDisplayKit/Texture.podspec b/submodules/AsyncDisplayKit/Texture.podspec new file mode 100644 index 0000000000..ef915ac597 --- /dev/null +++ b/submodules/AsyncDisplayKit/Texture.podspec @@ -0,0 +1,99 @@ +Pod::Spec.new do |spec| + spec.name = 'Texture' + spec.version = '2.8' + spec.license = { :type => 'Apache 2', } + spec.homepage = 'http://texturegroup.org' + spec.authors = { 'Huy Nguyen' => 'hi@huytnguyen.me', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'mischneider1@gmail.com', 'Adlai Holler' => 'adlai@icloud.com' } + spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' + spec.source = { :git => 'https://github.com/TextureGroup/Texture.git', :tag => spec.version.to_s } + spec.module_name = 'AsyncDisplayKit' + spec.header_dir = 'AsyncDisplayKit' + + spec.documentation_url = 'http://texturegroup.org/appledoc/' + + spec.ios.deployment_target = '9.0' + spec.tvos.deployment_target = '9.0' + + # Subspecs + spec.subspec 'Core' do |core| + core.compiler_flags = '-fno-exceptions -Wno-implicit-retain-self' + core.public_header_files = [ + 'Source/*.h', + 'Source/Details/**/*.h', + 'Source/Layout/**/*.h', + 'Source/Base/*.h', + 'Source/Debug/**/*.h', + 'Source/TextKit/ASTextNodeTypes.h', + 'Source/TextKit/ASTextKitComponents.h' + ] + + core.source_files = [ + 'Source/**/*.{h,mm}', + + # Most TextKit components are not public because the C++ content + # in the headers will cause build errors when using + # `use_frameworks!` on 0.39.0 & Swift 2.1. + # See https://github.com/facebook/AsyncDisplayKit/issues/1153 + 'Source/TextKit/*.h', + ] + end + + spec.subspec 'PINRemoteImage' do |pin| + pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.14' + pin.dependency 'PINRemoteImage/PINCache' + pin.dependency 'Texture/Core' + end + + spec.subspec 'IGListKit' do |igl| + igl.dependency 'IGListKit', '~> 3.0' + igl.dependency 'Texture/Core' + end + + spec.subspec 'Yoga' do |yoga| + yoga.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) YOGA=1' } + yoga.dependency 'Yoga', '1.6.0' + yoga.dependency 'Texture/Core' + end + + # If flag is enabled the old TextNode with all dependencies will be compiled out + spec.subspec 'TextNode2' do |text_node| + text_node.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_ENABLE_TEXTNODE=0' } + text_node.dependency 'Texture/Core' + end + + spec.subspec 'Video' do |video| + video.frameworks = ['AVFoundation', 'CoreMedia'] + video.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_USE_VIDEO=1' } + video.dependency 'Texture/Core' + end + + spec.subspec 'MapKit' do |map| + map.frameworks = ['CoreLocation', 'MapKit'] + map.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_USE_MAPKIT=1' } + map.dependency 'Texture/Core' + end + + spec.subspec 'Photos' do |photos| + photos.frameworks = 'Photos' + photos.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_USE_PHOTOS=1' } + photos.dependency 'Texture/Core' + end + + spec.subspec 'AssetsLibrary' do |assetslib| + assetslib.frameworks = 'AssetsLibrary' + assetslib.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_USE_ASSETS_LIBRARY=1' } + assetslib.dependency 'Texture/Core' + end + + # Include these by default for backwards compatibility. + # This will change in 3.0. + spec.default_subspecs = 'Core', 'PINRemoteImage', 'Video', 'MapKit', 'AssetsLibrary', 'Photos' + + spec.social_media_url = 'https://twitter.com/TextureiOS' + spec.library = 'c++' + spec.pod_target_xcconfig = { + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', + 'CLANG_CXX_LIBRARY' => 'libc++' + } + +end diff --git a/submodules/AsyncDisplayKit/buck-files/BUCK_FBSnapshotTestCase b/submodules/AsyncDisplayKit/buck-files/BUCK_FBSnapshotTestCase new file mode 100755 index 0000000000..c8b969639e --- /dev/null +++ b/submodules/AsyncDisplayKit/buck-files/BUCK_FBSnapshotTestCase @@ -0,0 +1,12 @@ +apple_library( + name = 'FBSnapshotTestCase', + exported_headers = glob(['FBSnapshotTestCase' + '/**/*.h']), + srcs = glob(['FBSnapshotTestCase' + '/**/*.m']), + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + '$SDKROOT/System/Library/Frameworks/QuartzCore.framework', + '$PLATFORM_DIR/Developer/Library/Frameworks/XCTest.framework', + ], + visibility = ['PUBLIC'], +) diff --git a/submodules/AsyncDisplayKit/buck-files/BUCK_FLAnimatedImage b/submodules/AsyncDisplayKit/buck-files/BUCK_FLAnimatedImage new file mode 100755 index 0000000000..f04abd396b --- /dev/null +++ b/submodules/AsyncDisplayKit/buck-files/BUCK_FLAnimatedImage @@ -0,0 +1,18 @@ +apple_library( + name = 'FLAnimatedImage', + exported_headers = glob(['FLAnimatedImage/*.h']), + srcs = glob(['FLAnimatedImage/*.m']), + preprocessor_flags = ['-fobjc-arc', '-Wno-deprecated-declarations'], + lang_preprocessor_flags = { + 'C': ['-std=gnu99'], + 'CXX': ['-std=gnu++11', '-stdlib=libc++'], + }, + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + '$SDKROOT/System/Library/Frameworks/ImageIO.framework', + '$SDKROOT/System/Library/Frameworks/MobileCoreServices.framework', + '$SDKROOT/System/Library/Frameworks/QuartzCore.framework', + ], + visibility = ['PUBLIC'], +) diff --git a/submodules/AsyncDisplayKit/buck-files/BUCK_JGMethodSwizzler b/submodules/AsyncDisplayKit/buck-files/BUCK_JGMethodSwizzler new file mode 100755 index 0000000000..169cfa1e01 --- /dev/null +++ b/submodules/AsyncDisplayKit/buck-files/BUCK_JGMethodSwizzler @@ -0,0 +1,9 @@ +apple_library( + name = 'JGMethodSwizzler', + exported_headers = ['JGMethodSwizzler' + '/JGMethodSwizzler.h'], + srcs = ['JGMethodSwizzler' + '/JGMethodSwizzler.m'], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + ], + visibility = ['PUBLIC'], +) diff --git a/submodules/AsyncDisplayKit/buck-files/BUCK_OCMock b/submodules/AsyncDisplayKit/buck-files/BUCK_OCMock new file mode 100755 index 0000000000..666f844582 --- /dev/null +++ b/submodules/AsyncDisplayKit/buck-files/BUCK_OCMock @@ -0,0 +1,9 @@ +apple_library( + name = 'OCMock', + exported_headers = glob(['Source/OCMock' + '/*.h']), + srcs = glob(['Source/OCMock' + '/*.m']), + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + ], + visibility = ['PUBLIC'], +) diff --git a/submodules/AsyncDisplayKit/buck-files/BUCK_PINCache b/submodules/AsyncDisplayKit/buck-files/BUCK_PINCache new file mode 100755 index 0000000000..660b69f716 --- /dev/null +++ b/submodules/AsyncDisplayKit/buck-files/BUCK_PINCache @@ -0,0 +1,23 @@ +apple_library( + name = 'PINCache', + exported_headers = glob(['PINCache/*.h']), + # PINDiskCache.m should be compiled with '-fobjc-arc-exceptions' (#105) + srcs = + glob(['PINCache/*.m'], excludes = ['PINCache/PINDiskCache.m']) + + [('PINCache/PINDiskCache.m', ['-fobjc-arc-exceptions'])], + preprocessor_flags = ['-fobjc-arc'], + lang_preprocessor_flags = { + 'C': ['-std=gnu99'], + 'CXX': ['-std=gnu++11', '-stdlib=libc++'], + }, + linker_flags = [ + '-weak_framework', + 'UIKit', + '-weak_framework', + 'AppKit', + ], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + ], + visibility = ['PUBLIC'], +) diff --git a/submodules/AsyncDisplayKit/buck-files/BUCK_PINRemoteImage b/submodules/AsyncDisplayKit/buck-files/BUCK_PINRemoteImage new file mode 100755 index 0000000000..95825e4d95 --- /dev/null +++ b/submodules/AsyncDisplayKit/buck-files/BUCK_PINRemoteImage @@ -0,0 +1,93 @@ +##################################### +# Defines +##################################### +COMMON_PREPROCESSOR_FLAGS = ['-fobjc-arc'] + +COMMON_LANG_PREPROCESSOR_FLAGS = { + 'C': ['-std=gnu99'], + 'CXX': ['-std=gnu++11', '-stdlib=libc++'], +} + +FLANIMATEDIMAGE_HEADER_FILES = ['Pod/Classes/Image Categories/FLAnimatedImageView+PINRemoteImage.h'] +FLANIMATEDIMAGE_SOURCE_FILES = ['Pod/Classes/Image Categories/FLAnimatedImageView+PINRemoteImage.m'] + +PINCACHE_HEADER_FILES = glob(['Pod/Classes/PINCache/**/*.h']) +PINCACHE_SOURCE_FILES = glob(['Pod/Classes/PINCache/**/*.m']) + +##################################### +# PINRemoteImage core targets +##################################### +apple_library( + name = 'PINRemoteImage-Core', + header_path_prefix = 'PINRemoteImage', + exported_headers = glob([ + 'Pod/Classes/**/*.h', + ], + excludes = FLANIMATEDIMAGE_HEADER_FILES + PINCACHE_HEADER_FILES + ), + srcs = glob([ + 'Pod/Classes/**/*.m', + ], + excludes = FLANIMATEDIMAGE_SOURCE_FILES + PINCACHE_SOURCE_FILES + ), + preprocessor_flags = COMMON_PREPROCESSOR_FLAGS + [ + '-DPIN_TARGET_IOS=(TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR || TARGET_OS_TV)', + '-DPIN_TARGET_MAC=(TARGET_OS_MAC)', + ], + lang_preprocessor_flags = COMMON_LANG_PREPROCESSOR_FLAGS, + linker_flags = [ + '-weak_framework', + 'UIKit', + '-weak_framework', + 'MobileCoreServices', + '-weak_framework', + 'Cocoa', + '-weak_framework', + 'CoreServices', + ], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/ImageIO.framework', + '$SDKROOT/System/Library/Frameworks/Accelerate.framework', + ], + visibility = ['PUBLIC'], +) + +apple_library( + name = 'PINRemoteImage', + deps = [ + ':PINRemoteImage-FLAnimatedImage', + ':PINRemoteImage-PINCache' + ], + visibility = ['PUBLIC'], +) + +##################################### +# Other PINRemoteImage targets +##################################### +apple_library( + name = 'PINRemoteImage-FLAnimatedImage', + header_path_prefix = 'PINRemoteImage', + exported_headers = FLANIMATEDIMAGE_HEADER_FILES, + srcs = FLANIMATEDIMAGE_SOURCE_FILES, + preprocessor_flags = COMMON_PREPROCESSOR_FLAGS, + deps = [ + ':PINRemoteImage-Core', + '//Pods/FLAnimatedImage:FLAnimatedImage' + ], + visibility = ['PUBLIC'], +) + +apple_library( + name = 'PINRemoteImage-PINCache', + header_path_prefix = 'PINRemoteImage', + exported_headers = PINCACHE_HEADER_FILES, + srcs = PINCACHE_SOURCE_FILES, + preprocessor_flags = COMMON_PREPROCESSOR_FLAGS, + deps = [ + ':PINRemoteImage-Core', + '//Pods/PINCache:PINCache' + ], + visibility = ['PUBLIC'], +) + +#TODO WebP variants diff --git a/submodules/AsyncDisplayKit/build.sh b/submodules/AsyncDisplayKit/build.sh new file mode 100755 index 0000000000..e866f305af --- /dev/null +++ b/submodules/AsyncDisplayKit/build.sh @@ -0,0 +1,212 @@ +#!/bin/bash + +PLATFORM="${TEXTURE_BUILD_PLATFORM:-platform=iOS Simulator,OS=10.2,name=iPhone 7}" +SDK="${TEXTURE_BUILD_SDK:-iphonesimulator11.4}" +DERIVED_DATA_PATH="~/ASDKDerivedData" + +# It is pitch black. +set -e +function trap_handler { + echo -e "\n\nOh no! You walked directly into the slavering fangs of a lurking grue!" + echo "**** You have died ****" + exit 255 +} +trap trap_handler INT TERM EXIT + +# Derived data handling +eval [ ! -d $DERIVED_DATA_PATH ] && eval mkdir $DERIVED_DATA_PATH +function clean_derived_data { + eval find $DERIVED_DATA_PATH -mindepth 1 -delete +} + +# Build example +function build_example { + example="$1" + + clean_derived_data + + if [ -f "${example}/Podfile" ]; then + echo "Using CocoaPods" + if [ -f "${example}/Podfile.lock" ]; then + rm "$example/Podfile.lock" + fi + rm -rf "$example/Pods" + pod install --project-directory=$example + + set -o pipefail && xcodebuild \ + -workspace "${example}/Sample.xcworkspace" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + -derivedDataPath "$DERIVED_DATA_PATH" \ + build | xcpretty $FORMATTER + elif [ -f "${example}/Cartfile" ]; then + echo "Using Carthage" + local_repo=`pwd` + current_branch=`git rev-parse --abbrev-ref HEAD` + cd $example + + echo "git \"file://${local_repo}\" \"${current_branch}\"" > "Cartfile" + carthage update --platform iOS + + set -o pipefail && xcodebuild \ + -project "Sample.xcodeproj" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build | xcpretty $FORMATTER + + cd ../.. + fi +} + +MODE="$1" + +if type xcpretty-travis-formatter &> /dev/null; then + FORMATTER="-f $(xcpretty-travis-formatter)" + else + FORMATTER="-s" +fi + +if [ "$MODE" = "tests" -o "$MODE" = "all" ]; then + echo "Building & testing AsyncDisplayKit." + pod install + set -o pipefail && xcodebuild \ + -workspace AsyncDisplayKit.xcworkspace \ + -scheme AsyncDisplayKit \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build-for-testing test | xcpretty $FORMATTER + success="1" +fi + +if [ "$MODE" = "tests_listkit" ]; then + echo "Building & testing AsyncDisplayKit+IGListKit." + pod install --project-directory=SubspecWorkspaces/ASDKListKit + set -o pipefail && xcodebuild \ + -workspace SubspecWorkspaces/ASDKListKit/ASDKListKit.xcworkspace \ + -scheme ASDKListKitTests \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build-for-testing test | xcpretty $FORMATTER + success="1" +fi + +if [ "$MODE" = "examples" -o "$MODE" = "all" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + #Update cocoapods repo + pod repo update master + + for example in examples/*/; do + echo "Building (examples) $example." + + build_example $example + done + success="1" +fi + +if [ "$MODE" = "examples-pt1" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + #Update cocoapods repo + pod repo update master + + for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -6 | head); do + echo "Building (examples-pt1) $example." + + build_example $example + done + success="1" +fi + +if [ "$MODE" = "examples-pt2" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + #Update cocoapods repo + pod repo update master + + for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -12 | tail -6 | head); do + echo "Building $example (examples-pt2)." + + build_example $example + done + success="1" +fi + +if [ "$MODE" = "examples-pt3" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + #Update cocoapods repo + pod repo update master + + for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -7 | head); do + echo "Building $example (examples-pt3)." + + build_example $example + done + success="1" +fi + +if [ "$MODE" = "examples-extra" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + #Update cocoapods repo + pod repo update master + + for example in $((find ./examples_extra -type d -maxdepth 1 \( ! -iname ".*" \)) | head -7 | head); do + echo "Building $example (examples-extra)." + + build_example $example + done + success="1" +fi + +# Support building a specific example: sh build.sh example examples/ASDKLayoutTransition +if [ "$MODE" = "example" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + #Update cocoapods repo + pod repo update master + + build_example $2 + success="1" +fi + +if [ "$MODE" = "life-without-cocoapods" -o "$MODE" = "all" ]; then + echo "Verifying that AsyncDisplayKit functions as a static library." + + set -o pipefail && xcodebuild \ + -workspace "smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcworkspace" \ + -scheme "Life Without CocoaPods" \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build | xcpretty $FORMATTER + success="1" +fi + +if [ "$MODE" = "framework" -o "$MODE" = "all" ]; then + echo "Verifying that AsyncDisplayKit functions as a dynamic framework (for Swift/Carthage users)." + + set -o pipefail && xcodebuild \ + -project "smoke-tests/Framework/Sample.xcodeproj" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build | xcpretty $FORMATTER + success="1" +fi + +if [ "$MODE" = "cocoapods-lint" -o "$MODE" = "all" ]; then + echo "Verifying that podspec lints." + + set -o pipefail && pod env && pod lib lint --allow-warnings + success="1" +fi + +if [ "$MODE" = "carthage" -o "$MODE" = "all" ]; then + echo "Verifying carthage works." + + set -o pipefail && carthage update && carthage build --no-skip-current +fi + +if [ "$success" = "1" ]; then + trap - EXIT + exit 0 +fi + +echo "Unrecognised mode '$MODE'." diff --git a/submodules/AsyncDisplayKit/docs/.gitignore b/submodules/AsyncDisplayKit/docs/.gitignore new file mode 100755 index 0000000000..bd5d18888f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/.gitignore @@ -0,0 +1,2 @@ +_site/ + diff --git a/submodules/AsyncDisplayKit/docs/404.md b/submodules/AsyncDisplayKit/docs/404.md new file mode 100755 index 0000000000..5c0791ac92 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/404.md @@ -0,0 +1,11 @@ +--- +id: 4oh4 +title: Page Not Found +layout: default +--- + +# Page Not Found + +Crikey! There doesn't seem to be anything here. + +If you find a broken link, feel free to send a pull request. You can also let us know at [Github](https://github.com/texturegroup/texture/issues) so that we can fix it. diff --git a/submodules/AsyncDisplayKit/docs/CNAME b/submodules/AsyncDisplayKit/docs/CNAME new file mode 100755 index 0000000000..80ea8a240d --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/CNAME @@ -0,0 +1 @@ +texturegroup.org \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/Gemfile b/submodules/AsyncDisplayKit/docs/Gemfile new file mode 100755 index 0000000000..dec8af5008 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem 'github-pages' +gem 'rouge', '~>1.7' +gem 'jekyll', '~>3.6.3' diff --git a/submodules/AsyncDisplayKit/docs/README.md b/submodules/AsyncDisplayKit/docs/README.md new file mode 100755 index 0000000000..8c1c2ee544 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/README.md @@ -0,0 +1,31 @@ +# Texture Documentation + +We use [Jekyll](http://jekyllrb.com/) to build the site using Markdown and host it on [Github Pages](https://pages.github.com/). + +### Dependencies + +Github Pages uses Jekyll to host a site and Jekyll has the following dependencies. + + - [Ruby](http://www.ruby-lang.org/) (version >= 2.0.0) + - [RubyGems](http://rubygems.org/) (version >= 1.3.7) + - [Bundler](http://gembundler.com/) + +Mac OS X comes pre-installed with Ruby, but you may need to update RubyGems (via `gem update --system`). +Once you have RubyGems, use it to install bundler. + +```sh +$ gem install bundler +$ cd gh-pages # Go to folder +$ bundle install # Might need sudo. +``` + +### Run Jekyll Locally + +Use Jekyll to serve the website locally (by default, at `http://localhost:4000`): + +```sh +$ bundle exec jekyll serve [--incremental] +$ open http://localhost:4000/ +``` + +For more, see https://help.github.com/articles/setting-up-your-github-pages-site-locally-with-jekyll/ diff --git a/submodules/AsyncDisplayKit/docs/_config.yml b/submodules/AsyncDisplayKit/docs/_config.yml new file mode 100755 index 0000000000..ab347c39b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_config.yml @@ -0,0 +1,24 @@ +--- +url: http://texturegroup.org +name: Texture +relative_permalinks: false +markdown: kramdown +timezone: America/Los_Angeles +google_analytics: UA-87502907-1 + +safe: true +lsi: false +highlighter: rouge + +defaults: + - + scope: + path: "" + type: "posts" + values: + layout: post + is_post: true + +collections: + docs: + output: true diff --git a/submodules/AsyncDisplayKit/docs/_data/nav_development.yml b/submodules/AsyncDisplayKit/docs/_data/nav_development.yml new file mode 100644 index 0000000000..3a3a2597c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_data/nav_development.yml @@ -0,0 +1,22 @@ +- title: Introduction + items: + - overview + - structure + - components + - how-to-develop-and-debug +- title: Threading + items: + - threading +- title: Node Lifecycle + items: + - node-lifecycle +- title: Layout System + items: + - layout-specs + - automatic-subnode-layout + - layout-transitions +- title: Collections + items: + - collection-asynchronous-updates + - collection-animations + - cell-node-lifecycle diff --git a/submodules/AsyncDisplayKit/docs/_data/nav_docs.yml b/submodules/AsyncDisplayKit/docs/_data/nav_docs.yml new file mode 100755 index 0000000000..6b4b7b8072 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_data/nav_docs.yml @@ -0,0 +1,71 @@ +- title: Quick Start + items: + - getting-started + - resources + - installation + - adoption-guide-2-0-beta1 +- title: Core Concepts + items: + - intelligent-preloading + - containers-overview + - node-overview + - subclassing + - node-lifecycle + - faq +- title: Layout + items: + - layout2-quickstart + - automatic-layout-examples-2 + - layout2-layoutspec-types + - layout2-layout-element-properties + - layout2-api-sizing + - layout-transition-api +- title: Conveniences + items: + - hit-test-slop + - batch-fetching-api + - automatic-subnode-mgmt + - inversion + - image-modification-block + - placeholder-fade-duration + - accessibility + - uicollectionviewinterop +- title: Optimizations + items: + - layer-backing + - subtree-rasterization + - synchronous-concurrency + - corner-rounding +- title: Tools + items: + - debug-tool-hit-test-visualization + - debug-tool-pixel-scaling + - debug-tool-ASRangeController +- title: Advanced Technologies + items: + - asvisibility + - asrunloopqueue +- title: Node Containers + items: + - containers-asviewcontroller + - containers-asnodecontroller + - containers-astablenode + - containers-ascollectionnode + - containers-aspagernode +- title: Nodes + items: + - display-node + - cell-node + - button-node + - text-node + - image-node + - network-image-node + - video-node + - map-node + - control-node + - scroll-node + - editable-text-node + - multiplex-image-node + + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/accessibility.md b/submodules/AsyncDisplayKit/docs/_docs/accessibility.md new file mode 100755 index 0000000000..45261003d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/accessibility.md @@ -0,0 +1,11 @@ +--- +title: Accessibility +layout: docs +permalink: /docs/accessibility.html +prevPage: placeholder-fade-duration.html +nextPage: layer-backing.html +--- + +Accessibility works seamlessly in ways that even UIKit doesn’t provide. When using the powerful optimization features of Layer Backing (`.layerBacked`) and Subtree Rasterization (`.shouldRasterizeDescendants`), VoiceOver can access fine-grained metadata about each element. This is pretty amazing: `CALayer` doesn’t support accessibility, and rasterization reduces everything to a single flat image. + +The Texture team fundamentally believes in Accessibility, and invested the time to create an innovative system to make this possible with zero developer effort. As a bonus, this also allows Automated UI Testing greater access to the interface. \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/adoption-guide-2-0-beta1.md b/submodules/AsyncDisplayKit/docs/_docs/adoption-guide-2-0-beta1.md new file mode 100755 index 0000000000..f3c199fff3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/adoption-guide-2-0-beta1.md @@ -0,0 +1,146 @@ +--- +title: "Upgrading to 2.0" +layout: docs +permalink: /docs/adoption-guide-2-0-beta1.html +prevPage: adoption-guide-2-0-beta1.html +--- + +
    +
  1. GitHub Release Notes
  2. +
  3. Getting the 2.0 Release Candidate
  4. +
  5. Testing your app with 2.0
  6. +
  7. Migrating to 2.0
  8. +
  9. Migrating to 2.0 (Layout)
  10. +
+ +## Release Notes + +Please read the official release notes on GitHub. + + +## Getting the Release Candidate + +Add the following to your podfile + +
+
+
+pod 'Texture', '>= 2.0'
+
+
+
+ +then run + +
+
+
+pod repo update
+pod update Texture
+
+
+
+ +in the terminal. + +## Testing 2.0 + +Once you have updated to 2.0, you will see many deprecation warnings. Don't worry! + +These warnings are quite safe, because we have bridged all of the old APIs for you, so that you can test out the 2.0, before migrating to the new API. + +If your app fails to build instead of just showing the warnings, you might have Warnings as Errors enabled for your project. You have a few options: + +1. Disable deprecation warnings in the Xcode project settings +2. Disable warnings as errors in the project's build settings. +3. Disable deprecation warnings in Texture. To do this, change `line 74` in `ASBaseDefines.h` to `# define ASDISPLAYNODE_WARN_DEPRECATED 0` + +Once your app builds and runs, test it to make sure everything is working normally. If you find any problems, try adopting the new API in that area and re-test. + +One key behavior change you may notice: + +- ASStackLayoutSpec's `.alignItems` property default changed to `ASStackLayoutAlignItemsStretch` instead of `ASStackLayoutAlignItemsStart`. This may cause distortion in your UI. + +If you still have issues, please file a GitHub issue and we'd be happy to help you out! + +## Migrating to 2.0 + +Once your app is working, it's time to start converting! + +A full API changelog from `1.9.92` to `2.0-beta.1` is available here. + +#### ASDisplayNode Changes + +- ASDisplayNode's `.usesImplicitHierarchyManagement` has been renamed to `.automaticallyManagesSubnodes`. The Automatic Subnode Management API has been moved out of Beta, but has a few documented [limitations](). + +- ASDisplayNode's `-cancelLayoutTransitionsInProgress` has been renamed to `-cancelLayoutTransition`. The Layout Transition API has been moved out of Beta. Significant new functionality is planed for future dot releases. + + +#### Updated Interface State Callback Methods + +The new method names are meant to unify the range update methods to show how they relate to each other and be a bit more self-explanatory: + +- `didEnterPreloadState / didExitPreloadState` +- `didEnterDisplayState / didExitDisplayState` +- `didEnterVisibleState / didExitVisibleState` + +These new methods replace the following: + +- `loadStateDidChange:(BOOL)inLoadState` +- `displayStateDidChange:(BOOL)inDisplayState` +- `visibleStateDidChange:(BOOL)isVisible` + +#### Collection / Table API Updates + +Texture's collection and table APIs have been moved from the view space (`collectionView`, `tableView`) to the node space (`collectionNode`, `tableNode`). + +- Search your project for `tableView` and `collectionView`. Most, if not all, of the data source / delegate methods have new node versions. + +It is important that developers using Texture understand that an ASCollectionNode is backed by an ASCollectionView (a subclass of UICollectionView). ASCollectionNode runs asynchronously, so calling -numberOfRowsInSection on the collectionNode is different than calling it on the collectionView. + +For example, let's say you have an empty table. You insert `100` rows and then immediately call -tableView:numberOfRowsInSection. This will return `0` rows. If you call -waitUntilAllUpdatesAreCommitted after insertion (waits until the collectionNode synchronizes with the collectionView), you will get 100, _but_ you might block the main thread. A good developer should rarely (or never) need to use -waitUntilAllUpdatesAreCommitted. If you update the collectionNode and then need to read back immediately, you should use the collectionNode API. You shouldn't need to talk to the collectionView. + +As a rule of thumb, use the collection / table node API for everything, unless the API is not available on the collectionNode. + +To summarize, any `indexPath` that is passed to the `collectionView` space references data that has been synced with `ASCollectionNode`'s underlying `UICollectionView`. Conversly, any `indexPath` that is passed to the `collectionNode` space references asynchronous data that *might not yet* have been synced with ASCollectionNode's underlying `UICollectionView`. The same concepts apply to `ASTableNode`. + +An exception to this is `ASTableNode`'s `-didSelectRowAtIndexPath:`, which is called in UIKit space to make sure that `indexPath` indicies reference the data in the onscreen (data that has been synced to the underlying `UICollectionView` `dataSource`). + +While previous versions of the framework required the developer to be aware of the asynchronous interplay between `ASCollectionNode` and its underlying `UICollectionView`, this new API should provide better safegaurds against developer-introduced data source inconsistencies. + +Other updates include: + +- Deprecate `ASTableView`'s -init method. Please use `ASTableNode` instead of `ASTableView`. While this makes adopting the framework marginally more difficult to, the benefits of using ASTableNode / ASCollectionNode over their ASTableView / ASCollectionView counterparts are signficant. + +- Deprecate `-beginUpdates` and `-endUpdatesAnimated:`. Please use the `-performBatchUpdates:` methods instead. + +- Deprecate `-reloadDataImmediately`. Please see the header file comments for the deprecation solution. + +- Moved range tuning to the `tableNode` / `collectionNode` (from the `tableView` / `collectionView`) + +- `constrainedSizeForNodeAtIndexPath:` moved from the `.dataSource` to the `.delegate` to be consistent with UIKit definitions of the roles. **Note:** Make sure that you provide a delegate for any `ASTableNode`, `ASCollectionNode` or `ASPagerNodes` that use this method. Your code will silently not call your delegate method, if you do not have a delegate assigned. + +- Renamed `pagerNode:constrainedSizeForNodeAtIndexPath:` to `pagerNode:constrainedSizeForNodeAtIndex:` + +- collection view update validation assertions are now enabled. If you see something like `"Invalid number of items in section 2. The number of items after the update (7) must be equal to the number of items before the update (4) plus or minus the number of items inserted or removed from the section (4 inserted, 0 removed)"`, please check the data source logic. If you have any questions, reach out to us on GitHub. + +Best Practices: + +- Use node blocks if possible. These are run in parallel on a background thread, resulting in 10x performance gains. +- Use nodes to store things about your rows. +- Make sure to batch updates that need to be batched. + +Resources: + +- [Video](https://youtu.be/yuDqvE5n_1g) of the ASCollectionNode Behind-the-Scenes talk at Pinterest. The diagrams seen in the talk. + +- PR [#2390](https://github.com/facebook/AsyncDisplayKit/pull/2390) and PR [#2381](https://github.com/facebook/AsyncDisplayKit/pull/2381) show how we converted AsyncDisplayKit's [example projects](https://github.com/texturegroup/texture/tree/master/examples) to conform to this new API. + + +#### Layout API Updates + +Please read the separate Layout 2.0 Conversion Guide for an overview of the upgrades and to see how to convert your existing layout code. + +#### Help us out + +If we're missing something from this list, please let us know or edit this doc for us (GitHub edit link at the top of page)! diff --git a/submodules/AsyncDisplayKit/docs/_docs/apidiff-1992-to-20beta1.md b/submodules/AsyncDisplayKit/docs/_docs/apidiff-1992-to-20beta1.md new file mode 100755 index 0000000000..bc681276fd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/apidiff-1992-to-20beta1.md @@ -0,0 +1,7 @@ +--- +title: API Diff +layout: apidiff +permalink: /docs/apidiff-1992-to-20beta1.html +--- + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/appledocs.md b/submodules/AsyncDisplayKit/docs/_docs/appledocs.md new file mode 100755 index 0000000000..4b466e9928 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/appledocs.md @@ -0,0 +1,9 @@ + +--- +title: api +layout: docs +permalink: /docs/appledocs.html +--- + +

hi

+ \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/asenvironment.md b/submodules/AsyncDisplayKit/docs/_docs/asenvironment.md new file mode 100755 index 0000000000..c749cd0802 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/asenvironment.md @@ -0,0 +1,19 @@ +--- +title: ASEnvironment +layout: docs +permalink: /docs/asenvironment.html +prevPage: asvisibility.html +nextPage: asrunloopqueue.html +--- + +`ASEnvironment` is a performant and scalable way to enable upward and downward propagation of information throughout the node hierarchy. It stores a variety of critical “environmental” metadata, like the trait collection, interface state, hierarchy state, and more. + +Any object that conforms to the `` protocol can propagate specific states defined in an `ASEnvironmentState` up and/or down the ASEnvironment tree. To define how merges of States should happen, specific merge functions can be provided. + +Compared to UIKit, this system is very efficient and one of the reasons why nodes are much lighter weight than UIViews. This is achieved by using simple structures to store data rather than creating objects. For example, `UITraitCollection` is an object, but `ASEnvironmentTraitCollection` is just a struct. + +This means that whenever a node needs to query something about its environment, for example to check its [interface state](http://texturegroup.org/docs/intelligent-preloading.html#interface-state-ranges), instead of climbing the entire tree or checking all of its children, it can go to one spot and read the value that was propogated to it. + +A key operating principle of ASEnvironment is to update values when new subnodes are added or removed. + +ASEnvironment powers many of the most valuable features of Texture. **There is no public API available at this time.** diff --git a/submodules/AsyncDisplayKit/docs/_docs/asrunloopqueue.md b/submodules/AsyncDisplayKit/docs/_docs/asrunloopqueue.md new file mode 100755 index 0000000000..f40f532ed8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/asrunloopqueue.md @@ -0,0 +1,14 @@ +--- +title: ASRunLoopQueue +layout: docs +permalink: /docs/asrunloopqueue.html +prevPage: asvisibility.html +--- + +Even with main thread work, Texture is able to dramatically reduce its impact on the user experience by way of the rather amazing ASRunLoopQueue. + +`ASRunloopQueue` breaks up operations that must be performed on the main thread into far smaller chunks, easily 1/10th of the size that they otherwise would be, so that operation such as allocating UIViews or even destroying objects can be spread out and allow the run loops to more frequently turn. This more periodic turning allows the device to much more frequently check if a user touch has started or if an animation timer requires a new frame to be drawn, allowing far greater responsiveness even when the device is very busy and processing a large queue of main thread work. + +It's a longer discussion why this kind of technique is extremely challenging to implement with `UIKit`, but it has to do with the fact that `Texture` prepares content in advance, giving it a buffer of time where it can spread out the creation of these objects in tiny chunks. If it doesn't finish by the time it needs to be on screen, then it finishes the rest of what needs to be created in a single chunk. `UIKit` has no similar mechanisms to create things in advance, and there is always just one huge chunk as a view controller or cell needs to come on screen. + +**ASRunLoopQueue is enabled by default when running Texture.** A developer does not need to be aware of it's existence except to know that it helps reduce main thread blockage. diff --git a/submodules/AsyncDisplayKit/docs/_docs/asviewcontroller.md b/submodules/AsyncDisplayKit/docs/_docs/asviewcontroller.md new file mode 100755 index 0000000000..9cba5be9e5 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/asviewcontroller.md @@ -0,0 +1,55 @@ +--- +title: ASViewController +layout: docs +permalink: /docs/asviewcontroller.html +prevPage: +nextPage: aspagernode.html +--- + +`ASViewController` is a direct subclass of `UIViewController`. For the most part, it can be used in place of any `UIViewController` relatively easily. + +The main difference is that you construct and return the node you'd like managed as opposed to the way `UIViewController` provides a view of its own. + +Consider the following `ASViewController` subclass that would like to use a custom table node as its managed node. + +
+SwiftObjective-C +
+
+- (instancetype)initWithModel:(NSArray *)models
+{
+    ASTableNode *tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];
+
+    if (!(self = [super initWithNode:tableNode])) { return nil; }
+
+    self.models = models;
+    
+    self.tableNode = tableNode;
+    self.tableNode.dataSource = self;
+    
+    return self;
+}
+
+ + +
+
+ +The most important line is: + +`if (!(self = [super initWithNode:tableNode])) { return nil; }` + +As you can see, `ASViewController`'s are initialized with a node of your choosing. diff --git a/submodules/AsyncDisplayKit/docs/_docs/asvisibility.md b/submodules/AsyncDisplayKit/docs/_docs/asvisibility.md new file mode 100755 index 0000000000..6ca8ef2de8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/asvisibility.md @@ -0,0 +1,15 @@ +--- +title: ASVisibility +layout: docs +permalink: /docs/asvisibility.html +prevPage: debug-tool-ASRangeController.html +nextPage: asenvironment.html +--- + +`ASNavigationController` and `ASTabBarController` both implement the `ASVisibility` protocol. These classes can be used even without `ASDisplayNodes`, making them suitable base classes for your inheritance hierarchy. For any child view controllers that are `ASViewControllers`, these classes know the exact number of user taps it would take to make the view controller visible (0 if currently visible). + +Knowing a view controller’s visibility depth allows view controllers to automatically take appropriate actions as a user approaches or leaves them. Non-default tabs in an app might preload some of their data; a controller 3 levels deep in a navigation stack might progressively free memory for images, text, and fetched data as it gets deeper. + +Any container view controller can implement a simple protocol to integrate with the system. For example, `ASNavigationController` will return a visibility depth of it's own `visibilityDepth` + 1 for a view controller that would be revealed by tapping the back button once. + +You can opt into some of this behavior automatically by enabling `automaticallyAdjustRangeModeBasedOnViewEvents` on `ASViewController`s. With this enabled, if either the view controller or its node conform to `ASRangeControllerUpdateRangeProtocol` (`ASCollectionNode` and `ASTableNode` do by default), the ranges will automatically be decreased as the visibility depth increases to save memory. diff --git a/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-basics.md b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-basics.md new file mode 100755 index 0000000000..128a1040d4 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-basics.md @@ -0,0 +1,36 @@ +--- +title: Layout Basics +layout: docs +permalink: /docs/automatic-layout-basics.html +prevPage: scroll-node.html +nextPage: automatic-layout-containers.html +--- + +## Box Model Layout + +ASLayout is an automatic, asynchronous, purely Objective-C box model layout feature. It is a simplified version of CSS flex box, loosely inspired by ComponentKit’s Layout. It is designed to make your layouts extensible and reusable. + +`UIView` instances store position and size in their `center` and `bounds` properties. As constraints change, Core Animation performs a layout pass to call `layoutSubviews`, asking views to update these properties on their subviews. + +`` instances (all ASDisplayNodes and subclasses) do not have any size or position information. Instead, Texture calls the `layoutSpecThatFits:` method with a given size constraint and the component must return a structure describing both its size, and the position and sizes of its children. + +## Terminology + +The terminology is a bit confusing, so here is a brief description of all of the Texture automatic layout players: + +Items that conform to the **\ protocol** declares a method for measuring the layout of an object. A layout is defined by an ASLayout return value, and must specify 1) the size (but not position) of the layoutable object, and 2) the size and position of all of its immediate child objects. The tree recursion is driven by parents requesting layouts from their children in order to determine their size, followed by the parents setting the position of the children once the size is known. + +This protocol also implements a "family" of layoutable protocols - the `AS{*}LayoutSpec` protocols. These protocols contain layout options that can be used for specific layout specs. For example, `ASStackLayoutSpec` has options defining how a layoutable should shrink or grow based upon available space. These layout options are all stored in an `ASLayoutOptions` class (that is defined in `ASLayoutablePrivate`). Generally you needn't worry about the layout options class, as the layoutable protocols allow all direct access to the options via convenience properties. If you are creating custom layout spec, then you can extend the backing layout options class to accommodate any new layout options. + +All ASDisplayNodes and subclasses as well as the `ASLayoutSpecs` conform to this protocol. + +An **`ASLayoutSpec`** is an immutable object that describes a layout. Creation of a layout spec should only happen by a user in layoutSpecThatFits:. During that method, a layout spec can be created and mutated. Once it is passed back to Texture, the isMutable flag will be set to NO and any further mutations will cause an assert. + +Every ASLayoutSpec must act on at least one child. The ASLayoutSpec has the responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, only require a single child. Others, have multiple. + +You don’t need to be aware of **`ASLayout`** except to know that it represents a computed immutable layout tree and is returned by objects conforming to the `` protocol. + +## Layout for UIKit Components: +- for UIViews that are added directly, you will still need to manually lay it out in `didLoad:` +- for UIViews that are added via `[ASDisplayNode initWithViewBlock:]` or its variants, you can then include it in `layoutSpecThatFits:` + diff --git a/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-containers.md b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-containers.md new file mode 100755 index 0000000000..b612b37c78 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-containers.md @@ -0,0 +1,192 @@ +--- +title: LayoutSpecs +layout: docs +permalink: /docs/automatic-layout-containers.html +prevPage: scroll-node.html +nextPage: layout-api-debugging.html +--- + +Texture includes a library of `layoutSpec` components that can be composed to declaratively specify a layout. + +The **child(ren) of a layoutSpec may be a node, a layoutSpec or a combination of the two types.** In the below image, an `ASStackLayoutSpec` (vertical) containing a text node and an image node, is wrapped in another `ASStackLayoutSpec` (horizontal) with another text node. + + + +Both nodes and layoutSpecs conform to the `` protocol. Any `ASLayoutable` object may be the child of a layoutSpec. ASLayoutable properties may be applied to `ASLayoutable` objects to create complex UI designs. + +### Single Child layoutSpecs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LayoutSpecDescription
ASInsetLayoutSpec

Applies an inset margin around a component.

The object that is being inset must have an intrinsic size.

ASOverlayLayoutSpec

Lays out a component, stretching another component on top of it as an overlay.

The underlay object must have an intrinsic size. Additionally, the order in which subnodes are added matters for this layoutSpec; the overlay object must be added as a subnode to the parent node after the underlay object.

ASBackgroundLayoutSpec

Lays out a component, stretching another component behind it as a backdrop.

The foreground object must have an intrinsic size. The order in which subnodes are added matters for this layoutSpec; the background object must be added as a subnode to the parent node before the foreground object.

ASCenterLayoutSpec

Centers a component in the available space.

The ASCenterLayoutSpec must have an intrinisic size.

ASRatioLayoutSpec

Lays out a component at a fixed aspect ratio (which can be scaled).

This spec is great for objects that do not have an intrinisic size, such as ASNetworkImageNodes and ASVideoNodes.

ASRelativeLayoutSpec

Lays out a component and positions it within the layout bounds according to vertical and horizontal positional specifiers. Similar to the “9-part” image areas, a child can be positioned at any of the 4 corners, or the middle of any of the 4 edges, as well as the center.

ASLayoutSpec

Can be used as a spacer in a stack spec with other children, when .flexGrow and/or .flexShrink is applied.

This class can also be subclassed to create custom layout specs - advanced Texture only!

+ +### Multiple Child(ren) layoutSpecs + +The following layoutSpecs may contain one or more children. + + + + + + + + + + + + + + +
LayoutSpecDescription
ASStackLayoutSpec

Allows you to stack components vertically or horizontally and specify how they should be flexed and aligned to fit in the available space.

This is the most common layoutSpec.

ASStaticLayoutSpecAllows positioning children at fixed offsets using the .sizeRange and .layoutPosition ASLayoutable properties.
+ +### ASLayoutable Properties + +The following properties can be applied to both nodes _and_ `layoutSpec`s; both conform to the `ASLayoutable` protocol. + +### ASStackLayoutable Properties + +The following properties may be set on any node or `layoutSpec`s, but will only apply to those who are a **child of a stack** `layoutSpec`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
CGFloat .spacingBeforeAdditional space to place before this object in the stacking direction.
CGFloat .spacingAfterAdditional space to place after this object in the stacking direction.
BOOL .flexGrowIf the sum of childrens' stack dimensions is less than the minimum size, should this object grow? Used when attached to a stack layout.
BOOL .flexShrinkIf the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink? Used when attached to a stack layout.
ASRelativeDimension .flexBasisSpecifies the initial size for this object, in the stack dimension (horizontal or vertical), before the flexGrow or flexShrink properties are applied and the remaining space is distributed.
ASStackLayoutAlignSelf alignSelfOrientation of the object along cross axis, overriding alignItems. Used when attached to a stack layout.
CGFloat .ascenderUsed for baseline alignment. The distance from the top of the object to its baseline.
CGFloat .descenderUsed for baseline alignment. The distance from the baseline of the object to its bottom.
+ +### ASStaticLayoutable Properties + +The following properties may be set on any node or `layoutSpec`s, but will only apply to those who are a **child of a static** `layoutSpec`. + + + + + + + + + + + + + + +
PropertyDescription
.sizeRangeIf specified, the child's size is restricted according to this ASRelativeSizeRange. Percentages are resolved relative to the static layout spec.
.layoutPositionThe CGPoint position of this object within its parent spec.
+ +### Providing Intrinsic Sizes for Leaf Nodes + +Texture's layout is recursive, starting at the layoutSpec returned from `layoutSpecThatFits:` and proceeding down until it reaches the leaf nodes included in any nested `layoutSpec`s. + +Some leaf nodes provide their own intrinsic size, such as `ASTextNode` or `ASImageNode`. An attributed string or an image have their own sizes. Other leaf nodes require an intrinsic size to be set. + +**Nodes that require the developer to provide an intrinsic size:** + + - `ASDisplayNode` custom subclasses may provide their intrinisc size by implementing `calculateSizeThatFits:`. + - `ASNetworkImageNode` or `ASMultiplexImageNode` have no intrinsic size until the image is downloaded. + - `ASVideoNode` or `ASVideoNodePlayer` have no intrinsic size until the video is downloaded. + + +To provide an intrinisc size for these nodes, you can set one of the following: + + 1. implement `calculateSizeThatFits:` for **custom ASDisplayNode subclasses** only. + 2. set `.preferredFrameSize` + 3. set `.sizeRange` for children of **static** nodes only. + + +Note that `.preferredFrameSize` is not considered by `ASTextNodes`. Also, setting .sizeRange on a node will override the node's intrinisic size provided by `calculateSizeThatFits:`. + +### Common Confusions + +There are two main confusions that developers have when using layoutSpecs + + 1. Certain ASLayoutable properties only apply to children of stack nodes, while other properties only apply to children of static nodes. All ASLayoutable properties can be applied to any node or layoutSpec, however certain properties will only take effect depending on the type of the parent layoutSpec they are wrapped in. These differences are highlighted above in the ASStackLayoutable Properties and ASStaticLayoutable Properties sections. + 2. Have I set an intrinsic size for all of my leaf nodes? + + +#### I set `.flexGrow` on my node, but it doesn't grow? + +Upward propogation of `ASLayoutable` properties is currently disabled. Thus, in certain situations, the `.flexGrow` property must be manually applied to the containers. Two common examples of this that we see include: + +- a node (with `flexGrow` enabled) is wrapped in a static layoutSpec, wrapped in a stack layoutSpec. **solution**: enable `flexGrow` on the static layoutSpec as well. +- a node (with `flexGrow` enabled) is wrapped in an inset spec. **solution**: enable `flexGrow` on the inset spec as well. + + +#### I want to provide a size for my image, but I don't want to hard code the size. + +#### Why won't my stack spec span the full width? + +#### Difference between `ASInsetLayoutSpec` and `ASOverlayLayoutSpec` + +An overlay spec requires the underlay object (object to which the overlay item will be applied) to have an intrinsic size. It will center the overlay object in the middle of this area. + +An inset spec requires its object to have an intrinsic size. It adds the inset padding to this size to calculate the final size of the inset spec. + + + +### Best Practices + - Texture layout is called on a background thread. Do not access the device screen bounds, or any other UIKit methods in `layoutSpecThatFits:`. + - don't wrap everything in a staticLayoutSpec? + - avoid using preferred frame size for everything - won't respond nicely to device rotation or device sizing differences? diff --git a/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-examples-2.md b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-examples-2.md new file mode 100755 index 0000000000..07da8df131 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-examples-2.md @@ -0,0 +1,264 @@ +--- +title: Layout Examples +layout: docs +permalink: /docs/automatic-layout-examples-2.html +prevPage: layout2-quickstart.html +nextPage: layout2-layoutspec-types.html +--- + +Check out the layout specs example project to play around with the code below. + +## Simple Header with Left and Right Justified Text + + + +To create this layout, we will use a: + +- a vertical `ASStackLayoutSpec` +- a horizontal `ASStackLayoutSpec` +- `ASInsetLayoutSpec` to inset the entire header + +The diagram below shows the composition of the layout elements (nodes + layout specs). + + + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  // when the username / location text is too long, 
+  // shrink the stack to fit onscreen rather than push content to the right, offscreen
+  ASStackLayoutSpec *nameLocationStack = [ASStackLayoutSpec verticalStackLayoutSpec];
+  nameLocationStack.style.flexShrink = 1.0;
+  nameLocationStack.style.flexGrow = 1.0;
+  
+  // if fetching post location data from server, 
+  // check if it is available yet and include it if so
+  if (_postLocationNode.attributedText) {
+    nameLocationStack.children = @[_usernameNode, _postLocationNode];
+  } else {
+    nameLocationStack.children = @[_usernameNode];
+  }
+  
+  // horizontal stack
+  ASStackLayoutSpec *headerStackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
+                                                                               spacing:40
+                                                                        justifyContent:ASStackLayoutJustifyContentStart
+                                                                            alignItems:ASStackLayoutAlignItemsCenter
+                                                                              children:@[nameLocationStack, _postTimeNode]];
+  
+  // inset the horizontal stack
+  return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 10, 0, 10) child:headerStackSpec];
+}
+  
+ +
+
+ +Rotate the example project from portrait to landscape to see how the spacer grows and shrinks. + +## Photo with Inset Text Overlay + + + +To create this layout, we will use a: + +- `ASInsetLayoutSpec` to inset the text +- `ASOverlayLayoutSpec` to overlay the inset text spec on top of the photo + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  _photoNode.style.preferredSize = CGSizeMake(USER_IMAGE_HEIGHT*2, USER_IMAGE_HEIGHT*2);
+
+  // INIFINITY is used to make the inset unbounded
+  UIEdgeInsets insets = UIEdgeInsetsMake(INFINITY, 12, 12, 12);
+  ASInsetLayoutSpec *textInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:_titleNode];
+  
+  return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:_photoNode overlay:textInsetSpec];
+}
+  
+ +
+
+ +## Photo with Outset Icon Overlay + + + +To create this layout, we will use a: + +- `ASAbsoluteLayoutSpec` to place the photo and icon which have been individually sized and positioned using their `ASLayoutable` properties + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  _iconNode.style.preferredSize = CGSizeMake(40, 40);
+  _iconNode.style.layoutPosition = CGPointMake(150, 0);
+  
+  _photoNode.style.preferredSize = CGSizeMake(150, 150);
+  _photoNode.style.layoutPosition = CGPointMake(40 / 2.0, 40 / 2.0);
+  
+  return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithSizing:ASAbsoluteLayoutSpecSizingSizeToFit
+                                                   children:@[_photoNode, _iconNode]];
+}
+  
+ +
+
+ + + +## Simple Inset Text Cell + + + +To recreate the layout of a single cell as is used in Pinterest's search view above, we will use a: + +- `ASInsetLayoutSpec` to inset the text +- `ASCenterLayoutSpec` to center the text according to the specified properties + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+    UIEdgeInsets insets = UIEdgeInsetsMake(0, 12, 4, 4);
+    ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets
+                                                                      child:_titleNode];
+
+    return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringY
+                                                      sizingOptions:ASCenterLayoutSpecSizingOptionMinimumX
+                                                              child:inset];
+}
+  
+ +
+
+ +## Top and Bottom Separator Lines + + + +To create the layout above, we will use a: + +- a `ASInsetLayoutSpec` to inset the text +- a vertical `ASStackLayoutSpec` to stack the two separator lines on the top and bottom of the text + +The diagram below shows the composition of the layoutables (layout specs + nodes). + + + +The following code can also be found in the `ASLayoutSpecPlayground` [example project](). + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  _topSeparator.style.flexGrow = 1.0;
+  _bottomSeparator.style.flexGrow = 1.0;
+
+  ASInsetLayoutSpec *insetContentSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(20, 20, 20, 20) child:_textNode];
+
+  return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
+                                                 spacing:0
+                                          justifyContent:ASStackLayoutJustifyContentCenter
+                                              alignItems:ASStackLayoutAlignItemsStretch
+                                                children:@[_topSeparator, insetContentSpec, _bottomSeparator]];
+}
+  
+ +
+
diff --git a/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-examples.md b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-examples.md new file mode 100755 index 0000000000..1523720935 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/automatic-layout-examples.md @@ -0,0 +1,204 @@ +--- +title: Layout Examples +layout: docs +permalink: /docs/automatic-layout-examples.html +prevPage: automatic-layout-containers.html +nextPage: automatic-layout-debugging.html +--- + +Three examples in increasing order of complexity. +#NSSpain Talk Example + + + +
+SwiftObjective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constraint
+{
+  ASStackLayoutSpec *vStack = [[ASStackLayoutSpec alloc] init];
+  
+  [vStack setChildren:@[titleNode, bodyNode];
+
+  ASStackLayoutSpec *hstack = [[ASStackLayoutSpec alloc] init];
+  hStack.direction          = ASStackLayoutDirectionHorizontal;
+  hStack.spacing            = 5.0;
+
+  [hStack setChildren:@[imageNode, vStack]];
+  
+  ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(5,5,5,5) child:hStack];
+
+  return insetSpec;
+}
+
+ +
+
+ +###Discussion + +#Social App Layout + + + +
+SwiftObjective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  // header stack
+  _userAvatarImageView.preferredFrameSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT);  // constrain avatar image frame size
+  
+  ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init];
+  spacer.flexGrow      = YES;
+
+  ASStackLayoutSpec *headerStack = [ASStackLayoutSpec horizontalStackLayoutSpec];
+  headerStack.alignItems         = ASStackLayoutAlignItemsCenter;       // center items vertically in horizontal stack
+  headerStack.justifyContent     = ASStackLayoutJustifyContentStart;    // justify content to left side of header stack
+  headerStack.spacing            = HORIZONTAL_BUFFER;
+
+  [headerStack setChildren:@[_userAvatarImageView, _userNameLabel, spacer, _photoTimeIntervalSincePostLabel]];
+  
+  // header inset stack
+  
+  UIEdgeInsets insets                = UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER);
+  ASInsetLayoutSpec *headerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:headerStack];
+  headerWithInset.flexShrink = YES;
+  
+  // vertical stack
+  
+  CGFloat cellWidth                  = constrainedSize.max.width;
+  _photoImageView.preferredFrameSize = CGSizeMake(cellWidth, cellWidth);  // constrain photo frame size
+  
+  ASStackLayoutSpec *verticalStack   = [ASStackLayoutSpec verticalStackLayoutSpec];
+  verticalStack.alignItems           = ASStackLayoutAlignItemsStretch;    // stretch headerStack to fill horizontal space
+  
+  [verticalStack setChildren:@[headerWithInset, _photoImageView, footerWithInset]];
+
+  return verticalStack;
+}
+
+ +
+
+ +###Discussion + +Get the full Texture project at examples/ASDKgram. + +#Social App Layout 2 + + + +
+SwiftObjective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize {
+
+  ASLayoutSpec *textSpec  = [self textSpec];
+  ASLayoutSpec *imageSpec = [self imageSpecWithSize:constrainedSize];
+  ASOverlayLayoutSpec *soldOutOverImage = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imageSpec 
+                                                                                  overlay:[self soldOutLabelSpec]];
+  
+  NSArray *stackChildren = @[soldOutOverImage, textSpec];
+  
+  ASStackLayoutSpec *mainStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical 
+                                                                         spacing:0.0
+                                                                  justifyContent:ASStackLayoutJustifyContentStart
+                                                                      alignItems:ASStackLayoutAlignItemsStretch          
+                                                                        children:stackChildren];
+  
+  ASOverlayLayoutSpec *soldOutOverlay = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:mainStack 
+                                                                                overlay:self.soldOutOverlay];
+  
+  return soldOutOverlay;
+}
+
+- (ASLayoutSpec *)textSpec {
+  CGFloat kInsetHorizontal        = 16.0;
+  CGFloat kInsetTop               = 6.0;
+  CGFloat kInsetBottom            = 0.0;
+  UIEdgeInsets textInsets         = UIEdgeInsetsMake(kInsetTop, kInsetHorizontal, kInsetBottom, kInsetHorizontal);
+  
+  ASLayoutSpec *verticalSpacer    = [[ASLayoutSpec alloc] init];
+  verticalSpacer.flexGrow         = YES;
+  
+  ASLayoutSpec *horizontalSpacer1 = [[ASLayoutSpec alloc] init];
+  horizontalSpacer1.flexGrow      = YES;
+  
+  ASLayoutSpec *horizontalSpacer2 = [[ASLayoutSpec alloc] init];
+  horizontalSpacer2.flexGrow      = YES;
+  
+  NSArray *info1Children = @[self.firstInfoLabel, self.distanceLabel, horizontalSpacer1, self.originalPriceLabel];
+  NSArray *info2Children = @[self.secondInfoLabel, horizontalSpacer2, self.finalPriceLabel];
+  if ([ItemNode isRTL]) {
+    info1Children = [[info1Children reverseObjectEnumerator] allObjects];
+    info2Children = [[info2Children reverseObjectEnumerator] allObjects];
+  }
+  
+  ASStackLayoutSpec *info1Stack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal 
+                                                                          spacing:1.0
+                                                                   justifyContent:ASStackLayoutJustifyContentStart 
+                                                                       alignItems:ASStackLayoutAlignItemsBaselineLast children:info1Children];
+  
+  ASStackLayoutSpec *info2Stack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal 
+                                                                          spacing:0.0
+                                                                   justifyContent:ASStackLayoutJustifyContentCenter 
+                                                                       alignItems:ASStackLayoutAlignItemsBaselineLast children:info2Children];
+  
+  ASStackLayoutSpec *textStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical 
+                                                                         spacing:0.0
+                                                                  justifyContent:ASStackLayoutJustifyContentEnd
+                                                                      alignItems:ASStackLayoutAlignItemsStretch
+                                                                        children:@[self.titleLabel, verticalSpacer, info1Stack, info2Stack]];
+  
+  ASInsetLayoutSpec *textWrapper = [ASInsetLayoutSpec insetLayoutSpecWithInsets:textInsets 
+                                                                          child:textStack];
+  textWrapper.flexGrow = YES;
+  
+  return textWrapper;
+}
+
+- (ASLayoutSpec *)imageSpecWithSize:(ASSizeRange)constrainedSize {
+  CGFloat imageRatio = [self imageRatioFromSize:constrainedSize.max];
+  
+  ASRatioLayoutSpec *imagePlace = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:imageRatio child:self.dealImageView];
+  
+  self.badge.layoutPosition = CGPointMake(0, constrainedSize.max.height - kFixedLabelsAreaHeight - kBadgeHeight);
+  self.badge.sizeRange = ASRelativeSizeRangeMake(ASRelativeSizeMake(ASRelativeDimensionMakeWithPercent(0), ASRelativeDimensionMakeWithPoints(kBadgeHeight)), ASRelativeSizeMake(ASRelativeDimensionMakeWithPercent(1), ASRelativeDimensionMakeWithPoints(kBadgeHeight)));
+  ASStaticLayoutSpec *badgePosition = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[self.badge]];
+  
+  ASOverlayLayoutSpec *badgeOverImage = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imagePlace overlay:badgePosition];
+  badgeOverImage.flexGrow = YES;
+  
+  return badgeOverImage;
+}
+
+- (ASLayoutSpec *)soldOutLabelSpec {
+  ASCenterLayoutSpec *centerSoldOutLabel = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY 
+  sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:self.soldOutLabelFlat];
+  ASStaticLayoutSpec *soldOutBG = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[self.soldOutLabelBackground]];
+  ASCenterLayoutSpec *centerSoldOut = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY   sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:soldOutBG];
+  ASBackgroundLayoutSpec *soldOutLabelOverBackground = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:centerSoldOutLabel background:centerSoldOut];
+  return soldOutLabelOverBackground;
+}
+
+ +
+
+ +###Discussion + +Get the full Texture project at examples/CatDealsCollectionView. diff --git a/submodules/AsyncDisplayKit/docs/_docs/automatic-subnode-mgmt.md b/submodules/AsyncDisplayKit/docs/_docs/automatic-subnode-mgmt.md new file mode 100755 index 0000000000..e4ff534206 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/automatic-subnode-mgmt.md @@ -0,0 +1,296 @@ +--- +title: Automatic Subnode Management +layout: docs +permalink: /docs/automatic-subnode-mgmt.html +prevPage: batch-fetching-api.html +nextPage: inversion.html +--- + +Enabling Automatic Subnode Management (ASM) is required to use the Layout Transition API. However, apps that don't require animations can still benefit from the reduction in code size that this feature enables. + +When enabled, ASM means that your nodes no longer require `addSubnode:` or `removeFromSupernode` method calls. The presence or absence of the ASM node _and_ its subnodes is completely determined in its `layoutSpecThatFits:` method. + +### Example ### +
+Consider the following intialization method from the PhotoCellNode class in ASDKgram sample app. This ASCellNode subclass produces a simple social media photo feed cell. + +In the "Original Code" we see the familiar `addSubnode:` calls in bold. In the "Code with ASM" these have been removed and replaced with a single line that enables ASM. + +By setting `.automaticallyManagesSubnodes` to `YES` on the `ASCellNode`, we _no longer_ need to call `addSubnode:` for each of the `ASCellNode`'s subnodes. These `subNodes` will be present in the node hierarchy as long as this class' `layoutSpecThatFits:` method includes them. + + +Original code +
+ + Objective-C + Swift + +
+
+- (instancetype)initWithPhotoObject:(PhotoModel *)photo;
+{
+  self = [super init];
+  
+  if (self) {
+    _photoModel = photo;
+    
+    _userAvatarImageNode = [[ASNetworkImageNode alloc] init];
+    _userAvatarImageNode.URL = photo.ownerUserProfile.userPicURL;
+    [self addSubnode:_userAvatarImageNode];
+
+    _photoImageNode = [[ASNetworkImageNode alloc] init];
+    _photoImageNode.URL = photo.URL;
+    [self addSubnode:_photoImageNode];
+
+    _userNameTextNode = [[ASTextNode alloc] init];
+    _userNameTextNode.attributedString = [photo.ownerUserProfile usernameAttributedStringWithFontSize:FONT_SIZE];
+    [self addSubnode:_userNameTextNode];
+    
+    _photoLocationTextNode = [[ASTextNode alloc] init];
+    [photo.location reverseGeocodedLocationWithCompletionBlock:^(LocationModel *locationModel) {
+      if (locationModel == _photoModel.location) {
+        _photoLocationTextNode.attributedString = [photo locationAttributedStringWithFontSize:FONT_SIZE];
+        [self setNeedsLayout];
+      }
+    }];
+    [self addSubnode:_photoLocationTextNode];
+  }
+  
+  return self;
+}
+
+ +
+
+ +Code with ASM +
+ + Objective-C + Swift + +
+
+- (instancetype)initWithPhotoObject:(PhotoModel *)photo;
+{
+  self = [super init];
+  
+  if (self) {
+    self.automaticallyManagesSubnodes = YES;
+    
+    _photoModel = photo;
+    
+    _userAvatarImageNode = [[ASNetworkImageNode alloc] init];
+    _userAvatarImageNode.URL = photo.ownerUserProfile.userPicURL;
+
+    _photoImageNode = [[ASNetworkImageNode alloc] init];
+    _photoImageNode.URL = photo.URL;
+
+    _userNameTextNode = [[ASTextNode alloc] init];
+    _userNameTextNode.attributedString = [photo.ownerUserProfile usernameAttributedStringWithFontSize:FONT_SIZE];
+    
+    _photoLocationTextNode = [[ASTextNode alloc] init];
+    [photo.location reverseGeocodedLocationWithCompletionBlock:^(LocationModel *locationModel) {
+      if (locationModel == _photoModel.location) {
+        _photoLocationTextNode.attributedString = [photo locationAttributedStringWithFontSize:FONT_SIZE];
+        [self setNeedsLayout];
+      }
+    }];
+  }
+  
+  return self;
+}
+
+ +
+
+ +Several of the elements in this cell - `_userAvatarImageNode`, `_photoImageNode`, and `_photoLocationLabel` depend on seperate data fetches from the network that could return at any time. When should they be added to the UI? + +ASM knows whether or not to include these elements in the UI based on the information provided in the cell's `ASLayoutSpec`. + +
+An ASLayoutSpec completely describes the UI of a view in your app by specifying the hierarchy state of a node and its subnodes. An ASLayoutSpec is returned by a node from its layoutSpecThatFits: method. +
+ +**It is your job to construct a `layoutSpecThatFits:` that handles how the UI should look with and without these elements.** + +Consider the abreviated `layoutSpecThatFits:` method for the `ASCellNode` subclass above. + +
+ +Objective-C +Swift + + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{  
+  ASStackLayoutSpec *headerSubStack = [ASStackLayoutSpec verticalStackLayoutSpec];
+  headerSubStack.flexShrink         = YES;
+  if (_photoLocationLabel.attributedString) {
+    [headerSubStack setChildren:@[_userNameLabel, _photoLocationLabel]];
+  } else {
+    [headerSubStack setChildren:@[_userNameLabel]];
+  }
+  
+  _userAvatarImageNode.preferredFrameSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT);     // constrain avatar image frame size
+
+  ASLayoutSpec *spacer           = [[ASLayoutSpec alloc] init]; 
+  spacer.flexGrow                = YES;
+  
+  UIEdgeInsets avatarInsets      = UIEdgeInsetsMake(HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER);
+  ASInsetLayoutSpec *avatarInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:avatarInsets child:_userAvatarImageNode];
+
+  ASStackLayoutSpec *headerStack = [ASStackLayoutSpec horizontalStackLayoutSpec];
+  headerStack.alignItems         = ASStackLayoutAlignItemsCenter;                     // center items vertically in horizontal stack
+  headerStack.justifyContent     = ASStackLayoutJustifyContentStart;                  // justify content to the left side of the header stack
+  [headerStack setChildren:@[avatarInset, headerSubStack, spacer]];
+  
+  // header inset stack
+  UIEdgeInsets insets                = UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER);
+  ASInsetLayoutSpec *headerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:headerStack];
+  
+  // footer inset stack
+  UIEdgeInsets footerInsets          = UIEdgeInsetsMake(VERTICAL_BUFFER, HORIZONTAL_BUFFER, VERTICAL_BUFFER, HORIZONTAL_BUFFER);
+  ASInsetLayoutSpec *footerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:footerInsets child:_photoCommentsNode];
+  
+  // vertical stack
+  CGFloat cellWidth                  = constrainedSize.max.width;
+  _photoImageNode.preferredFrameSize = CGSizeMake(cellWidth, cellWidth);              // constrain photo frame size
+  
+  ASStackLayoutSpec *verticalStack   = [ASStackLayoutSpec verticalStackLayoutSpec];
+  verticalStack.alignItems           = ASStackLayoutAlignItemsStretch;                // stretch headerStack to fill horizontal space
+  [verticalStack setChildren:@[headerWithInset, _photoImageNode, footerWithInset]];
+
+  return verticalStack;
+}
+
+ + +
+
+ + +Here you can see that the children of the `headerSubStack` depend on whether or not the `_photoLocationLabel` attributed string has returned from the reverseGeocode process yet. + +The `_userAvatarImageNode`, `_photoImageNode`, and `_photoCommentsNode` are added into the ASLayoutSpec, but will not show up until their data fetches return. + +### Updating an ASLayoutSpec ### +
+**If something happens that you know will change your `ASLayoutSpec`, it is your job to call `setNeedsLayout`**. This is equivalent to `transitionLayout:duration:0` in the Transition Layout API. You can see this call in the completion block of the `photo.location reverseGeocodedLocationWithCompletionBlock:` call in the first code block. + +An appropriately constructed ASLayoutSpec will know which subnodes need to be added, removed or animated. + +Try out the ASDKgram sample app after looking at the code above, and you will see how simple it is to code an `ASCellNode` whose layout is responsive to numerous, individual data fetches and returns. While the `ASLayoutSpec` is coded in a way that leaves holes for the avatar and photo to populate, you can see how the cell's height will automatically adjust to accomodate the comments node at the bottom of the photo. + +This is just a simple example, but this feature has many more powerful uses. + +
+Warning: addSubnode: and removeFromSupernode should never be called on a node that has ASM enabled. Doing so could cause the following exception - "A flattened layout must consist exclusively of node sublayouts". +
diff --git a/submodules/AsyncDisplayKit/docs/_docs/batch-fetching-api.md b/submodules/AsyncDisplayKit/docs/_docs/batch-fetching-api.md new file mode 100755 index 0000000000..5fd9044697 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/batch-fetching-api.md @@ -0,0 +1,151 @@ +--- +title: Batch Fetching API +layout: docs +permalink: /docs/batch-fetching-api.html +prevPage: hit-test-slop.html +nextPage: automatic-subnode-mgmt.html +--- + +Texture's Batch Fetching API makes it easy to add fetching chunks of new data. Usually this would be done in a `-scrollViewDidScroll:` method, but Texture provides a more structured mechanism. + +By default, as a user is scrolling, when they approach the point in the table or collection where they are 2 "screens" away from the end of the current content, the table will try to fetch more data. + +If you'd like to configure how far away from the end you should be, just change the `leadingScreensForBatching` property on an `ASTableView` or `ASCollectionView` to something else. + +
+SwiftObjective-C + +
+
+tableNode.view.leadingScreensForBatching = 3.0;  // overriding default of 2.0
+
+ +
+
+ +### Batch Fetching Delegate Methods + +The first thing you have to do in order to support batch fetching, is implement a method that decides if it's an appropriate time to load new content or not. + +For tables it would look something like: + +
+SwiftObjective-C + +
+
+- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode
+{
+  if (_weNeedMoreContent) {
+    return YES;
+  }
+
+  return NO;
+}
+
+ +
+
+ +and for collections: + +
+SwiftObjective-C + +
+
+
+- (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode
+{
+  if (_weNeedMoreContent) {
+    return YES;
+  }
+
+  return NO;
+}
+
+ +
+
+ +These methods will be called when the user has scrolled into the batch fetching range, and their answer will determine if another request actually needs to be made or not. Usually this decision is based on if there is still data to fetch. + +If you return NO, then no new batch fetching process will happen. If you return YES, the batch fetching mechanism will start and the following method will be called next. + +`-tableNode:willBeginBatchFetchWithContext:` + +or + +`-collectionNode:willBeginBatchFetchWithContext:` + +This is where you should actually fetch data, be it from a web API or some local database. + +
+Note: This method will always be called on a background thread. This means, if you need to do any work on the main thread, you should dispatch it to the main thread and then proceed with the work needed in order to finish the batch fetch operation. +
+ +
+SwiftObjective-C + +
+
+- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context 
+{
+  // Fetch data most of the time asynchronoulsy from an API or local database
+  NSArray *newPhotos = [SomeSource getNewPhotos];
+
+  // Insert data into table or collection node
+  [self insertNewRowsInTableNode:newPhotos];
+
+  // Decide if it's still necessary to trigger more batch fetches in the future
+  _stillDataToFetch = ...;
+
+  // Properly finish the batch fetch
+  [context completeBatchFetching:YES];
+}
+
+ +
+
+ +Once you've finished fetching your data, it is very important to let Texture know that you have finished the process. To do that, you need to call `-completeBatchFetching:` on the `context` object that was passed in with a parameter value of `YES`. This assures that the whole batch fetching mechanism stays in sync and the next batch fetching cycle can happen. Only by passing `YES` will the context know to attempt another batch update when necessary. + +Check out the following sample apps to see the batch fetching API in action: + diff --git a/submodules/AsyncDisplayKit/docs/_docs/button-node.md b/submodules/AsyncDisplayKit/docs/_docs/button-node.md new file mode 100755 index 0000000000..11eb6ca955 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/button-node.md @@ -0,0 +1,98 @@ +--- +title: ASButtonNode +layout: docs +permalink: /docs/button-node.html +prevPage: cell-node.html +nextPage: text-node.html +--- + +### Basic Usage + +`ASButtonNode` subclasses `ASControlNode` in the same way `UIButton` subclasses `UIControl`. In contrast, being able to layer back the subnodes of every button can significantly lighten main thread impact relative to `UIButton`. + +### Control State + +If you've used `-setTitle:forControlState:` then you already know how to set up an ASButtonNode. The `ASButtonNode` version adds in a few parameters for conveniently setting attributes. + +
+SwiftObjective-C + +
+
+[buttonNode setTitle:@"Button Title Normal" withFont:nil withColor:[UIColor blueColor] forState:ASControlStateNormal];
+
+ +
+
+ +If you need even more control, you can also opt to use the attributed string version directly: + +
+SwiftObjective-C + +
+
+[self.buttonNode setAttributedTitle:attributedTitle forState:ASControlStateNormal];
+
+ +
+
+ +### Target-Action Pairs + +Again, analagous to UIKit, you can add sets of target-action pairs to respond to various events. + +
+SwiftObjective-C + +
+
+[buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchUpInside];
+
+ +
+
+ +### Content Alignment + +`ASButtonNode` offers both `contentVerticalAlignment` and `contentHorizontalAlignment` properties. This allows you to easily set the alignment of the titleLabel or image you're using for your button. + +
+SwiftObjective-C + +
+
+self.buttonNode.contentVerticalAlignment = ASVerticalAlignmentTop;
+self.buttonNode.contentHorizontalAlignment = ASHorizontalAlignmentMiddle;
+
+ +
+
+ +
Note: At the moment, this property will not work if you aren't using -layoutSpecThatFits:. +
+ +### Gotchas + +There are a few things that might trip up someone new to the framework. + +##### View Hierarchies +Let's say you want to add an `ASButtonNode` to the view of one of your existing view controllers. The first thing you'll notice is that setting a title for a control state doesn't seem to make your title appear. You can fix this by calling `-measure:` on the button which will cause its title label to be measured and laid out. + +The next thing you'll notice is that, if you set titles of various lengths for different control states, the button will dynamically grow and shrink as the title changes. This is because changing the title causes `-setNeedsLayout` to be called on the button. Within a node hierarchy, this makes sense, and will work as expected. + +Long story short, use an `ASViewController`. + +##### Selected State + +If you want your button to change to a "selected" state after being tapped, you'll need to do that manually. + diff --git a/submodules/AsyncDisplayKit/docs/_docs/cell-node.md b/submodules/AsyncDisplayKit/docs/_docs/cell-node.md new file mode 100755 index 0000000000..60b1cfad3f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/cell-node.md @@ -0,0 +1,142 @@ +--- +title: ASCellNode +layout: docs +permalink: /docs/cell-node.html +prevPage: display-node.html +nextPage: button-node.html +--- + +`ASCellNode`, as you may have guessed, is the cell class of Texture. Unlike the various cells in UIKit, `ASCellNode` can be used with `ASTableNodes`, `ASCollectionNodes` and `ASPagerNodes`, making it incredibly flexible. + +### 3 Ways to Party + +There are three ways in which you can implement the cells you'll use in your Texture app: subclassing `ASCellNode`, initializing with an existing `ASViewController` or using an existing UIView or `CALayer`. + +#### Subclassing + +Subclassing an `ASCellNode` is pretty much the same as subclassing a regular `ASDisplayNode`. + +Most likely, you'll write a few of the following: + +- `-init` -- Thread safe initialization. +- `-layoutSpecThatFits:` -- Return a layout spec that defines the layout of your cell. +- `-didLoad` -- Called on the main thread. Good place to add gesture recognizers, etc. +- `-layout` -- Also called on the main thread. Layout is complete after the call to super which means you can do any extra tweaking you need to do. + + +#### Initializing with an `ASViewController` + +Say you already have some type of view controller written to display a view in your app. If you want to take that view controller and drop its view in as a cell in one of the scrolling nodes or a pager node its no problem. + +For example, say you already have a view controller written that manages an `ASTableNode`. To use that table as a page in an `ASPagerNode` you can use `-initWithViewControllerBlock`. + +
+SwiftObjective-C +
+
+- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index
+{
+    NSArray *animals = self.allAnimals[index];
+    
+    ASCellNode *node = [[ASCellNode alloc] initWithViewControllerBlock:^UIViewController * _Nonnull{
+        return [[AnimalTableNodeController alloc] initWithAnimals:animals];
+    } didLoadBlock:nil];
+    
+    node.style.preferredSize = pagerNode.bounds.size;
+    
+    return node;
+}
+
+ +
+
+ +And this works for any combo of scrolling container node and `UIViewController` subclass. You want to embed random view controllers in your collection node? Go for it. + +
+Notice that you need to set the .style.preferredSize of a node created this way. Normally your nodes will implement -layoutSpecThatFits: but since these don't you'll need give the cell a size. +
+ + +#### Initializing with a `UIView` or `CALayer` + +Alternatively, if you already have a `UIView` or `CALayer` subclass that you'd like to drop in as cell you can do that instead. + +
+SwiftObjective-C +
+
+- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index
+{
+    NSArray *animal = self.animals[index];
+    
+    ASCellNode *node = [[ASCellNode alloc] initWithViewBlock:^UIView * _Nonnull{
+        return [[SomeAnimalView alloc] initWithAnimal:animal];
+    }];
+
+    node.style.preferredSize = pagerNode.bounds.size;
+    
+    return node;
+}
+
+ +
+
+ +As you can see, its roughly the same idea. That being said, if you're doing this, you may consider converting the existing `UIView` subclass to be an `ASCellNode` subclass in order to gain the advantage of asynchronous display. + +### Never Show Placeholders + +Usually, if a cell hasn't finished its display pass before it has reached the screen it will show placeholders until it has completed drawing its content. + +If placeholders are unacceptable, you can set an `ASCellNode`'s `neverShowPlaceholders` property to `YES`. + +
+SwiftObjective-C +
+
+node.neverShowPlaceholders = YES;
+
+ +
+
+ +With this property set to `YES`, the main thread will be blocked until display has completed for the cell. This is more similar to UIKit, and in fact makes Texture scrolling visually indistinguishable from UIKit's, except being faster. + +
+Using this option does not eliminate all of the performance advantages of Texture. Normally, a cell has been preloading and is almost done when it reaches the screen, so the blocking time is very short. Even if the rangeTuningParameters are set to 0 this option outperforms UIKit. While the main thread is waiting, subnode display executes concurrently. +
+ +### `UITableViewCell` specific propertys + +UITableViewCell has properties like selectionStyle, accessoryType and seperatorInset that many of us use sometimes to give the Cell more detail. For this case ASCellNode has the same (passthrough) properties that can be used. + +
+UIKits UITableViewCell contains ASCellNode as a subview. Depending how your ASLayoutSpec is defined it may occur that your Layout overlays the UITableViewCell.accessoryView and therefore is not visible. Make sure that your Layout doesn't overlay any of UITableViewCell's specific properties. +
diff --git a/submodules/AsyncDisplayKit/docs/_docs/containers-ascollectionnode.md b/submodules/AsyncDisplayKit/docs/_docs/containers-ascollectionnode.md new file mode 100755 index 0000000000..780e5c3ad9 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/containers-ascollectionnode.md @@ -0,0 +1,228 @@ +--- +title: ASCollectionNode +layout: docs +permalink: /docs/containers-ascollectionnode.html +prevPage: containers-astablenode.html +nextPage: containers-aspagernode.html +--- + +`ASCollectionNode` is equivalent to UIKit's `UICollectionView` and can be used in place of any `UICollectionView`. + +`ASCollectionNode` replaces `UICollectionView`'s required method + +
+ + Swift + Objective-C + + +
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
+  
+ + +
+
+ +with your choice of **_one_** of the following methods + +
+SwiftObjective-C + +
+
+- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath
+
+ +
+
+ +

+or +

+ +
+SwiftObjective-C + +
+
+- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
+
+ +
+
+ +It is recommended that you use the node block version of the method so that your collection node will be able to prepare and display all of its cells concurrently. + +As noted in the previous section: + +
    +
  • ASCollectionNodes do not utilize cell reuse.
  • +
  • Using the "nodeBlock" method is preferred.
  • +
  • It is very important that the returned node blocks are thread-safe.
  • +
  • ASCellNodes can be used by ASTableNode, ASCollectionNode and ASPagerNode.
  • +
+ +### Node Block Thread Safety Warning + +It is very important that node blocks be thread-safe. One aspect of that is ensuring that the data model is accessed _outside_ of the node block. Therefore, it is unlikely that you should need to use the index inside of the block. + +Consider the following `-collectionNode:nodeBlockForItemAtIndexPath:` method. + +
+SwiftObjective-C +
+
+- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PhotoModel *photoModel = [_photoFeed objectAtIndex:indexPath.row];
+    
+    // this may be executed on a background thread - it is important to make sure it is thread safe
+    ASCellNode *(^cellNodeBlock)() = ^ASCellNode *() {
+        PhotoCellNode *cellNode = [[PhotoCellNode alloc] initWithPhoto:photoModel];
+        cellNode.delegate = self;
+        return cellNode;
+    };
+    
+    return cellNodeBlock;
+}
+  
+ + +
+
+ +In the example above, you can see how the index is used to access the photo model before creating the node block. + +### Replacing a UICollectionViewController with an ASViewController + +Texture does not offer an equivalent to UICollectionViewController. Instead, you can use the flexibility of ASViewController to recreate any type of UI...ViewController. + +Consider, the following ASViewController subclass. + +An ASCollectionNode is assigned to be managed by an `ASViewController` in its `-initWithNode:` designated initializer method, thus making it a sort of ASCollectionNodeController. + +
+SwiftObjective-C +
+
+- (instancetype)init
+{
+  _flowLayout = [[UICollectionViewFlowLayout alloc] init];
+  _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:_flowLayout];
+  
+  self = [super initWithNode:_collectionNode];
+  if (self) {
+    _flowLayout.minimumInteritemSpacing = 1;
+    _flowLayout.minimumLineSpacing = 1;
+  }
+  
+  return self;
+}
+
+ + +
+
+ +This works just as well with any node including as an ASTableNode, ASPagerNode, etc. + +### Accessing the ASCollectionView +If you've used previous versions of Texture, you'll notice that `ASCollectionView` has been removed in favor of `ASCollectionNode`. + +
+`ASCollectionView`, an actual `UICollectionView` subclass, is still used internally by `ASCollectionNode`. While it should not be created directly, it can still be used directly by accessing the `.view` property of an `ASCollectionNode`. +

+Don't forget that a node's `view` or `layer` property should only be accessed after viewDidLoad or didLoad, respectively, have been called. +
+ +The `LocationCollectionNodeController` above accesses the `ASCollectionView` directly in `-viewDidLoad`. + +
+SwiftObjective-C +
+
+- (void)viewDidLoad
+{
+  [super viewDidLoad];
+  
+  _collectionNode.delegate = self;
+  _collectionNode.dataSource = self;
+  _collectionNode.view.allowsSelection = NO;
+  _collectionNode.view.backgroundColor = [UIColor whiteColor];
+}
+
+ + +
+
+ +### Cell Sizing and Layout + +As discussed in the previous section, `ASCollectionNode` and `ASTableNode` do not need to keep track of the height of their `ASCellNode`s. + +Right now, cells will grow to fit their constrained size and will be laid out by whatever `UICollectionViewLayout` you provide. + +You can also constrain cells used in a collection node using `ASCollectionNode`'s `-constrainedSizeForItemAtIndexPath:`. + +### Examples + +The most detailed example of laying out the cells of an `ASCollectionNode` is the CustomCollectionView app. It includes a Pinterest style cell layout using an `ASCollectionNode` and a custom `UICollectionViewLayout`. + +#### More Sample Apps with ASCollectionNodes + + + +### Interoperability with UICollectionViewCells + +`ASCollectionNode` supports using UICollectionViewCells alongside native ASCellNodes. + +Note that these UIKit cells will **not** have the performance benefits of `ASCellNodes` (like preloading, async layout, and async drawing), even when mixed within the same `ASCollectionNode`. + +However, this interoperability allows developers the flexibility to test out the framework without needing to convert all of their cells at once. Read more here. diff --git a/submodules/AsyncDisplayKit/docs/_docs/containers-asnodecontroller.md b/submodules/AsyncDisplayKit/docs/_docs/containers-asnodecontroller.md new file mode 100755 index 0000000000..09b644440f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/containers-asnodecontroller.md @@ -0,0 +1,238 @@ +--- +title: "ASNodeController (Beta)" +layout: docs +permalink: /docs/containers-asnodecontroller.html +prevPage: containers-asviewcontroller.html +nextPage: containers-astablenode.html +--- + +
+To use this feature, you will need to import "ASNodeController+Beta.h" +
+ +The Texture team has many exciting ideas for expanding `ASNodeController`. Follow along [here](https://github.com/facebook/AsyncDisplayKit/issues/2964) if you'd like to participate in shaping the future of node controllers. + +For now, `ASNodeController` remains a simple, but powerful class. + +### Example + +The [example project](https://github.com/texturegroup/texture/pull/2945) attached in the initial PR modifies the normal [ASDKgram](https://github.com/texturegroup/texture/tree/master/examples/ASDKgram) project to use an `ASNodeController`. +This `PhotoCellNodeController` is used to manage the fetching of the comments data for a photo in a photo feed, once the photo enters the preload range. This node controller allows us to separate the preloading logic from where it previously existed in the `PhotoCellNode` "view" class. + +To convert ASDKgram to use an `ASNodeController`, we first create a `PhotoCellNodeController` class. + +This node controller overrides `ASNodeController`'s' `-loadNode` method to create a `PhotoCellNode` once required. It is not neccessary to call super in this method. + +This node controller also observes its node's interface state in order to intelligently preload the photo's comment feed model data when the `PhotoCellNode` enters the preload state (which indicates that the photo cell is likely to scroll onscreen soon). + +All of this logic can be removed from where it previously existed in the "view" (our `PhotoCellNode` class), leading to a more concise and MVC-friendly view class. + +
+ + Swift + Objective-C + + +
+
+@implementation PhotoCellNodeController
+
+- (void)loadNode
+{
+  self.node = [[PhotoCellNode alloc] initWithPhotoObject:self.photoModel];
+}
+
+- (void)didEnterPreloadState
+{
+  [super didEnterPreloadState];
+  
+  CommentFeedModel *commentFeedModel = _photoModel.commentFeed;
+  [commentFeedModel refreshFeedWithCompletionBlock:^(NSArray *newComments) {
+    // load comments for photo
+    if (commentFeedModel.numberOfItemsInFeed > 0) {
+      [self.node.photoCommentsNode updateWithCommentFeedModel:commentFeedModel];
+      [self.node setNeedsLayout];
+    }
+  }];
+}
+
+@end
+  
+ + +
+
+ +Next, we add a mutable array to the `PhotoFeedNodeController` to store our node controllers and instantiate it in the init method. + +
+ + Swift + Objective-C + + +
+
+@implementation PhotoFeedNodeController
+{
+  PhotoFeedModel          *_photoFeed;
+  ASTableNode             *_tableNode;
+  NSMutableArray *_photoCellNodeControllers;
+}
+
+- (instancetype)init
+{
+  _tableNode = [[ASTableNode alloc] init];
+  self = [super initWithNode:_tableNode];
+  
+  if (self) {
+    self.navigationItem.title = @"Texture";
+    [self.navigationController setNavigationBarHidden:YES];
+    
+    _tableNode.dataSource = self;
+    _tableNode.delegate = self;
+    
+    _photoCellNodeControllers = [NSMutableArray array];
+  }
+  
+  return self;
+}
+  
+ + +
+
+ +To use this node controller, we modify our table row insertion logic to create a `PhotoCellNodeController` rather than a `PhotoCellNode` directly and add it to our node controller array. + +
+ + Swift + Objective-C + + +
+
+- (void)insertNewRowsInTableNode:(NSArray *)newPhotos
+{
+  NSInteger section = 0;
+  NSMutableArray *indexPaths = [NSMutableArray array];
+  
+  NSUInteger newTotalNumberOfPhotos = [_photoFeed numberOfItemsInFeed];
+  for (NSUInteger row = newTotalNumberOfPhotos - newPhotos.count; row < newTotalNumberOfPhotos; row++) {
+  
+    // create photoCellNodeControllers for the new photos
+    PhotoCellNodeController *cellController = [[PhotoCellNodeController alloc] init];
+    cellController.photoModel = [_photoFeed objectAtIndex:row];
+    [_photoCellNodeControllers addObject:cellController];
+    
+    // include this index path in the insert rows call for the table
+    NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:section];
+    [indexPaths addObject:path];
+  }
+  
+  [_tableNode insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
+}
+  
+ + +
+
+ +Don't forget to modify the table data source method to return the node controller rather than the cell node. + +
+ + Swift + Objective-C + + +
+
+- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+  PhotoCellNodeController *cellController = [_photoCellNodeControllers objectAtIndex:indexPath.row];
+  // this will be executed on a background thread - important to make sure it's thread safe
+  ASCellNode *(^ASCellNodeBlock)() = ^ASCellNode *() {
+    PhotoCellNode *cellNode = [cellController node];
+    return cellNode;
+  };
+  
+  return ASCellNodeBlock;
+}
+  
+ + +
+
+ + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/containers-aspagernode.md b/submodules/AsyncDisplayKit/docs/_docs/containers-aspagernode.md new file mode 100755 index 0000000000..f538983ee0 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/containers-aspagernode.md @@ -0,0 +1,166 @@ +--- +title: ASPagerNode +layout: docs +permalink: /docs/containers-aspagernode.html +prevPage: containers-ascollectionnode.html +nextPage: display-node.html +--- + +`ASPagerNode` is a subclass of `ASCollectionNode` with a specific `UICollectionViewLayout` used under the hood. + +Using it allows you to produce a page style UI similar to what you'd create with UIKit's `UIPageViewController`. `ASPagerNode` currently supports staying on the correct page during rotation. It does _not_ currently support circular scrolling. + +The main dataSource methods are: + +
+SwiftObjective-C +
+
+- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode
+
+ + +
+
+ +and + +
+SwiftObjective-C +
+
+- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index
+
+ + +
+
+ +or + +
+SwiftObjective-C +
+
+- (ASCellNodeBlock)pagerNode:(ASPagerNode *)pagerNode nodeBlockAtIndex:(NSInteger)index`
+
+ + +
+
+ +These two methods, just as with `ASCollectionNode` and `ASTableNode` need to return either an `ASCellNode` or an `ASCellNodeBlock` - a block that creates an `ASCellNode` and can be run on a background thread. + +Note that neither methods should rely on cell reuse (they will be called once per row). Also, unlike UIKit, these methods are not called when the row is just about to display. + +While `-pagerNode:nodeAtIndex:` will be called on the main thread, `-pagerNode:nodeBlockAtIndex:` is preferred because it concurrently allocates cell nodes, meaning that the `-init:` method of each of your subnodes will be run in the background. **It is very important that node blocks be thread-safe** as they can be called on the main thread or a background queue. + +### Node Block Thread Safety Warning + +It is imperative that the data model be accessed outside of the node block. This means that it is highly unlikely that you should need to use the index inside of the block. + +In the example below, you can see how the index is used to access the photo model before creating the node block. + +
+SwiftObjective-C +
+
+- (ASCellNodeBlock)pagerNode:(ASPagerNode *)pagerNode nodeBlockAtIndex:(NSInteger)index
+{
+  PhotoModel *photoModel = _photoFeed[index];
+  
+  // this part can be executed on a background thread - it is important to make sure it is thread safe!
+  ASCellNode *(^cellNodeBlock)() = ^ASCellNode *() {
+    PhotoCellNode *cellNode = [[PhotoCellNode alloc] initWithPhoto:photoModel];
+    return cellNode;
+  };
+  
+  return cellNodeBlock;
+}
+
+ + +
+
+ +### Using an ASViewController For Optimal Performance + +One especially useful pattern is to return an `ASCellNode` that is initialized with an existing `UIViewController` or `ASViewController`. For optimal performance, use an `ASViewController`. + +
+SwiftObjective-C +
+
+- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index
+{
+    NSArray *animals = self.animals[index];
+    
+    ASCellNode *node = [[ASCellNode alloc] initWithViewControllerBlock:^{
+        return [[AnimalTableNodeController alloc] initWithAnimals:animals];;
+    } didLoadBlock:nil];
+    
+    node.style.preferredSize = pagerNode.bounds.size;
+    
+    return node;
+}
+
+ + +
+
+ +In this example, you can see that the node is constructed using the `-initWithViewControllerBlock:` method. It is usually necessary to provide a cell created this way with a `style.preferredSize` so that it can be laid out correctly. + +### Use ASPagerNode as root node of an ASViewController + +#### Log message while popping back in the view controller hierarchy +If you use an `ASPagerNode` embedded in an `ASViewController` in full screen. If you pop back from the view controller hierarchy you will see some error message in the console. + +To resolve the error message set `self.automaticallyAdjustsScrollViewInsets = NO;` in `viewDidLoad` in your `ASViewController` subclass. + +#### `navigationBar.translucent` is set to YES +If you have an `ASPagerNode` embedded in an `ASViewController` in full screen and set the `navigationBar.translucent` to `YES`, you will see an error message while pushing the view controller on the view controller stack. + +To resolve the error message add `[self.pagerNode waitUntilAllUpdatesAreCommitted];` within `- (void)viewWillAppear:(BOOL)animated` in your `ASViewController` subclass. +Unfortunately the disadvantage of this is that the first measurement pass will block the main thread until it finishes. + +#### Some more details about the error messages above +The reason for this error message is that due to the asynchronous nature of Texture, measurement of nodes will happen on a background thread as UIKit will resize the view of the `ASViewController` on on the main thread. The new layout pass has to wait until the old layout pass finishes with an old layout constrained size. Unfortunately while the measurement pass with the old constrained size is still in progress the `ASPagerFlowLayout` that is backing a `ASPagerNode` will print some errors in the console as it expects sizes for nodes already measured with the new constrained size. + +### Sample Apps + +Check out the following sample apps to see an `ASPagerNode` in action: + diff --git a/submodules/AsyncDisplayKit/docs/_docs/containers-astablenode.md b/submodules/AsyncDisplayKit/docs/_docs/containers-astablenode.md new file mode 100755 index 0000000000..7742e23c3b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/containers-astablenode.md @@ -0,0 +1,228 @@ +--- +title: ASTableNode +layout: docs +permalink: /docs/containers-astablenode.html +prevPage: containers-asnodecontroller.html +nextPage: containers-ascollectionnode.html +--- + +`ASTableNode` is equivalent to UIKit's `UITableView` and can be used in place of any `UITableView`. + +`ASTableNode` replaces `UITableView`'s required method + +
+ + Swift + Objective-C + + +
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+  
+ + +
+
+ +with your choice of **_one_** of the following methods + +
+ + Swift + Objective-C + + +
+
+- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath
+  
+ + +
+
+ +or + +
+ + Swift + Objective-C + + +
+
+- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
+  
+ + +
+
+ +
+
+It is recommended that you use the node block version of these methods so that your table node will be able to prepare and display all of its cells concurrently. This means that all subnode initialization methods can be run in the background. Make sure to keep 'em thread safe. +
+ +These two methods, need to return either an `ASCellNode` or an `ASCellNodeBlock`. An `ASCellNodeBlock` is a block that creates a `ASCellNode` which can be run on a background thread. Note that `ASCellNodes` are used by `ASTableNode`, `ASCollectionNode` and `ASPagerNode`. + +Note that neither of these methods require a reuse mechanism. + +### Replacing UITableViewController with ASViewController + +Texture does not offer an equivalent to `UITableViewController`. Instead, use an `ASViewController` initialized with an `ASTableNode`. + +Consider, again, the `ASViewController` subclass - PhotoFeedNodeController - from the `ASDKgram sample app` that uses a table node as its managed node. + +An `ASTableNode` is assigned to be managed by an `ASViewController` in its `-initWithNode:` designated initializer method. + +
+SwiftObjective-C +
+
+- (instancetype)init
+{
+    _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];
+    self = [super initWithNode:_tableNode];
+    
+    if (self) {
+      _tableNode.dataSource = self;
+      _tableNode.delegate = self;
+    }
+    
+    return self;
+}
+  
+ + +
+
+ +### Node Block Thread Safety Warning + +It is very important that node blocks be thread-safe. One aspect of that is ensuring that the data model is accessed _outside_ of the node block. Therefore, it is unlikely that you should need to use the index inside of the block. + +Consider the following `-tableNode:nodeBlockForRowAtIndexPath:` method from the `PhotoFeedNodeController.m` file in the ASDKgram sample app. + +
+SwiftObjective-C +
+
+- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    PhotoModel *photoModel = [_photoFeed objectAtIndex:indexPath.row];
+    
+    // this may be executed on a background thread - it is important to make sure it is thread safe
+    ASCellNode *(^cellNodeBlock)() = ^ASCellNode *() {
+        PhotoCellNode *cellNode = [[PhotoCellNode alloc] initWithPhoto:photoModel];
+        cellNode.delegate = self;
+        return cellNode;
+    };
+    
+    return cellNodeBlock;
+}
+  
+ + +
+
+ +In the example above, you can see how the index is used to access the photo model before creating the node block. + +### Accessing the ASTableView + +If you've used previous versions of Texture, you'll notice that `ASTableView` has been removed in favor of `ASTableNode`. + +
+ASTableView, an actual UITableView subclass, is still used internally by ASTableNode. While it should not be created directly, it can still be used directly by accessing the .view property of an ASTableNode. + +Don't forget that a node's view or layer property should only be accessed after -viewDidLoad or -didLoad, respectively, have been called. +
+ +For example, you may want to set a table's separator style property. This can be done by accessing the table node's view in the `-viewDidLoad:` method as seen in the example below. + +
+SwiftObjective-C +
+
+- (void)viewDidLoad
+{
+  [super viewDidLoad];
+  
+  _tableNode.view.allowsSelection = NO;
+  _tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone;
+  _tableNode.view.leadingScreensForBatching = 3.0;  // default is 2.0
+}
+
+ + +
+
+ +### Table Row Height + +An important thing to notice is that `ASTableNode` does not provide an equivalent to `UITableView`'s `-tableView:heightForRowAtIndexPath:`. + +This is because nodes are responsible for determining their own height based on the provided constraints. This means you no longer have to write code to determine this detail at the view controller level. + +A node defines its height by way of the layoutSpec returned in the `-layoutSpecThatFits:` method. All nodes given a constrained size are able to calculate their desired size. + +
+By default, a ASTableNode provides its cells with a size range constraint where the minimum width is the tableNode's width and a minimum height is 0. The maximum width is also the tableNode's width but the maximum height is FLT_MAX. +

+This is all to say, a `tableNode`'s cells will always fill the full width of the `tableNode`, but their height is flexible making self-sizing cells something that happens automatically. +
+ +If you call `-setNeedsLayout` on an `ASCellNode`, it will automatically perform another layout pass and if its overall desired size has changed, the table will be informed and will update itself. + +This is different from `UIKit` where normally you would have to call reload row / item. This saves tons of code, check out the ASDKgram sample app to see side by side implementations of an `UITableView` and `ASTableNode` implemented social media feed. + +### Sample Apps using ASTableNode + diff --git a/submodules/AsyncDisplayKit/docs/_docs/containers-asviewcontroller.md b/submodules/AsyncDisplayKit/docs/_docs/containers-asviewcontroller.md new file mode 100755 index 0000000000..8b4fd3648d --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/containers-asviewcontroller.md @@ -0,0 +1,67 @@ +--- +title: ASViewController +layout: docs +permalink: /docs/containers-asviewcontroller.html +prevPage: faq.html +nextPage: containers-asnodecontroller.html +--- + +`ASViewController` is a subclass of `UIViewController` that adds several useful features for hosting `ASDisplayNode` hierarchies. + +An `ASViewController` can be used in place of any `UIViewController` - including within a `UINavigationController`, `UITabBarController` and `UISplitViewController` or as a modal view controller. + +Benefits of using an `ASViewController`: +
    +
  1. Save Memory. An ASViewController that goes off screen will automatically reduce the size of the fetch data and display ranges of any of its children. This is key for memory management in large applications.
  2. +
  3. ASVisibility Feature. When used in an ASNavigationController or ASTabBarController, these classes know the exact number of user taps it would take to make the view controller visible.
  4. +
+ +More features will be added over time, so it is a good idea to base your view controllers off of this class. + +## Usage + +A `UIViewController` provides a view of its own. An `ASViewController` is assigned a node to manage in its designated initializer `-initWithNode:`. + +Consider the following `ASViewController` subclass, `PhotoFeedNodeController`, from the ASDKgram example project that would like to use a table node as its managed node. + +This table node is assigned to the `ASViewController` in its `-initWithNode:` designated initializer method. + +
+SwiftObjective-C +
+
+- (instancetype)init
+{
+  _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain];
+  self = [super initWithNode:_tableNode];
+  
+  if (self) {
+    _tableNode.dataSource = self;
+    _tableNode.delegate = self;
+  }
+  
+  return self;
+}
+  
+ + +
+
+ +
+
+Conversion Tip: If your app already has a complex view controller hierarchy, it is perfectly fine to have all of them subclass ASViewController. That is to say, even if you don't use ASViewController's designated initializer -initWithNode:, and only use the ASViewController in the manner of a traditional UIViewController, this will give you the additional node support if you choose to adopt it in different areas your application. +
+ diff --git a/submodules/AsyncDisplayKit/docs/_docs/containers-overview.md b/submodules/AsyncDisplayKit/docs/_docs/containers-overview.md new file mode 100755 index 0000000000..cde2fb7f8d --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/containers-overview.md @@ -0,0 +1,52 @@ +--- +title: Node Containers +layout: docs +permalink: /docs/containers-overview.html +prevPage: intelligent-preloading.html +nextPage: node-overview.html +--- + +### Use Nodes in Node Containers +It is highly recommended that you use Texture's nodes within a node container. Texture offers the following node containers. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Texture Node ContainerUIKit Equivalent
ASCollectionNodein place of UIKit's UICollectionView
ASPagerNodein place of UIKit's UIPageViewController
ASTableNodein place of UIKit's UITableView
ASViewControllerin place of UIKit's UIViewController
ASNavigationControllerin place of UIKit's UINavigationController. Implements the ASVisibility protocol.
ASTabBarControllerin place of UIKit's UITabBarController. Implements the ASVisibility protocol.
+ +
+Example code and specific sample projects are highlighted in the documentation for each node container. + + + +### What do I Gain by Using a Node Container? + +A node container automatically manages the intelligent preloading of its nodes. This means that all of the node's layout measurement, data fetching, decoding and rendering will be done asynchronously. Among other conveniences, this is why it is recommended to use nodes within a container node. + +Note that while it _is_ possible to use nodes directly (without a Texture node container), unless you add additional calls, they will only start displaying once they come onscreen (as UIKit does). This can lead to performance degradation and flashing of content. diff --git a/submodules/AsyncDisplayKit/docs/_docs/control-node.md b/submodules/AsyncDisplayKit/docs/_docs/control-node.md new file mode 100755 index 0000000000..30aef32914 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/control-node.md @@ -0,0 +1,122 @@ +--- +title: ASControlNode +layout: docs +permalink: /docs/control-node.html +prevPage: map-node.html +nextPage: scroll-node.html +--- + +`ASControlNode` is the Texture equivalent to `UIControl`. You don't create instances of `ASControlNode` directly. Instead, you can use it as a subclassing point when creating controls of your own. In fact, ASTextNode, ASImageNode, ASVideoNode and ASMapNode are all subclasses of `ASControlNode`. + +This fact is especially useful when it comes to image and text nodes. Having the ability to add target-action pairs means that you can use any text or image node as a button without having to rely on creating gesture recognizers, as you would with text in UIKit, or creating extraneous views as you might when using `UIButton`. + +### Control State + +Like `UIControl`, `ASControlNode` has a state which defines its appearance and ability to support user interactions. Its state can be one of any state defined by `ASControlState`. + +
+SwiftObjective-C +
+
+typedef NS_OPTIONS(NSUInteger, ASControlState) {
+    ASControlStateNormal       = 0,
+    ASControlStateHighlighted  = 1 << 0,  // used when isHighlighted is set
+    ASControlStateDisabled     = 1 << 1,
+    ASControlStateSelected     = 1 << 2,  // used when isSelected is set
+    ...
+};
+
+ +
+
+ +### Target-Action Mechanism + +Also similarly to `UIControl`, `ASControlNode`'s have a set of events defined which you can react to by assigning a target-action pair. + +The available actions are: +
+SwiftObjective-C +
+
+typedef NS_OPTIONS(NSUInteger, ASControlNodeEvent)
+{
+  /** A touch-down event in the control node. */
+  ASControlNodeEventTouchDown         = 1 << 0,
+  /** A repeated touch-down event in the control node; for this event the value of the UITouch tapCount method is greater than one. */
+  ASControlNodeEventTouchDownRepeat   = 1 << 1,
+  /** An event where a finger is dragged inside the bounds of the control node. */
+  ASControlNodeEventTouchDragInside   = 1 << 2,
+  /** An event where a finger is dragged just outside the bounds of the control. */
+  ASControlNodeEventTouchDragOutside  = 1 << 3,
+  /** A touch-up event in the control node where the finger is inside the bounds of the node. */
+  ASControlNodeEventTouchUpInside     = 1 << 4,
+  /** A touch-up event in the control node where the finger is outside the bounds of the node. */
+  ASControlNodeEventTouchUpOutside    = 1 << 5,
+  /** A system event canceling the current touches for the control node. */
+  ASControlNodeEventTouchCancel       = 1 << 6,
+  /** All events, including system events. */
+  ASControlNodeEventAllEvents         = 0xFFFFFFFF
+};
+
+ +
+
+ +Assigning a target and action for these events is done with the same methods as a `UIControl`, namely using `–addTarget:action:forControlEvents:`. + +### Hit Test Slop + +While all node's have a `hitTestSlop` property, this is usually most useful when dealing with controls. Instead of needing to make your control bigger, or needing to override `-hitTest:withEvent:` you can just assign a `UIEdgeInsets` to your control and its boundaries will be expanded accordingly. + +
+SwiftObjective-C +
+
+CGFloat horizontalDiff = (bounds.size.width - _playButton.bounds.size.width)/2;
+CGFloat verticalDiff = (bounds.size.height - _playButton.bounds.size.height)/2;
+
+_playButton.hitTestSlop = UIEdgeInsetsMake(-verticalDiff, -horizontalDiff, -verticalDiff, -horizontalDiff);
+
+ +
+
+ +Remember that, since the property is an inset, you'll need to use negative values in order to expand the size of your tappable region. + +### Hit Test Visualization + +The hit test visualization tool is an option to enable highlighting of the tappable areas of your nodes. To enable it, include `[ASControlNode setEnableHitTestDebug:YES]` in your app delegate in `-application:didFinishLaunchingWithOptions:`. diff --git a/submodules/AsyncDisplayKit/docs/_docs/corner-rounding.md b/submodules/AsyncDisplayKit/docs/_docs/corner-rounding.md new file mode 100755 index 0000000000..d8eca56c87 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/corner-rounding.md @@ -0,0 +1,226 @@ +--- +title: Corner Rounding +layout: docs +permalink: /docs/corner-rounding.html +prevPage: synchronous-concurrency.html +nextPage: debug-tool-hit-test-visualization.html +--- + +When it comes to corner rounding, many developers stick with CALayer's `.cornerRadius` property. Unfortunately, this convenient property greatly taxes performance and should only be used when there is _no_ alternative. This post will cover: + + + +## CALayer's .cornerRadius is Expensive + +Why is `.cornerRadius` so expensive? Use of CALayer's `.cornerRadius` property triggers offscreen rendering to perform the clipping operation on every frame - 60 FPS during scrolling - even if the content in that area isn't changing! This means that the GPU has to switch contexts on every frame, between compositing the overall frame + additional passes for each use of `.cornerRadius`. + +Importantly, these costs don't show up in the Time Profiler, because they affect work done by the CoreAnimation Render Server on your app's behalf. This intensive thrash annihilates performance for a lot of devices. On the iPhone 4, 4S, and 5 / 5C (along with comparable iPads / iPods), expect to see notably degraded performance. On the iPhone 5S and newer, even if you can't see the impact directly, it will reduce headroom so that it takes less to cause a frame drop. + +## Performant Corner Rounding Strategies + +There are only three things to consider when picking a corner rounding strategy: + +
    +
  1. Is there movement underneath the corner?
  2. +
  3. Is there movement through the corner?
  4. +
  5. Are all 4 corners the same node *and* no other nodes intersect in the corner area?
  6. +
+ +Movement **underneath the corner** is any movement behind the corner. For example, as a rounded-corner collection cell scrolls over a background, the background will move underneath and out from under the corners. + +To describe movement **through the corner,** imagine a small rounded-corner scroll view containing a much larger photo. As you zoom and pan the photo inside of the scroll view, the photo will move through the corners of the scroll view. + + + +The above image shows movement underneath the corner highlighted in blue and movement through the corner highlighted in orange. + +
+Note: There can be movement inside of the rounded-corner object, without moving through the corner. The following image shows content, highlighted in green, inset from the edge with a margin equal to the size of the corner radius. When the content scrolls, it will not move through the corners. +
+ + + +Using the above method to adjust your design to eliminate one source of corner movement can make the difference between being able to use a fast rounding technique, or resorting to `.cornerRadius.`. + +The final consideration is to determine if all four corners cover the same node or if any subnodes interesect the corner area. + + + +### Precomposited Corners + +Precomposited corners refer to corners drawn using bezier paths to clip the content in a CGContext / UIGraphicsContext (`[path clip]`). In this scenario, the corners become part of the image itself — and are "baked in" to the single CALayer. There are two types of precomposited corners. + +The absolute best method is to use **precomposited opaque corners**. This is the most efficient method available, resulting in zero alpha blending (although this is much less critical than avoiding offscreen rendering). Unfortunately, this method is also the least flexible; the background behind the corners will need to be a solid color if the rounded image needs to move around on top of it. It's possible, but tricky to make precomposited corners with a textured or photo background - usually it's best to use precomposited alpha corners instead'.' + +The second method involves using bezier paths with **precomposited alpha corners**. This method is pretty flexible and should be one of the most frequently used. It does incur the cost of alpha blending across the full size of the content, and including an alpha channel increases memory impact by 25% over opaque precompositing - but these costs are tiny on modern devices, and a different order of magnitude than `.cornerRadius` offscreen rendering. + +A key limitation of precomposited corners is that the corners must only touch one node and not intersect with any subnodes. If either of these conditions exist, clip corners must be used. + +Note that Texture nodes have a special optimization of `.cornerRadius` that automatically implements precomposited corners **only when using** `.shouldRasterizeDescendants`. It's important to think carefully before you enable rasterization, so don't use this option without first reading all about the concept. + +
+If you're looking for a simple, flat-color rounded rectangle or circle, Texture offers a variety of conveniences to provide this. See `UIImage+ASConveniences.h` for methods to create flat-colored, rounded-corner resizable images using precomposited corners (both alpha and opaque are supported). These are great for use as placeholders for image nodes or backgrounds for ASButtonNode. +
+ +### Clip Corner + +This strategy involves placing **4 seperate opaque corners that sit on top of the content** that needs corner rounding. This method is flexible and has quite good performance. It has minor CPU overhead of 4 seperate layers, one layer for each corner. + + + +Clip corners applies to two main types of corner rounding situations: + +
    +
  • Rounded corners in situations in which the corners touch more than one node or intersect with any subnodes.
  • +
  • Rounded corners on top of a stationary texture or photo background. The photo clip corner method is tricky, but useful!
  • +
+ +## Is it ever okay to use CALayer's .cornerRadius property? + +There are a few, quite rare cases in which it is appropriate to use `.cornerRadius.` These include when there is dynamic content moving _both_ through the inside and underneath the corner. For certain animations, this is impossible to avoid. However, in many cases, it is easy to adjust your design to eliminate one of the sources of movement. One such case was discussed in the section on corner movement. + +It is much less bad, and okay as a shortcut, to use `.cornerRadius.` for screens in which nothing moves. However, *any* motion on the screen, even movement that doesn't involve the corners, will cause the `.cornerRadius.` performance tax. For example, having a rounded element in the navigation bar with a scrolling view beneath it will cause the impact even if they don't overlap. Animating anything onscreen, even if the user doesn't interact, will as well.' Additionally, any type of screen refresh will incur the cost of corner rounding. + +### Rasterization and Layerbacking + +Some people have suggested that using CALayer's `.shouldRasterize` can improve the performance of the `.cornerRadius` property. This is not well understood option that is generally perilous. As long as nothing causes it to re-rasterize (no movement, no tap to change color, not on a table view that moves, etc), it is okay to use. Generally we don't encourage this because it is very easy to cause much worse performance. For people who have not great app architecture and insist on using CALayer's `.cornerRadius` (e.g. their app is not very performant), this _can_ make a meaningful difference. However, if you are building your app from the ground up, we highly reccommend that you choose one of the better corner rounding strategies above. + +CALayer's `.shouldRasterize` is unrelated to Texture `node.shouldRasterizeDescendents`. When enabled, `.shouldRasterizeDescendents` will prevent the actual view and layer of the subnode children from being created. + +## Corner Rounding Strategy Flowchart + +Use this flowchart to select the most performant strategy to round a set of corners. + +corner rounding strategy flowchart + +## Texture Support + +The following code exemplifies different ways how to archive corner rounding within Texture: + +### Use `.cornerRadius` + +
+SwiftObjective-C +
+
+CGFloat cornerRadius = 20.0;
+    
+_photoImageNode.cornerRoundingType = ASCornerRoundingTypeDefaultSlowCALayer;
+_photoImageNode.cornerRadius = cornerRadius;
+
+ +
+
+ + +### Use precomposition for rounding corners + +
+SwiftObjective-C +
+
+CGFloat cornerRadius = 20.0;
+    
+_photoImageNode.cornerRoundingType = ASCornerRoundingTypePrecomposited;
+_photoImageNode.cornerRadius = cornerRadius;
+
+ +
+
+ + +### Use clipping for rounding corners + +
+SwiftObjective-C +
+
+CGFloat cornerRadius = 20.0;
+
+_photoImageNode.cornerRoundingType = ASCornerRoundingTypeClipping;
+_photoImageNode.backgroundColor = [UIColor whiteColor];
+_photoImageNode.cornerRadius = cornerRadius;
+
+ +
+
+ + +### Use `willDisplayNodeContentWithRenderingContext` to set a clipping path for the content for rounding corners + +
+SwiftObjective-C +
+
+CGFloat cornerRadius = 20.0;
+    
+// Use the screen scale for corner radius to respect content scale
+CGFloat screenScale = UIScreen.mainScreen.scale;
+_photoImageNode.willDisplayNodeContentWithRenderingContext = ^(CGContextRef context, id drawParameters) {
+    CGRect bounds = CGContextGetClipBoundingBox(context);
+    CGFloat radius = cornerRadius * screenScale; 
+    UIImage *overlay = [UIImage as_resizableRoundedImageWithCornerRadius:radius
+                                                             cornerColor:[UIColor clearColor]
+                                                               fillColor:[UIColor clearColor]];
+    [overlay drawInRect:bounds];
+    [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:radius] addClip];
+};
+
+
+ +
+
+ +### Use `ASImageNode` extras to round the image and add a border. + +This is great for example to round avatar images. + +
+SwiftObjective-C +
+
+CGFloat cornerRadius = 20.0;
+
+_photoImageNode.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(5.0, [UIColor orangeColor]);
+
+ +
+
diff --git a/submodules/AsyncDisplayKit/docs/_docs/debug-tool-ASRangeController.md b/submodules/AsyncDisplayKit/docs/_docs/debug-tool-ASRangeController.md new file mode 100755 index 0000000000..b1bbc33a80 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/debug-tool-ASRangeController.md @@ -0,0 +1,42 @@ +--- +title: Range Visualization +layout: docs +permalink: /docs/debug-tool-ASRangeController.html +prevPage: debug-tool-pixel-scaling.html +nextPage: asvisibility.html +--- + +## Visualize ASRangeController tuning parameters (PR #1390) +
+This debug feature adds a semi-transparent subview in the bottom right hand corner of the sharedApplication keyWindow that visualizes the ASRangeTuningParameters per each ASLayoutRangeType for each visible (on-screen) instance of ASRangeController. + +- The instances of ASRangeController are represented as bars +- As you scroll around within ASTable/CollectionViews you can see the parameters (green = Visible, yellow = Display, and red = FetchData) move relative to each other. +- White arrows on the L and R sides of the individual RangeController bar views indicate the scrolling direction so that you can determine the leading / trailing tuning parameters (especially useful for vertically-oriented rangeControllers whose leading edge might be unclear within the horizontally-oriented bar view). +- The white debug label above the RangeController bar displays the RangeController dataSource’s class name to differentiate between nested views. +- The overlay can be moved with a panning gesture in order to see content under it. + +This debug feature is useful for highly optimized Texture apps that require tuning of any ASRangeController. Or for anyone who is curious about how ASRangeControllers work. + +The VerticalWithinHorizontal example app contains an ASPagerNode with embedded ASTableViews. In the screenshot with this feature enabled, you can see the two range controllers - ASTableView and ASCollectionView (ASPagerNode) - in the overlay. + +- The white arrows to the right of the rangeController bars indicate that the user is currently scrolling down through the table and right through the ASCollectionView/PagerNode. +- The ASTableView rangeController bar indicates that the range parameters are tuned to both fetch and decode more data in the downward table direction rather than in the reverse direction (which makes sense as the user is scrolling down). +- Since it’s less obvious whether or not the user will page to the right or left next, the ASCollectionView is tuned to fetch and decode equal amounts of data in each direction. +- In the video demo, you can see as the user scrolls between pages, that new ASTableView rangeControllers are created and removed in the overlay view. +![bc0b98f0-ebb8-11e5-8f50-421cb0f320c2](https://cloud.githubusercontent.com/assets/3419380/14057072/ef7f63a0-f2b2-11e5-92a5-f65b2d207e63.png) + +## Limitations +
    +
  • only shows onscreen ASRangeControllers
  • +
  • currently the ratio of red (fetch data), yellow (display) and green (visible) are relative to each other, but not between each bar view. So you cannot compare individual bars to eachother
  • +
+ +## Usage +In your `AppDelegate.m` file, +
    +
  • import AsyncDisplayKit+Debug.h
  • +
  • add [ASDisplayNode setShouldShowRangeDebugOverlay:YES] at the top of your AppDelegate's didFinishLaunchingWithOptions: method
  • +
+ +**Make sure to call this method before initializing any component that uses an ASRangeControllers (ASTableView, ASCollectionView).** diff --git a/submodules/AsyncDisplayKit/docs/_docs/debug-tool-hit-test-visualization.md b/submodules/AsyncDisplayKit/docs/_docs/debug-tool-hit-test-visualization.md new file mode 100755 index 0000000000..2b2f5f2a50 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/debug-tool-hit-test-visualization.md @@ -0,0 +1,34 @@ +--- +title: Hit Test Visualization +layout: docs +permalink: /docs/debug-tool-hit-test-visualization.html +prevPage: corner-rounding.html +nextPage: debug-tool-pixel-scaling.html +--- + +## Visualize ASControlNode Tappable Areas +
+This debug feature adds a semi-transparent highlight overlay on any ASControlNodes containing a `target:action:` pair or gesture recognizer. The tappable range is defined as the ASControlNode’s frame + its `.hitTestSlop` `UIEdgeInsets`. Hit test slop is a unique feature of `ASControlNode` that allows it to extend its tappable range. + +In the screenshot below, you can quickly see that +
    +
  • The tappable area for the avatar image overlaps the username’s tappable area. In this case, the user avatar image is on top in the view hierarchy and is capturing some touches that should go to the username.
  • +
  • It would probably make sense to expand the `.hitTestSlop` for the username to allow the user to more easily hit it.
  • +
  • I’ve accidentally set the hitTestSlop’s UIEdgeInsets to be positive instead of negative for the photo likes count label. It’s going to be hard for a user to tap the smaller target.
  • +
+ +![screen shot 2016-03-25 at 4 39 23 pm](https://cloud.githubusercontent.com/assets/3419380/14057034/e1e71450-f2b1-11e5-8091-3e6f22862994.png) + +## Restrictions +
+A _green_ border on the edge(s) of the highlight overlay indicates that that edge of the tapable area is restricted by one of it's superview's tapable areas. An _orange_ border on the edge(s) of the highlight overlay indicates that that edge of the tapable area is clipped by .clipsToBounds of a parent in its hierarchy. + +## Usage +
+In your `AppDelegate.m` file, +
    +
  • import AsyncDisplayKit+Debug.h
  • +
  • add [ASControlNode setEnableHitTestDebug:YES] at the top of your AppDelegate's didFinishLaunchingWithOptions: method
  • +
+ +**Make sure to call this method before initializing any ASControlNodes - including ASButtonNodes, ASImageNodes, and ASTextNodes.** diff --git a/submodules/AsyncDisplayKit/docs/_docs/debug-tool-pixel-scaling.md b/submodules/AsyncDisplayKit/docs/_docs/debug-tool-pixel-scaling.md new file mode 100755 index 0000000000..2ff4e5e37b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/debug-tool-pixel-scaling.md @@ -0,0 +1,60 @@ +--- +title: Image Scaling +layout: docs +permalink: /docs/debug-tool-pixel-scaling.html +prevPage: debug-tool-hit-test-visualization.html +nextPage: debug-tool-ASRangeController.html +--- + +## Visualize ASImageNode.image’s pixel scaling +
+This debug feature adds a red text overlay on the bottom right hand corner of an ASImageNode if (and only if) the image’s size in pixels does not match it’s bounds size in pixels, e.g. + +
+SwiftObjective-C + +
+
+CGFloat imageSizeInPixels = image.size.width * image.size.height;
+CGFloat boundsSizeInPixels = imageView.bounds.size.width * imageView.bounds.size.height;
+CGFloat scaleFactor = imageSizeInPixels / boundsSizeInPixels;
+
+if (scaleFactor != 1.0) {
+      NSString *scaleString = [NSString stringWithFormat:@"%.2fx", scaleFactor];
+      _debugLabelNode.hidden = NO;
+}
+
+ +
+
+ + +This debug feature is useful for quickly determining if you are + +
    +
  • downloading and rendering excessive amounts of image data
  • +
  • upscaling a low quality image
  • +
+ +In the screenshot below of an app with this debug feature enabled, you can see that the avatar image is unnecessarily large (9x too large) for it’s bounds size and that the center picture is more optimized, but not perfectly so. If you control your own endpoint, make sure to return an optimally sized image. + +![screen shot 2016-03-25 at 4 04 59 pm](https://cloud.githubusercontent.com/assets/3419380/14056994/15561daa-f2b1-11e5-9606-59d54d2b5354.png) + +## Usage +
+In your `AppDelegate.m` file, +
    +
  • import AsyncDisplayKit+Debug.h
  • +
  • add [ASImageNode setShouldShowImageScalingOverlay:YES] at the top of your AppDelegate's didFinishLaunchingWithOptions: method
  • +
+ +**Make sure to call this method before initializing any ASImageNodes.** diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/automatic-subnode-layout.md b/submodules/AsyncDisplayKit/docs/_docs/development/automatic-subnode-layout.md new file mode 100644 index 0000000000..705411d113 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/automatic-subnode-layout.md @@ -0,0 +1,7 @@ +--- +title: Automatic subnode layout +layout: docs +permalink: /development/automatic-subnode-layout.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/cell-node-lifecycle.md b/submodules/AsyncDisplayKit/docs/_docs/development/cell-node-lifecycle.md new file mode 100644 index 0000000000..9ae549f915 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/cell-node-lifecycle.md @@ -0,0 +1,7 @@ +--- +title: Cell node lifecycle +layout: docs +permalink: /development/cell-node-lifecycle.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/collection-animations.md b/submodules/AsyncDisplayKit/docs/_docs/development/collection-animations.md new file mode 100644 index 0000000000..e97a4d540c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/collection-animations.md @@ -0,0 +1,7 @@ +--- +title: Collection animations +layout: docs +permalink: /development/collection-animations.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/collection-asynchronous-updates.md b/submodules/AsyncDisplayKit/docs/_docs/development/collection-asynchronous-updates.md new file mode 100644 index 0000000000..aacb9bc35a --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/collection-asynchronous-updates.md @@ -0,0 +1,7 @@ +--- +title: Collections and asynchronous updates +layout: docs +permalink: /development/collection-asynchronous-updates.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/components.md b/submodules/AsyncDisplayKit/docs/_docs/development/components.md new file mode 100644 index 0000000000..52f5e35b93 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/components.md @@ -0,0 +1,7 @@ +--- +title: How components work together +layout: docs +permalink: /development/components.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/how-to-develop-and-debug.md b/submodules/AsyncDisplayKit/docs/_docs/development/how-to-develop-and-debug.md new file mode 100644 index 0000000000..d22ddbc879 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/how-to-develop-and-debug.md @@ -0,0 +1,7 @@ +--- +title: Develop and debug +layout: docs +permalink: /development/how-to-develop-and-debug.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/layout-specs.md b/submodules/AsyncDisplayKit/docs/_docs/development/layout-specs.md new file mode 100644 index 0000000000..c7089b81f8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/layout-specs.md @@ -0,0 +1,7 @@ +--- +title: Layout specs +layout: docs +permalink: /development/layout-specs.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/layout-transitions.md b/submodules/AsyncDisplayKit/docs/_docs/development/layout-transitions.md new file mode 100644 index 0000000000..8ba604966f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/layout-transitions.md @@ -0,0 +1,7 @@ +--- +title: Layout transitions and animations +layout: docs +permalink: /development/layout-transitions.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/node-lifecycle.md b/submodules/AsyncDisplayKit/docs/_docs/development/node-lifecycle.md new file mode 100644 index 0000000000..b8695b8923 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/node-lifecycle.md @@ -0,0 +1,7 @@ +--- +title: Node lifecycle +layout: docs +permalink: /development/node-lifecycle.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/overview.md b/submodules/AsyncDisplayKit/docs/_docs/development/overview.md new file mode 100644 index 0000000000..531fefe951 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/overview.md @@ -0,0 +1,7 @@ +--- +title: Overview +layout: docs +permalink: /development/overview.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/structure.md b/submodules/AsyncDisplayKit/docs/_docs/development/structure.md new file mode 100644 index 0000000000..ec05b3f043 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/structure.md @@ -0,0 +1,7 @@ +--- +title: Structure +layout: docs +permalink: /development/structure.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/development/threading.md b/submodules/AsyncDisplayKit/docs/_docs/development/threading.md new file mode 100644 index 0000000000..33499aeb29 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/development/threading.md @@ -0,0 +1,7 @@ +--- +title: Threading +layout: docs +permalink: /development/threading.html +--- + +

👷👷‍♀️Under construction…

\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/display-node.md b/submodules/AsyncDisplayKit/docs/_docs/display-node.md new file mode 100755 index 0000000000..896c969dad --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/display-node.md @@ -0,0 +1,93 @@ +--- +title: ASDisplayNode +layout: docs +permalink: /docs/display-node.html +prevPage: containers-aspagernode.html +nextPage: cell-node.html +--- + +### Node Basics + +`ASDisplayNode` is the main view abstraction over `UIView` and `CALayer`. It initializes and owns a `UIView` in the same way `UIViews` create and own their own backing `CALayers`. + +
+SwiftObjective-C + +
+
+ASDisplayNode *node = [[ASDisplayNode alloc] init];
+node.backgroundColor = [UIColor orangeColor];
+node.bounds = CGRectMake(0, 0, 100, 100);
+
+NSLog(@"Underlying view: %@", node.view);
+	
+ + +
+
+ +A node has all the same properties as a `UIView`, so using them should feel very familiar to anyone familiar with UIKit. + +Properties of both views and layers are forwarded to nodes and can be easily accessed. + +
+SwiftObjective-C + +
+
+ASDisplayNode *node = [[ASDisplayNode alloc] init];
+node.clipsToBounds = YES;				  // not .masksToBounds
+node.borderColor = [UIColor blueColor];  //layer name when there is no UIView equivalent
+
+NSLog(@"Backing layer: %@", node.layer);
+	
+ + +
+
+ +As you can see, naming defaults to the `UIView` conventions*** unless there is no `UIView` equivalent. You also have access to your underlying `CALayer` just as you would when dealing with a plain `UIView`. + +When used with one of the node containers, a node’s properties will be set on a background thread, and its backing view/layer will be lazily constructed with the cached properties collected by the node. You rarely need to worry about jumping to a background thread as this will be taken care of by the framework, but it's important to know that this is happening under the hood. + +### View Wrapping + +In some cases, it is desirable to initialize a node and provide a view to be used as the backing view. These views are provided via a block that will return a view so that the actual construction of the view can be saved until later. These nodes’ display step happens synchronously. This is because a node can only be asynchronously displayed when it wraps an `_ASDisplayView` (the internal view subclass), not when it wraps a plain `UIView`. + +
+SwiftObjective-C + +
+
+ASDisplayNode *node = [ASDisplayNode alloc] initWithViewBlock:^{
+	SomeView *view  = [[SomeView alloc] init];
+	return view;
+}];
+	
+ + +
+
+ +Doing this allows you to wrap existing views if that is preferable to converting the `UIView` subclass to an `ASDisplayNode` subclass. + +
+ *** The only exception is that nodes use `position` instead of `center` for reasons beyond this intro. +
diff --git a/submodules/AsyncDisplayKit/docs/_docs/editable-text-node.md b/submodules/AsyncDisplayKit/docs/_docs/editable-text-node.md new file mode 100755 index 0000000000..9eccfd0321 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/editable-text-node.md @@ -0,0 +1,153 @@ +--- +title: ASEditableTextNode +layout: docs +permalink: /docs/editable-text-node.html +prevPage: scroll-node.html +nextPage: multiplex-image-node.html +--- + +`ASEditableTextNode` is available to be used anywhere you'd normally use a `UITextView` or `UITextField`. Under the hood, it uses a specialized `UITextView` as its backing view. You can access and configure this view directly any time after the node has loaded, as long as you do it on the main thread. + +It's also important to note that this node does not support layer backing due to the fact that it supports user interaction. + +### Basic Usage + +Using an editable text node as a text input is easy. If you want it to have text by default, you can assign an attributed string to the `attributedText` property. + +
+SwiftObjective-C + +
+
+ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init];
+
+editableTextNode.attributedText = [[NSAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet."];
+editableTextNode.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
+
+ + +
+
+ +### Placeholder Text + +If you want to display a text box with a placeholder that disappears after a user starts typing, just assign an attributed string to the `attributedPlaceholderText` property. + +
+SwiftObjective-C + +
+
+editableTextNode.attributedPlaceholderText = [[NSAttributedString alloc] initWithString:@"Type something here..."];
+
+ + +
+
+ +The property `isDisplayingPlaceholder` will initially return `YES`, but will toggle to `NO` any time the `attributedText` property is set to a non-empty string. + +### Typing Attributes + +To set up the style of the text your user will type into this text field, you can set the `typingAttributes`. + + +
+SwiftObjective-C + +
+
+editableTextNode.typingAttributes = @{NSForegroundColorAttributeName: [UIColor blueColor], 
+                                      NSBackgroundColorAttributeName: [UIColor redColor]};
+
+ + +
+
+ + +### ASEditableTextNode Delegate + +In order to respond to events associated with an editable text node, you can use any of the following delegate methods: + + +-- Indicates to the delegate that the text node began editing. + +
+SwiftObjective-C +
+
+- (void)editableTextNodeDidBeginEditing:(ASEditableTextNode *)editableTextNode;
+
+ +
+
+ +-- Asks the delegate whether the specified text should be replaced in the editable text node. + +
+SwiftObjective-C +
+
+- (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
+
+ +
+
+ +-- Indicates to the delegate that the text node's selection has changed. + +
+SwiftObjective-C +
+
+- (void)editableTextNodeDidChangeSelection:(ASEditableTextNode *)editableTextNode fromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange dueToEditing:(BOOL)dueToEditing;
+
+ +
+
+ +-- Indicates to the delegate that the text node's text was updated. + +
+SwiftObjective-C +
+
+- (void)editableTextNodeDidUpdateText:(ASEditableTextNode *)editableTextNode;
+
+ +
+
+ +--  Indicates to the delegate that the text node has finished editing. + +
+SwiftObjective-C +
+
+- (void)editableTextNodeDidFinishEditing:(ASEditableTextNode *)editableTextNode;
+
+ +
+
+ diff --git a/submodules/AsyncDisplayKit/docs/_docs/faq.md b/submodules/AsyncDisplayKit/docs/_docs/faq.md new file mode 100755 index 0000000000..fa7d3b2be7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/faq.md @@ -0,0 +1,125 @@ +--- +title: FAQ +layout: docs +permalink: /docs/faq.html +prevPage: subclassing.html +nextPage: containers-asviewcontroller.html +--- + +### Common Developer Mistakes + + + +### Common Conceptual Misunderstandings + + + +### Common Questions + + + +### Accessing the node's view before it is loaded +
+Node `-init` methods are often called off the main thread, therefore it is imperative that no UIKit objects are accessed. Examples of common errors include accessing the node's view or creating a gesture recognizer. Instead, these operations are ideal to perform in `-didLoad`. + +Interacting with UIKit in `-init` can cause crashes and performance problems. +
+ +### Make sure you access your data source outside the node block +
+The `indexPath` parameter is only valid _outside_ the node block returned in `nodeBlockForItemAtIndexPath:` or `nodeBlockForRowAtIndexPath:`. Because these blocks are executed on a background thread, the `indexPath` may be invalid by execution time, due to additional changes in the data source. + +See an example of how to correctly code a node block in the ASTableNode page. Just as with UIKit, it will cause an exception if Nil is returned from the block for any `ASCellNode`. +
+ +### Take steps to avoid a retain cycle in viewBlocks +
+When using `initWithViewBlock:` it is important to prevent a retain cycle by capturing a strong reference to self. The two ways that a cycle can be created are by using any instance variable inside the block or directly referencing self without using a weak pointer. + +You can use properties instead of instance variables as long as they are accessed on a weak pointer to self. + +Because viewBlocks are always executed on the main thread, it is safe to preform UIKit operations (including gesture recognizer creation and addition). + +Although the block is destroyed after the view is created, in the event that the block is never run and the view is never created, then a cycle can persist preventing memory from being released. +
+ +### ASCellNode Reusability +
+Texture does not use cell reuse, for a number of specific reasons, one side effect of this is that it eliminates the large class of bugs associated with cell reuse. +
+ +### LayoutSpecs Are Regenerated +
+A node's layoutSpec gets regenerated every time its `layoutThatFits:` method is called. +
+ +### Layout API Sizing +
+If you're confused by `ASRelativeDimension`, `ASRelativeSize`, `ASRelativeSizeRange` and `ASSizeRange`, check out our Layout API Sizing guide. +
+ +### CALayer's .cornerRadius Property Kills Performance +
+CALayer's' .cornerRadius property is a disastrously expensive property that should only be used when there is no alternative. It is one of the least efficient, most render-intensive properties on CALayer (alongside shadowPath, masking, borders, etc). These properties trigger offscreen rendering to perform the clipping operation on every frame — 60FPS during scrolling! — even if the content in that area isn't changing. + +Using `.cornerRadius` will visually degraded performance on iPhone 4, 4S, and 5 / 5C (along with comparable iPads / iPods) and reduce head room and make frame drops more likely on 5S and newer devices. + +For a longer discussion and easy alternative corner rounding solutions, please read our comprehensive corner rounding guide. +
+ +### Texture does not support UIKit Auto Layout or InterfaceBuilder +
+UIKit Auto Layout and InterfaceBuilder are not supported by Texture. + +However, Texture's Layout API provides a variety of ASLayoutSpec objects that allow implementing automatic layout which is more efficient (multithreaded, off the main thread), often easier to debug (can step into the code and see where all values come from, as it is open source), and reusable (you can build composable layouts that can be shared with different parts of the UI). +
+ +### ASDisplayNode keep alive reference + +
+
+
+ASTextNode *title=[[ASTextNode alloc]init];
+title.attributedString=Text;
+[self addSubnode:title];
+
+retain cycles
+(
+"-> _keepalive_node -> ASTextNode ",
+"-> _view -> _ASDisplayView "
+)
+
+
+
+ +
+This retain cycle is intentionally created because the node is in a "live" view hierarchy (it is inside the UIWindow that is onscreen). + +To see why this is necessary, consider that Apple also creates this retain cycle between UIView and CALayer. If you create a UIView and add its layer to a super layer, and then release the UIView, it will stay alive even though the CALayer delegate pointing to it is weak. + +For the same reason, if the node's view is a descendant of a window, but there is no reference to the node, we keep the node alive with a strong reference from the view. + +Good application design should not rely on this behavior, because a strong reference to the node should be maintained by the subnodes array or by an instance variable. However, this condition occasionally occurs, for example when using a UIView animation API. This cycle should never create a leak or even extend the lifecycle of a node any longer than it is absolutely necessary. +
+ +### UICollectionViewCell Compatibility + +Texture supports using UICollectionViewCells alongside native ASCellNodes. + +Note that these UIKit cells will **not** have the performance benefits of `ASCellNodes` (like preloading, async layout, and async drawing), even when mixed within the same `ASCollectionNode`. + +However, this interoperability allows developers the flexibility to test out the framework without needing to convert all of their cells at once. Read more here. diff --git a/submodules/AsyncDisplayKit/docs/_docs/getting-started.md b/submodules/AsyncDisplayKit/docs/_docs/getting-started.md new file mode 100755 index 0000000000..ae96e6cfb2 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/getting-started.md @@ -0,0 +1,53 @@ +--- +title: Getting Started +layout: docs +permalink: /docs/getting-started.html +nextPage: resources.html +--- + +Texture's basic unit is the `node`. `ASDisplayNode` is an abstraction +over `UIView`, which in turn is an abstraction over `CALayer`. Unlike views, which +can only be used on the main thread, nodes are thread-safe: you can +instantiate and configure entire hierarchies of them in parallel on background +threads. + +To keep its user interface smooth and responsive, your app should render at 60 +frames per second — the gold standard on iOS. This means the main thread +has one-sixtieth of a second to push each frame. That's 16 milliseconds to +execute all layout and drawing code! And because of system overhead, your code +usually has less than ten milliseconds to run before it causes a frame drop. + +Texture lets you move image decoding, text sizing and rendering, and +other expensive UI operations off the main thread, to keep the main thread available to +respond to user interaction. Texture has other tricks up its +sleeve too... but we'll get to that later. + +

Nodes

+ +If you're used to working with views, you already know how to use nodes. Most methods have a node equivalent and most `UIView` and `CALayer` properties are available as well. In any case where there is a naming discrepancy (such as `.clipsToBounds` vs `.masksToBounds`), nodes will default to the `UIView` name. The only exception is that nodes use position instead of center. + +Of course, you can always access the underlying view or layer directly via `node.view` or `node.layer`, just make sure to do it on the main thread! + +Texture offers a variety of nodes to replace the majority of the UIKit components that you are used to. Large scale apps have been able to completely write their UI using just Texture nodes. + +

Node Containers

+ +When converting an app to use Texture, a common mistake is to add nodes directly to an existing view hierarchy. Doing this will virtually guarantee that your nodes will flash as they are rendered. + +Instead, you should add nodes as subnodes of one of the many node container classes. These containers are in charge of telling contained nodes what state they're currently in so that data can be loaded and nodes can be rendered as efficiently as possible. You should think of these classes as the integration point between UIKit and Texture. + +

Layout Engine

+ +Texture's layout engine is both one of its most powerful and one of its most unique features. Based on the CSS FlexBox model, it provides a declarative way of specifying a custom node's size and layout of its subnodes. While all nodes are concurrently rendered by default, asynchronous measurement and layout are performed by providing an `ASLayoutSpec` for each node. + +

Advanced Developer Features

+ +Texture offers a variety of advanced developer features that cannot be found in UIKit or Foundation. Our developers have found that Texture allows simplifications in their architecture and improves developer velocity. + +(Full list coming soon!) + +

Adding Texture to your App

+ +If you are new to Texture, we recommend that you check out our ASDKgram example app. We've created a handy guide (coming soon!) with step-by-step directions and a follow along example on how to add Texture to an app. + +If you run into any problems along the way, reach out to us GitHub or the Texture Slack community for help. diff --git a/submodules/AsyncDisplayKit/docs/_docs/hit-test-slop.md b/submodules/AsyncDisplayKit/docs/_docs/hit-test-slop.md new file mode 100755 index 0000000000..fa126ce880 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/hit-test-slop.md @@ -0,0 +1,44 @@ +--- +title: Hit Test Slop +layout: docs +permalink: /docs/hit-test-slop.html +prevPage: layout-transition-api.html +nextPage: batch-fetching-api.html +--- + +`ASDisplayNode` has a `hitTestSlop` property of type `UIEdgeInsets` that when set to a non-zero inset, increase the bounds for hit testing to make it easier to tap or perform gestures on this node. + +ASDisplayNode is the base class for all nodes, so this property is available on any of Texture's nodes. + +
+Note: This affects the default implementation of -hitTest and -pointInside, so subclasses should call super if you override it and want hitTestSlop applied. +
+ +A node's ability to capture touch events is restricted by its parent's bounds + parent hitTestSlop UIEdgeInsets. Should you want to extend the hitTestSlop of a child outside its parent's bounds, simply extend the parent node's hitTestSlop to include the child's hitTestSlop needs. + +### Usage + +A common need for hit test slop, is when you have a text node (aka label) you'd like to use as a button. Often, the text node's height won't meet the 44 point minimum recommended for tappable areas. In that case, you can calculate the difference, and apply a negative inset to your label to increase the tappable area. + +
+SwiftObjective-C + +
+
+ASTextNode *textNode = [[ASTextNode alloc] init];
+
+CGFloat padding = (44.0 - button.bounds.size.height)/2.0;
+textNode.hitTestSlop = UIEdgeInsetsMake(-padding, 0, -padding, 0);
+
+ +
+
+ +
+To visualize hitTestSlop, check out the debug tool. +
diff --git a/submodules/AsyncDisplayKit/docs/_docs/image-modification-block.md b/submodules/AsyncDisplayKit/docs/_docs/image-modification-block.md new file mode 100755 index 0000000000..1e2e2716aa --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/image-modification-block.md @@ -0,0 +1,152 @@ +--- +title: Image Modification Blocks +layout: docs +permalink: /docs/image-modification-block.html +prevPage: inversion.html +nextPage: placeholder-fade-duration.html +--- + +Many times, operations that would affect the appearance of an image you're displaying are big sources of main thread work. Naturally, you want to move these to a background thread. + +By assigning an `imageModificationBlock` to your imageNode, you can define a set of transformations that need to happen asynchronously to any image that gets set on the imageNode. + +
+SwiftObjective-C + +
+
+_backgroundImageNode.imageModificationBlock = ^(UIImage *image) {
+	UIImage *newImage = [image applyBlurWithRadius:30
+		tintColor:[UIColor colorWithWhite:0.5 alpha:0.3]
+		saturationDeltaFactor:1.8
+		maskImage:nil];
+	return newImage ?: image;
+};
+
+//some time later...
+
+_backgroundImageNode.image = someImage;
+
+ + +
+
+ +The image named "someImage" will now be blurred asynchronously before being assigned to the imageNode to be displayed. + +### Adding image effects + +The most efficient way to add image effects is by leveraging the `imageModificationBlock` block. If a block is provided it can perform drawing operations on the image during the display phase. As display is happening on a background thread it will not block the main thread. + +In the following example we assume we have an avatar node that will be setup in `init` of a supernode and the image of the node should be rounded. We provide the `imageModificationBlock` and in there we call a convenience method that transforms the image passed in into a circular image and return it. + +
+SwiftObjective-C + +
+
+- (instancetype)init
+{
+// ...
+  _userAvatarImageNode.imageModificationBlock = ^UIImage *(UIImage *image) {
+    CGSize profileImageSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT);
+    return [image makeCircularImageWithSize:profileImageSize];
+  };
+  // ...
+}
+
+ + +
+ +
+ +The actual drawing code is nicely abstracted away in an UIImage category and looks like the following: + +
+SwiftObjective-C + +
+
+@implementation UIImage (Additions)
+- (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];
+
+  // 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
+
+ + +
+
+ +The imageModificationBlock is very handy and can be used to add all kind of image effects, such as rounding, adding borders, or other pattern overlays, without extraneous display calls. diff --git a/submodules/AsyncDisplayKit/docs/_docs/image-node.md b/submodules/AsyncDisplayKit/docs/_docs/image-node.md new file mode 100755 index 0000000000..4fcc2da69b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/image-node.md @@ -0,0 +1,90 @@ +--- +title: ASImageNode +layout: docs +permalink: /docs/image-node.html +prevPage: text-node.html +nextPage: network-image-node.html +--- + +`ASImageNode` is the Texture equivalent to `UIImageView`. The most basic difference is that images are decoded asynchronously by default. Of course, there are more advanced improvements as well such as GIF support and `imageModificationBlock`s. + +### Basic Usage + +Using an image node works exactly like using an image view. + +
+SwiftObjective-C + +
+
+ASImageNode *imageNode = [[ASImageNode alloc] init];
+
+imageNode.image = [UIImage imageNamed:@"someImage"];
+imageNode.contentMode = UIViewContentModeScaleAspectFill;
+
+ + +
+
+ + +### Image transformations and effects + +Many times, operations that would affect the appearance of an image you're displaying are big sources of main thread work. Naturally, you want to move these to a background thread. + +By assigning an `imageModificationBlock` to your `imageNode`, you can define a set of transformations that need to happen asynchronously to any image that gets set on the `imageNode`, including image effects such as rounding, adding borders, or other pattern overlays, without extraneous display calls. + +You can read more about it at Image Modification Blocks. + +### Image Cropping + +When an `imageNode`'s `contentMode` property is set to `UIViewContentModeScaleAspectFill`, it will automatically expand the image to fill the entire area of the imageNode, and crop any areas that go past the bounds due to scaling the image. + +By default, the expanded image will be centered within the bounds of the view. Take the following cat image. His face gets cut off by default. + + + +That's messed up. To fix it, you can set the `cropRect` property to move the image over. By default it is set to `CGRectMake(0.5, 0.5, 0.0, 0.0)`. + +The rectangle is specified as a "unit rectangle," using percentages of the source image's width and height. To show the image starting at the left side, you can set the `cropRect`'s `x` value to be `0.0`, meaning the image's origin should start at `{0, 0}` as opposed to the default. + +
+SwiftObjective-C + +
+
+self.animalImageNode.cropRect = CGRectMake(0, 0, 0.0, 0.0);
+
+ + +
+
+ +Leaving the width and height values at `0.0` means the image won't be stretched. + + + +Alternatively, you can set the `x` value of the origin to `1.0` to right align the image. + + + +### Forced Upscaling + +By default, an image won't be upscaled on the CPU when it is too small to fit into the bounds of the `imageNode` it has been set on. + +You can set `forceUpscaling` to `YES` if you'd like to change this fact. Doing so means your app will take up more memory any time you use an image that is smaller than its destination. + +### Detecting Image Scaling + +By using the pixel scaling tool, you can easily check each image in your app to see how much it has been scaled up or down. + +If images are too big, you risk rendering excessive amounts of image data, and when they're too small you spend time upscaling a low quality image. + +If you control your API, consider returning correctly scaled images so that this work can be avoided. diff --git a/submodules/AsyncDisplayKit/docs/_docs/index.html b/submodules/AsyncDisplayKit/docs/_docs/index.html new file mode 100755 index 0000000000..930d84ddd9 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/index.html @@ -0,0 +1,4 @@ +--- +layout: redirect +destination: /docs/getting-started.html +--- diff --git a/submodules/AsyncDisplayKit/docs/_docs/inset-layout-spec.md b/submodules/AsyncDisplayKit/docs/_docs/inset-layout-spec.md new file mode 100755 index 0000000000..ad3bd3e5b1 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/inset-layout-spec.md @@ -0,0 +1,7 @@ +--- +title: ASInsetLayoutSpec +layout: docs +permalink: /docs/inset-layout-spec.html +--- + +
😑 This page is coming soon...
\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/installation.md b/submodules/AsyncDisplayKit/docs/_docs/installation.md new file mode 100755 index 0000000000..81e6573a32 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/installation.md @@ -0,0 +1,114 @@ +--- +title: Installation +layout: docs +permalink: /docs/installation.html +prevPage: resources.html +nextPage: adoption-guide-2-0-beta1.html +--- + +Texture may be added to your project via CocoaPods or Carthage. Do not forget to import the framework header: + +
+
+
+#import 
+
+
+
+ +or create a Objective-C bridging header (Swift). If you have any problems installing Texture, please contact us on Github or Slack! + +## CocoaPods + +Texture is available on CocoaPods. Add the following to your Podfile: + +
+
+
+target 'MyApp' do
+	pod "Texture"
+end
+
+
+
+ +Quit Xcode completely before running + +
+
+
+> pod install
+
+
+
+ +in the project directory in Terminal. + +To update your version of Texture, run + +
+
+
+> pod update Texture
+
+
+
+ +in the project directory in Terminal. + +Don't forget to use the workspace `.xcworkspace` file, _not_ the project `.xcodeproj` file. + +## Carthage (standard build) + +
+The standard way to use Carthage is to have a Cartfile list the dependencies, and then run `carthage update` to download the dependenices into the `Cathage/Checkouts` folder and build each of those into frameworks located in the `Carthage/Build` folder, and finally the developer has to manually integrate in the project. +
+ +Texture is also available through Carthage. + +Add the following to your Cartfile to get the **latest release** branch: + +
+
+
+github "texturegroup/texture"
+
+
+
+ +
+Or, to get the **master** branch: + +
+
+
+github "texturegroup/texture" "master"
+
+
+
+ +
+Texture has its own Cartfile which lists its dependencies, so this is the only line you will need to include in your Cartfile. + +Run + +
+
+
+> carthage update
+
+
+
+ +
+in Terminal. This will fetch dependencies into a `Carthage/Checkouts` folder, then build each one. + +Look for terminal output confirming `Texture`, `PINRemoteImage (3.0.0-beta.2)` and `PINCache` are all fetched and built. The Texture framework Cartfile should handle the dependencies correctly. + +In Xcode, on your application targets’ **“General”** settings tab, in the **“Linked Frameworks and Libraries”** section, drag and drop each framework you want to use from the `Carthage/Build` folder on disk. + +## Carthage (light) + +Texture does not yet support the lighter way of using Carthage, in which you manually add the project files. This is because one of its dependencies, `PINCache` (a nested dependency of `PINRemoteImage`) does not yet have a project file. + +Without including `PINRemoteImage` and `PINCache`, you will not get Texture's full image feature set. diff --git a/submodules/AsyncDisplayKit/docs/_docs/intelligent-preloading.md b/submodules/AsyncDisplayKit/docs/_docs/intelligent-preloading.md new file mode 100755 index 0000000000..24440f1acd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/intelligent-preloading.md @@ -0,0 +1,95 @@ +--- +title: Intelligent Preloading +layout: docs +permalink: /docs/intelligent-preloading.html +prevPage: upgrading.html +nextPage: containers-overview.html +--- + +While a node's ability to be rendered and measured asynchronously and concurrently makes it quite powerful, another crucially important layer to Texture is the idea of intelligent preloading. + +As was pointed out in Getting Started, it is rarely advantageous to use a node outside of the context of one of the node containers. This is due to the fact that all nodes have a notion of their current interface state. + +This `interfaceState` property is constantly updated by an `ASRangeController` which all containers create and maintain internally. + +A node used outside of a container won't have its state updated by any range controller. This sometimes results in a flash as nodes are rendered after realizing they're already onscreen without any warning. + +## Interface State Ranges + +When nodes are added to a scrolling or paging interface they are typically in one of the following ranges. This means that as the scrolling view is scrolled, their interface states will be updated as they move through them. + + + +A node will be in one of following ranges: + + + + + + + + + + + + + + + + + + +
Interface StateDescription
PreloadThe furthest range out from being visible. This is where content is gathered from an external source, whether that’s some API or a local disk.
DisplayHere, display tasks such as text rasterization and image decoding take place.
VisibleThe node is onscreen by at least one pixel.
+ +## ASRangeTuningParameters + +The size of each of these ranges is measured in "screenfuls". While the default sizes will work well for many use cases, they can be tweaked quite easily by setting the tuning parameters for range type on your scrolling node. + + + +In the above visualization of a scrolling collection, the user is scrolling down. As you can see, the sizes of the ranges in the leading direction are quite a bit larger than the content the user is moving away from (the trailing direction). If the user were to change directions, the leading and trailing sides would dynamically swap in order to keep memory usage optimal. This allows you to worry about defining the leading and trailing sizes without having to worry about reacting to the changing scroll directions of your user. + +Intelligent preloading also works in multiple dimensions. + +## Interface State Callbacks + +As a user scrolls, nodes move through the ranges and react appropriately by loading data, rendering, etc. Your own node subclasses can easily tap into this mechanism by implementing the corresponding callback methods. + +#### Visible Range + +
+ +
+
+-didEnterVisibleState
+-didExitVisibleState
+
+
+
+ +#### Display Range + +
+ +
+
+-didEnterDisplayState
+-didExitDisplayState
+
+
+
+ +#### Preload Range + +
+ +
+
+-didEnterPreloadState
+-didExitPreloadState
+
+
+
+ +
+Just remember to call super ok? 😉 diff --git a/submodules/AsyncDisplayKit/docs/_docs/inversion.md b/submodules/AsyncDisplayKit/docs/_docs/inversion.md new file mode 100755 index 0000000000..5dd18140dd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/inversion.md @@ -0,0 +1,34 @@ +--- +title: Inversion +layout: docs +permalink: /docs/inversion.html +prevPage: automatic-subnode-mgmt.html +nextPage: image-modification-block.html +--- + +`ASTableNode` and `ASCollectionNode` have a `inverted` property of type `BOOL` that when set to `YES`, will automatically invert the content so that it's layed out bottom to top, that is the 'first' (indexPath 0, 0) node is at the bottom rather than the top as usual. This is extremely covenient for chat/messaging apps, and with Texture it only takes one property. + +When this is enabled, developers only have to take one more step to have full inversion support and that is to adjust the `contentInset` of their `ASTableNode` or `ASCollectionNode` like so: + +
+ + Swift + Objective-C + + +
+
+ CGFloat inset = [self topBarsHeight];
+ self.tableNode.view.contentInset = UIEdgeInsetsMake(0, 0, inset, 0);
+ self.tableNode.view.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, inset, 0);
+  
+ +
+  let inset = self.topBarsHeight
+  self.tableNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: inset, right: 0.0)
+  self.tableNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: inset, right: 0.0)
+  
+
+
+ +See the SocialAppLayout-Inverted example project for more details. diff --git a/submodules/AsyncDisplayKit/docs/_docs/layer-backing.md b/submodules/AsyncDisplayKit/docs/_docs/layer-backing.md new file mode 100755 index 0000000000..17f4e70fb3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layer-backing.md @@ -0,0 +1,29 @@ +--- +title: Layer Backing +layout: docs +permalink: /docs/layer-backing.html +prevPage: accessibility.html +nextPage: subtree-rasterization.html +--- + +In some cases, you can substantially improve your app's performance by using layers instead of views. **We recommend enabling layer-backing in any custom node that doesn't need touch handling**. + +With UIKit, manually converting view-based code to layers is laborious due to the difference in APIs. Worse, if at some point you need to enable touch handling or other view-specific functionality, you have to manually convert everything back (and risk regressions!). + +With all Texture nodes, converting an entire subtree from views to layers is as simple as: + +
+SwiftObjective-C +
+
+rootNode.isLayerBacked = YES;
+
+ +
+
+ +...and if you need to go back, it's as simple as deleting one line. + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout-api-debugging.md b/submodules/AsyncDisplayKit/docs/_docs/layout-api-debugging.md new file mode 100755 index 0000000000..5b86012570 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout-api-debugging.md @@ -0,0 +1,69 @@ +--- +title: Layout Debugging +layout: docs +permalink: /docs/layout-api-debugging.html +prevPage: automatic-layout-containers.html +nextPage: layout-api-sizing.html +--- + +Here are some helpful questions to ask yourself when you encounter any issues composing layoutSpecs. + +## Am I the child of an `ASStackLayoutSpec` or an `ASStaticLayoutSpec`? +
+Certain `ASLayoutable` properties will _only_ apply when the layoutable is a child of a _stack_ spec (the child is called an ASStackLayoutable), while other properties _only_ apply when the layoutable is a child of a _static_ spec (the child is called an ASStaticLayoutable). + +- table of [`ASStackLayoutables` properties](http://texturegroup.org/docs/automatic-layout-containers.html#asstacklayoutable-properties) +- table of [`ASStaticLayoutable` properties](http://texturegroup.org/docs/automatic-layout-containers.html#asstaticlayoutable-properties) + +All ASLayoutable properties can be applied to _any_ layoutable (e.g. any node or layout spec), however certain properties will only take effect depending on the type of the parent layout spec they are wrapped in. + +## Have I considered where to set `ASLayoutable` properties? +
+Let's say you have a node (`n1`) and you wrap it in a layout spec (`s1`). If you want to wrap the layout spec (`s1`) in a stack or static spec (`s2`), you will need to set all of the properties on the spec (`s1`) and not the node (`n1`). + +A common examples of this confusion involves flex grow and flex shrink. E.g. a node with `.flexGrow` enabled is wrapped in an inset spec. The inset spec will not grow as we expect. **Solution:** enable `.flexGrow` on the inset spec as well. + +## Have I provided sizes for any node that lacks an intrinsic size? +
+Texture's layout pass is recursive, starting at the layout spec returned from `-layoutSpecThatFits:` and proceeding down until it reaches the leaf nodes included in any nested layout specs. + +Some leaf nodes have a concept of their own intrinsic size, such as ASTextNode or ASImageNode. A text node knows the length of its formatted string and an ASImageNode knows the size of its static image. Other leaf nodes require an intrinsic size to be set. + +Nodes that require the developer to provide an intrinsic size: + +- `ASNetworkImageNode` or `ASMultiplexImageNode` have no intrinsic size until the image is downloaded. **A size must be provided for either node.** +- `ASVideoNode` or `ASVideoNodePlayer` have no intrinsic size until the video is downloaded. **A size must be provided for either node.** +- `ASDisplayNode` custom subclasses may provide their intrinisc size by implementing `-calculateSizeThatFits:`. + +To provide an intrinisc size for these nodes that lack intrinsic sizes (even if only momentarily), you can set one of the following: + +- set `.preferredFrameSize` on any node. +- set `.sizeRange` for children of **static** specs only. +- implement `-calculateSizeThatFits:` for **custom ASDisplayNode subclasses only**. + +*_Note that .preferredFrameSize is not considered by ASTextNodes. Also, setting .sizeRange on a node will override the node's intrinisic size provided by -calculateSizeThatFits:_ + +## When do I use `.preferredFrameSize` vs `.sizeRange`? +
+Set `.preferredFrameSize` to set a size for any node. Note that setting .preferredFrameSize on an `ASTextNode` will silently fail. We are working on fixing this, but in the meantime, you can wrap the ASTextNode in a static spec and provide it a .sizeRange. + +Set `.sizeRange` to set a size range for any node or layout spec that is the child of a *static* spec consisting. `.sizeRange` is the *only* way to set a size on a layout spec. Again, when using `.sizeRange`, you *must wrap the layoutable in a static layout spec for it to take effect.* + +A `.sizeRange` consists of a minimum and maximum constrained size (these sizes can be a specific point value or a relative value, like 70%). For details on the `.sizeRange` property's custom value type, check out our [Layout API Sizing guide](http://texturegroup.org/docs/layout-api-sizing.html). + +## `ASRelativeDimension` vs `ASRelativeSize` vs `ASRelativeSizeRange` vs `ASSizeRange` +
+Texture's Layout API supports configuring node and layout spec sizes with specific point values as well as relative values. Read the [Layout API Sizing guide](http://texturegroup.org/docs/layout-api-sizing.html) for a helpful chart and documentation on our custom layout value types. + +## Debugging layout specs with ASCII art +
+Calling `-asciiArtString` on any `ASDisplayNode` or `ASLayoutSpec` returns an ascii-art representation of the object and its children. An example of a simple layoutSpec ascii-art console output can be seen below. + +``` +-----------------------ASStackLayoutSpec---------------------- +| -----ASStackLayoutSpec----- -----ASStackLayoutSpec----- | +| | ASImageNode | | ASImageNode | | +| | ASImageNode | | ASImageNode | | +| --------------------------- --------------------------- | +-------------------------------------------------------------- + ``` diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout-api-sizing.md b/submodules/AsyncDisplayKit/docs/_docs/layout-api-sizing.md new file mode 100755 index 0000000000..7937793c61 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout-api-sizing.md @@ -0,0 +1,171 @@ +--- +title: Layout API Sizing +layout: docs +permalink: /docs/layout-api-sizing.html +prevPage: layout-api-debugging.html +nextPage: layout-transition-api.html +--- + +The easiest way to understand the compound dimension types in the Layout API is to see all the units in relation to one another. + + + +## Values (CGFloat, ASRelativeDimension) +
+`ASRelativeDimension` is essentially a normal **CGFloat with support for representing either a point value, or a % value**. It allows the same API to take in both fixed values, as well as relative ones. + +ASRelativeDimension is used to set the `flexBasis` property on a child of an `ASStackLayoutSpec`. The flexBasis property specifies the initial size in the stack dimension for this object, where the stack dimension is whether it is a horizontal or vertical stack. + +When a relative (%) value is used, it is resolved against the size of the parent. For example, an item with 50% flexBasis will ultimately have a point value set on it at the time that the stack achieves a concrete size. + +
+Note that .flexBasis can be set on any <ASLayoutable> (a node, or a layout spec), but will only take effect if that element is added as a child of a stack layout spec. This container-dependence of layoutable properties is a key area we’re working on clarifying. +
+ +#### Constructing ASRelativeDimensions +
+`ASDimension.h` contains 3 convenience functions to construct an `ASRelativeDimension`. It is easiest to use function that corresponds to the type (top 2 functions). + +
+SwiftObjective-C + +
+
+ASRelativeDimensionMakeWithPoints(CGFloat points);
+ASRelativeDimensionMakeWithPercent(CGFloat percent);
+ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value);
+
+ +
+
+ +#### ASRelativeDimension Example +
+`PIPlaceSingleDetailNode` uses flexBasis to set 2 child nodes of a horizontal stack to share the width 40 / 60: + +
+SwiftObjective-C + +
+
+leftSideStack.flexBasis = ASRelativeDimensionMakeWithPercent(0.4f);
+self.detailLabel.flexBasis  = ASRelativeDimensionMakeWithPercent(0.6f);
+[horizontalStack setChildren:@[leftSideStack, self.detailLabel]];
+
+ +
+
+ + + +## Sizes (CGSize, ASRelativeSize) +
+`ASRelativeSize` is **similar to a CGSize, but its width and height may represent either a point or percent value.**  In fact, their unit type may even be different from one another. `ASRelativeSize` doesn't have a direct use in the Layout API, except to construct an `ASRelativeSizeRange`. + +- an `ASRelativeSize` consists of a `.width` and `.height` that are each `ASRelativeDimensions`. + +- the type of the width and height are independent; either one individually, or both, may be a point or percent value. (e.g. you could specify that an ASRelativeSize that has a height in points, but a variable % width) + +#### Constructing ASRelativeSizes +
+`ASRelativeSize.h` contains 2 convenience functions to construct an `ASRelativeSize`. **If you don't need to support relative (%) values, you can construct an `ASRelativeSize` with just a CGSize.** + +
+SwiftObjective-C + +
+
+ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height);
+ASRelativeSizeMakeWithCGSize(CGSize size);
+
+ +
+
+ +## Size Ranges (ASSizeRange, ASRelativeSizeRange) + +Because the layout spec system allows flexibility with elements growing and shrinking, we sometimes need to provide limits / boundaries to its flexibility. + +There are two size range types, but in essence, both contain a minimum and maximum size and that are used to influence the result of layout measurements. + +In the Pinterest code base, the **minimum size seems to be only necessary for stack specs in order to determine how much space to fill in between the children.** For example, with buttons in a nav bar, we don’t want them to stack as closely together as they can fit — rather a minimum width, as wide as the screen, is specified and causes the stack to add spacing to satisfy that constraint. + +**It’s much more common that the “max” constraint is what matters, though.** This is the case when text is wrapping or truncating - it’s encountering the maximum allowed width. Setting a minimum width for text doesn’t actually do anything—the text can’t be made longer—unless it’s in a stack, and spacing is added around it. + +#### ASSizeRange +
+UIKit doesn't provide a structure to bundle a minimum and maximum CGSize. So `ASSizeRange` was created to support **a minimum and maximum CGSize pair**. + +The `constrainedSize` that is passed as an input to `layoutSpecThatFits:` is an `ASSizeRange`. + +
+SwiftObjective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize;
+
+ +
+
+ +#### ASRelativeSizeRange +
+`ASRelativeSizeRange` is essentially **a minimum and maximum size pair, that are used to constrain the size of a layout object.** The minimum and maximum sizes must **support both point and relative sizes**, which is where our friend the ASRelativeSize comes in. Hence, an ASRelativeSizeRange consists of a minimum and maximum `ASRelativeSize`. + +ASRelativeSizeRange is used to set the `sizeRange` property on a child of an `ASStaticLayoutSpec`. If specified, the child's size is restricted according to this size. + +
+Note that .sizeRange can be set on any <ASLayoutable> (a node, or a layout spec), but will only take effect if that element is added as a child of a static layout spec. This container-dependence of layoutable properties is a key area we’re working on clarifying. +
+ +#### ASSizeRange vs. ASRelativeSizeRange +
+Why do we pass a `ASSizeRange *constrainedSize` to a node's `layoutSpecThatFits:` function, but a `ASRelativeSizeRange` for the `.sizeRange` property on an element provided as a child of a layout spec? + + It’s pretty rare that you need the percent feature for a .sizeRange feature, but it’s there to make the API as flexible as possible. The input value of the constrainedSize that comes into the argument, has already been resolved by the parent’s size. It may have been influenced by a percent type, but has always be converted by that point into points. + +#### Constructing ASRelativeSizeRange +
+`ASRelativeSize.h` contains 4 convenience functions to construct an `ASRelativeSizeRange` from the various smaller units. + +- Percentage and point values can be combined. E.g. you could specify that an object is a certain height in points, but a variable percentage width. + +- If you only care to constrain the min / max or width / height, you can pass in `CGFLOAT_MIN`, `CGFLOAT_MAX`, `constrainedSize.max.width`, etc + +Most of the time, relative values are not needed for a size range _and_ the design requires an object to be forced to a particular size (min size = max size = no range). In this common case, you can use: + +
+SwiftObjective-C + +
+
+ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact);
+
+ +
+
+ +### Sizing Conclusion +
+Here we have our original table, which has been annotated to show the uses of the various units in the Layout API. + + + +It’s worth noting that that there’s a certain flexibility to be able to use so many powerful options with a single API - flexBasis and sizeRange can be used to set points and percentages in different directions. However, since the majority of do not use the full set of options, we should adjust the API so that the powerful capabilities are a slightly more hidden. + diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout-engine.md b/submodules/AsyncDisplayKit/docs/_docs/layout-engine.md new file mode 100755 index 0000000000..f73bed3225 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout-engine.md @@ -0,0 +1,87 @@ +--- +title: Layout Engine +layout: docs +permalink: /docs/layout-engine.html +prevPage: subclassing.html +nextPage: containers-overview.html +--- + +Texture's layout engine is based on the CSS Box Model. While it is the feature of the framework that bears the weakest resemblance to the UIKit equivalent (AutoLayout), it is also among the most useful features once you've gotten used to it. With enough practice, you may just come to prefer creating declarative layouts to the constraint based approach. ;] + +The main way you participate in this system is by implementing `-layoutSpecThatFits:` in a node subclass. Here, you declaratively build up layout specs from the inside out, returning the final spec which will contain the rest. + +
+SwiftObjective-C +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec];
+  verticalStack.direction          = ASStackLayoutDirectionVertical;
+  verticalStack.spacing            = 4.0;
+  [verticalStack setChildren:_commentNodes];
+
+  return verticalStack;
+}
+  
+ + +
+
+ +Whle this example is extremely simple, it gives you an idea of how to use a layout spec. A stack layout spec, for instance, defines a layout of nodes in which the chlidren will be laid out adjacently, in the direction specified, with the spacing specified. It is very similar to `UIStackView` but with the added benefit of backwards compatibility. + +### ASLayoutable + +Layout spec's children can be any object whose class conforms to the `` protocol. All nodes, as well as all layout specs conform to the `` protocol. This means that your layout can be built up in composable chunks until you have what you want. + +Say you wanted to add 8 pts of padding to the stack you've already set up: + +
+SwiftObjective-C +
+ +
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec];
+  verticalStack.direction          = ASStackLayoutDirectionVertical;
+  verticalStack.spacing            = 4.0;
+  [verticalStack setChildren:_commentNodes];
+  
+  UIEdgeInsets insets = UIEdgeInsetsMake(8, 8, 8, 8);
+  ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets 
+                                      child:verticalStack];
+  
+  return insetSpec;
+}
+  
+ + +
+
+ +You can easily do that by making that stack the child of an inset layout spec. + +Naturally, using layout specs takes a bit of practice so to learn more, check out the layout section. diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout-options.md b/submodules/AsyncDisplayKit/docs/_docs/layout-options.md new file mode 100755 index 0000000000..709a7f8be3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout-options.md @@ -0,0 +1,52 @@ +--- +title: Layout Options +layout: docs +permalink: /docs/layout-options.html +prevPage: automatic-layout-debugging.html +nextPage: layer-backing.html +--- + +When using Texture, you have three options for layout. Note that UIKit Autolayout is **not** supported by Texture. +#Manual Sizing & Layout + +This original layout method shipped with Texture 1.0 and is analogous to UIKit's layout methods. Use this method for ASViewControllers (unless you subclass the node). + +`[ASDisplayNode calculateSizeThatFits:]` **vs.** `[UIView sizeThatFits:]` + +`[ASDisplayNode layout]` **vs.** `[UIView layoutSubviews]` + +###Advantages (over UIKit) +- Eliminates all main thread layout cost +- Results are cached + +###Shortcomings (same as UIKit): +- Code duplication between methods +- Logic is not reusable + +#Unified Sizing & Layout + +This layout method does not have a UIKit analog. It is implemented by calling + +`- (ASLayout *)calculateLayoutThatFits: (ASSizeRange)constraint` + +###Advantages +- zero duplication +- still async, still cached + +###Shortcomings +- logic is not reusable, and is still manual + +# Automatic, Extensible Layout + +This is the reccomended layout method. It does not have a UIKit analog and is implemented by calling + +`- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constraint` +###Advantages +- can reuse even complex, custom layouts +- built-in specs provide automatic layout +- combine to compose new layouts easily +- still async, cached, and zero duplication + +The diagram below shows how options #2 and #3 above both result in an ASLayout, except that in option #3, the ASLayout is produced automatically by the ASLayoutSpec. + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout-transition-api.md b/submodules/AsyncDisplayKit/docs/_docs/layout-transition-api.md new file mode 100755 index 0000000000..e1f7afc702 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout-transition-api.md @@ -0,0 +1,259 @@ +--- +title: Layout Transition API +layout: docs +permalink: /docs/layout-transition-api.html +prevPage: layout2-api-sizing.html +nextPage: hit-test-slop.html +--- + +The Layout Transition API was designed to make all animations with Texture easy - even transforming an entire set of views into a completely different set of views! + +With this system, you simply specify the desired layout and Texture will do the work to figure out differences from the current layout. It will automatically add new elements, remove unneeded elements after the transition, and update the position of any existing elements. + +There are also easy to use APIs that allow you to fully customize the starting position of newly introduced elements, as well as the ending position of removed elements. + +
+Use of Automatic Subnode Management is required to use the Layout Transition API. +
+ +## Animating between Layouts +
+The layout Transition API makes it easy to animate between a node's generated layouts in response to some internal state change in a node. + +Imagine you wanted to implement this sign up form and animate in the new field when tapping the next button: + +![Imgur](http://i.imgur.com/Dsf1R72.gif) + +A standard way to implement this would be to create a container node called `SignupNode` that includes two editable text field nodes and a button node as subnodes. We'll include a property on the SignupNode called `fieldState` that will be used to select which editable text field node to show when the node calculates its layout. + +The internal layout spec of the `SignupNode` container would look something like this: + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  FieldNode *field;
+  if (self.fieldState == SignupNodeName) {
+    field = self.nameField;
+  } else {
+    field = self.ageField;
+  }
+
+  ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init];
+  [stack setChildren:@[field, self.buttonNode]];
+
+  UIEdgeInsets insets = UIEdgeInsetsMake(15.0, 15.0, 15.0, 15.0);
+  return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:stack];
+}
+
+ +
+
+ +To trigger a transition from the `nameField` to the `ageField` in this example, we'll update the SignupNode's `.fieldState` property and begin the transition with `transitionLayoutWithAnimation:`. + +This method will invalidate the current calculated layout and recompute a new layout with the `ageField` now in the stack. + +
+ + Swift + Objective-C + +
+
+self.signupNode.fieldState = SignupNodeAge;
+
+[self.signupNode transitionLayoutWithAnimation:YES];
+
+ +
+
+ +In the default implementation of this API, the layout will recalculate the new layout and use its sublayouts to size and position the SignupNode's subnodes without animation. Future versions of this API will likely include a default animation between layouts and we're open to feedback on what you'd like to see here. However, we'll need to implement a custom animation block to handle the signup form case. + +The example below represents an override of `animateLayoutTransition:` in the SignupNode. + +This method is called after the new layout has been calculated via `transitionLayoutWithAnimation:` and in the implementation we'll perform a specific animation based upon the fieldState property that was set before the animation was triggered. + +
+ + Swift + Objective-C + +
+
+- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
+{
+  if (self.fieldState == SignupNodeName) {
+    CGRect initialNameFrame = [context initialFrameForNode:self.ageField];
+    initialNameFrame.origin.x += initialNameFrame.size.width;
+    self.nameField.frame = initialNameFrame;
+    self.nameField.alpha = 0.0;
+    CGRect finalAgeFrame = [context finalFrameForNode:self.nameField];
+    finalAgeFrame.origin.x -= finalAgeFrame.size.width;
+    [UIView animateWithDuration:0.4 animations:^{
+      self.nameField.frame = [context finalFrameForNode:self.nameField];
+      self.nameField.alpha = 1.0;
+      self.ageField.frame = finalAgeFrame;
+      self.ageField.alpha = 0.0;
+    } completion:^(BOOL finished) {
+      [context completeTransition:finished];
+    }];
+  } else {
+    CGRect initialAgeFrame = [context initialFrameForNode:self.nameField];
+    initialAgeFrame.origin.x += initialAgeFrame.size.width;
+    self.ageField.frame = initialAgeFrame;
+    self.ageField.alpha = 0.0;
+    CGRect finalNameFrame = [context finalFrameForNode:self.ageField];
+    finalNameFrame.origin.x -= finalNameFrame.size.width;
+    [UIView animateWithDuration:0.4 animations:^{
+      self.ageField.frame = [context finalFrameForNode:self.ageField];
+      self.ageField.alpha = 1.0;
+      self.nameField.frame = finalNameFrame;
+      self.nameField.alpha = 0.0;
+    } completion:^(BOOL finished) {
+      [context completeTransition:finished];
+    }];
+  }
+}
+
+ +
+
+ +The passed `ASContextTransitioning` context object in this method contains relevant information to help you determine the state of the nodes before and after the transition. It includes getters into old and new constrained sizes, inserted and removed nodes, and even the raw old and new `ASLayout` objects. In the `SignupNode` example, we're using it to determine the frame for each of the fields and animate them in an out of place. + +It is imperative to call `completeTransition:` on the context object once your animation has finished, as it will perform the necessary internal steps for the newly calculated layout to become the current `calculatedLayout`. + +Note that there hasn't been a use of `addSubnode:` or `removeFromSupernode` during the transition. Texture's layout transition API analyzes the differences in the node hierarchy between the old and new layout, implicitly performing node insertions and removals via Automatic Subnode Management. + +Nodes are inserted before your implementation of `animateLayoutTransition:` is called and this is a good place to manually manage the hierarchy before you begin the animation. Removals are performed in `didCompleteLayoutTransition:` after you call `completeTransition:` on the context object. If you need to manually perform deletions, override `didCompleteLayoutTransition:` and perform your custom operations. Note that this will override the default behavior and it is recommended to either call `super` or walk through the `removedSubnodes` getter in the context object to perform the cleanup. + +Passing `NO` to `transitionLayoutWithAnimation:` will still run through your `animateLayoutTransition:` and `didCompleteLayoutTransition:` implementations with the `[context isAnimated]` property set to `NO`. It is your choice on how to handle this case — if at all. An easy way to provide a default implementation this is to call super: + +
+ + Swift + Objective-C + +
+
+- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
+{
+  if ([context isAnimated]) {
+    // perform animation
+  } else {
+    [super animateLayoutTransition:context];
+  }
+}
+
+ +
+
+ +## Animating constrainedSize Changes +
+There will be times you'll simply want to respond to bounds changes to your node and animate the recalculation of its layout. To handle this case, call `transitionLayoutWithSizeRange:animated:` on your node. + +This method is similar to `transitionLayoutWithAnimation:`, but will not trigger an animation if the passed `ASSizeRange` is equal to the current `constrainedSizeForCalculatedLayout` value. This is great for responding to rotation events and view controller size changes: + +
+ + Swift + Objective-C + +
+
+- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
+{
+  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
+  [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
+    [self.node transitionLayoutWithSizeRange:ASSizeRangeMake(size, size) animated:YES];
+  } completion:nil];
+}
+
+ +
+
+ +## Examples that use the Layout Transition API + +- [ASDKLayoutTransition](https://github.com/texturegroup/texture/tree/master/examples/ASDKLayoutTransition) diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-api-sizing.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-api-sizing.md new file mode 100755 index 0000000000..6459eadcf1 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-api-sizing.md @@ -0,0 +1,194 @@ +--- +title: Layout API Sizing +layout: docs +permalink: /docs/layout2-api-sizing.html +nextPage: layout-transition-api.html +--- + +The easiest way to understand the compound dimension types in the Layout API is to see all the units in relation to one another. + + + +## Values (`CGFloat`, `ASDimension`) + +`ASDimension` is essentially a **normal CGFloat with support for representing either a point value, a relative percentage value, or an auto value**. + +This unit allows the same API to take in both fixed values, as well as relative ones. + +
+ + Swift + Objective-C + +
+
+// dimension returned is relative (%)
+ASDimensionMake(@"50%");  
+ASDimensionMakeWithFraction(0.5);
+
+// dimension returned in points
+ASDimensionMake(@"70pt");
+ASDimensionMake(70);      
+ASDimensionMakeWithPoints(70);
+
+ +
+
+ +### Example using `ASDimension` + +`ASDimension` is used to set the `flexBasis` property on a child of an `ASStackLayoutSpec`. The `flexBasis` property specifies an object's initial size in the stack dimension, where the stack dimension is whether it is a horizontal or vertical stack. + +In the following view, we want the left stack to occupy `40%` of the horizontal width and the right stack to occupy `60%` of the width. + + + +We do this by setting the `.flexBasis` property on the two childen of the horizontal stack: + +
+ + Swift + Objective-C + +
+
+self.leftStack.style.flexBasis = ASDimensionMake(@"40%");
+self.rightStack.style.flexBasis = ASDimensionMake(@"60%");
+
+[horizontalStack setChildren:@[self.leftStack, self.rightStack]];
+
+ +
+
+ +## Sizes (`CGSize`, `ASLayoutSize`) + +`ASLayoutSize` is similar to a `CGSize`, but its **width and height values may represent either a point or percent value**. The type of the width and height are independent; either one may be a point or percent value. + +
+ + Swift + Objective-C + +
+
+ASLayoutSizeMake(ASDimension width, ASDimension height);
+
+ +
+
+ +
+`ASLayoutSize` is used for setting a layout element's `.preferredLayoutSize`, `.minLayoutSize` and `.maxLayoutSize` properties. It allows the same API to take in both fixed sizes, as well as relative ones. + +
+ + Swift + Objective-C + +
+
+// Dimension type "Auto" indicates that the layout element may 
+// be resolved in whatever way makes most sense given the circumstances
+ASDimension width = ASDimensionMake(ASDimensionUnitAuto, 0);  
+ASDimension height = ASDimensionMake(@"50%");
+
+layoutElement.style.preferredLayoutSize = ASLayoutSizeMake(width, height);
+
+ +
+
+ +
+If you do not need relative values, you can set the layout element's `.preferredSize`, `.minSize` and `.maxSize` properties. The properties take regular `CGSize` values. + +
+ + Swift + Objective-C + +
+
+layoutElement.style.preferredSize = CGSizeMake(30, 160);
+
+ +
+
+ +
+Most of the time, you won't want to constrain both width and height. In these cases, you can individually set a layout element's size properties using `ASDimension` values. + +
+ + Swift + Objective-C + +
+
+layoutElement.style.width     = ASDimensionMake(@"50%");
+layoutElement.style.minWidth  = ASDimensionMake(@"50%");
+layoutElement.style.maxWidth  = ASDimensionMake(@"50%");
+
+layoutElement.style.height    = ASDimensionMake(@"50%");
+layoutElement.style.minHeight = ASDimensionMake(@"50%");
+layoutElement.style.maxHeight = ASDimensionMake(@"50%");
+
+ +
+
+ +## Size Range (`ASSizeRange`) + +`UIKit` doesn't provide a structure to bundle a minimum and maximum `CGSize`. So, `ASSizeRange` was created to support **a minimum and maximum CGSize pair**. + +`ASSizeRange` is used mostly in the internals of the layout API. However, the `constrainedSize` value passed as an input to `layoutSpecThatFits:` is an `ASSizeRange`. + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize;
+
+ +
+
+ +The `constrainedSize` passed to an `ASDisplayNode` subclass' `layoutSpecThatFits:` method is the minimum and maximum sizes that the node should fit in. The minimum and maximum `CGSize`s contained in `constrainedSize` can be used to size the node's layout elements. diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-conversion-guide.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-conversion-guide.md new file mode 100755 index 0000000000..6283cf1ec0 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-conversion-guide.md @@ -0,0 +1,519 @@ +--- +title: Upgrading to Layout 2.0 (Beta) +layout: docs +permalink: /docs/layout2-conversion-guide.html +--- + +A list of the changes: + +- Introduction of true flex factors +- `ASStackLayoutSpec` `.alignItems` property default changed to `ASStackLayoutAlignItemsStretch` +- Rename `ASStaticLayoutSpec` to `ASAbsoluteLayoutSpec` +- Rename `ASLayoutable` to `ASLayoutElement` +- Set `ASLayoutElement` properties via `style` property +- Easier way to size of an `ASLayoutElement` +- Deprecation of `-[ASDisplayNode preferredFrameSize]` +- Deprecation of `-[ASLayoutElement measureWithSizeRange:]` +- Deprecation of `-[ASDisplayNode measure:]` +- Removal of `-[ASAbsoluteLayoutElement sizeRange]` +- Rename `ASRelativeDimension` to `ASDimension` +- Introduction of `ASDimensionUnitAuto` + +In addition to the inline examples comparing **1.x** layout code vs **2.0** layout code, the [example projects](https://github.com/texturegroup/texture/tree/master/examples) and layout documentation have been updated to use the new API. + +All other **2.0** changes not related to the Layout API are documented here. + +## Introduction of true flex factors + +With **1.x** the `flexGrow` and `flexShrink` properties were of type `BOOL`. + +With **2.0**, these properties are now type `CGFloat` with default values of `0.0`. + +This behavior is consistent with the Flexbox implementation for web. See [`flexGrow`](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-grow) and [`flexShrink`](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-shrink) for further information. + +
+SwiftObjective-C + +
+
+id<ASLayoutElement> layoutElement = ...;
+
+// 1.x:
+layoutElement.flexGrow = YES;
+layoutElement.flexShrink = YES;
+
+// 2.0:
+layoutElement.style.flexGrow = 1.0;
+layoutElement.style.flexShrink = 1.0;
+
+ + +
+
+ +## `ASStackLayoutSpec`'s `.alignItems` property default changed + +`ASStackLayoutSpec`'s `.alignItems` property default changed to `ASStackLayoutAlignItemsStretch` instead of `ASStackLayoutAlignItemsStart` to align with the CSS align-items property. + +## Rename `ASStaticLayoutSpec` to `ASAbsoluteLayoutSpec` & behavior change + +`ASStaticLayoutSpec` has been renamed to `ASAbsoluteLayoutSpec`, to be consistent with web terminology and better represent the intended behavior. + +
+SwiftObjective-C + +
+
+// 1.x:
+ASStaticLayoutSpec *layoutSpec = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[...]];
+
+// 2.0:
+ASAbsoluteLayoutSpec *layoutSpec = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[...]];
+
+ +
+
+ +
+**Please note** that there has also been a behavior change introduced. The following text overlay layout was previously created using a `ASStaticLayoutSpec`, `ASInsetLayoutSpec` and `ASOverlayLayoutSpec` as seen in the code below. + + + +
+Using `INFINITY` for the `top` value in the `UIEdgeInsets` property of the `ASInsetLayoutSpec` allowed the text inset to start at the bottom. This was possible because it would adopt the size of the static layout spec's `_photoNode`. + +
+ + Swift + Objective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  _photoNode.preferredFrameSize = CGSizeMake(USER_IMAGE_HEIGHT*2, USER_IMAGE_HEIGHT*2);
+  ASStaticLayoutSpec *backgroundImageStaticSpec = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[_photoNode]];
+
+  UIEdgeInsets insets = UIEdgeInsetsMake(INFINITY, 12, 12, 12);
+  ASInsetLayoutSpec *textInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:_titleNode];
+
+  ASOverlayLayoutSpec *textOverlaySpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:backgroundImageStaticSpec
+                                                                                 overlay:textInsetSpec];
+  
+  return textOverlaySpec;
+}
+  
+ +
+
+ +
+With the new `ASAbsoluteLayoutSpec` and same code above, the layout would now look like the picture below. The text is still there, but at ~900 pts (offscreen). + + + +## Rename `ASLayoutable` to `ASLayoutElement` + +Remember that an `ASLayoutSpec` contains children that conform to the `ASLayoutElement` protocol. Both `ASDisplayNodes` and `ASLayoutSpecs` conform to this protocol. + +The protocol has remained the same as **1.x**, but the name has been changed to be more descriptive. + +## Set `ASLayoutElement` properties via `ASLayoutElementStyle` + +An `ASLayoutElement`'s properties are are now set via it's `ASLayoutElementStyle` object. + +
+SwiftObjective-C + +
+
+id<ASLayoutElement> *layoutElement = ...;
+
+// 1.x:
+layoutElement.spacingBefore = 1.0;
+
+// 2.0:
+layoutElement.style.spacingBefore = 1.0;
+
+ +
+
+ +However, the properties specific to an `ASLayoutSpec` are still set directly on the layout spec. + +
+SwiftObjective-C + +
+
+// 1.x and 2.0
+ASStackLayoutSpec *stackLayoutSpec = ...;
+stackLayoutSpec.direction = ASStackLayoutDirectionVertical;
+stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentStart;
+
+ +
+
+ +## Setting the size of an `ASLayoutElement` + +With **2.0** we introduce a new, easier, way to set the size of an `ASLayoutElement`. These methods replace the deprecated `-preferredFrameSize` and `-sizeRange` **1.x** methods. + +The following **optional** properties are provided via the layout element's `style` property: + +- `-[ASLayoutElementStyle width]`: specifies the width of an ASLayoutElement. The `minWidth` and `maxWidth` properties will override `width`. The height will be set to Auto unless provided. + +- `-[ASLayoutElementStyle minWidth]`: specifies the minimum width of an ASLayoutElement. This prevents the used value of the `width` property from becoming smaller than the specified for `minWidth`. + +- `-[ASLayoutElementStyle maxWidth]`: specifies the maximum width of an ASLayoutElement. It prevents the used value of the `width` property from becoming larger than the specified for `maxWidth`. + +- `-[ASLayoutElementStyle height]`: specifies the height of an ASLayoutElement. The `minHeight` and `maxHeight` properties will override `height`. The width will be set to Auto unless provided. + +- `-[ASLayoutElementStyle minHeight]`: specifies the minimum height of an ASLayoutElement. It prevents the used value of the `height` property from becoming smaller than the specified for `minHeight`. + +- `-[ASLayoutElementStyle maxHeight]`: specifies the maximum height of an ASLayoutElement. It prevents the used value of the `height` property from becoming larger than the specified for `maxHeight`. + +To set both the width and height with a `CGSize` value: + +- `-[ASLayoutElementStyle preferredSize]`: Provides a suggested size for a layout element. If the optional minSize or maxSize are provided, and the preferredSize exceeds these, the minSize or maxSize will be enforced. If this optional value is not provided, the layout element’s size will default to it’s intrinsic content size provided calculateSizeThatFits: + +- `-[ASLayoutElementStyle minSize]`: An optional property that provides a minimum size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s minimum size is smaller than its child’s minimum size, the child’s minimum size will be enforced and its size will extend out of the layout spec’s. + +- `-[ASLayoutElementStyle maxSize]`: An optional property that provides a maximum size bound for a layout element. If provided, this restriction will always be enforced. If a child layout element’s maximum size is smaller than its parent, the child’s maximum size will be enforced and its size will extend out of the layout spec’s. + +To set both the width and height with a relative (%) value (an `ASRelativeSize`): + +- `-[ASLayoutElementStyle preferredRelativeSize]`: Provides a suggested RELATIVE size for a layout element. An ASRelativeSize uses percentages rather than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minRelativeSize or maxRelativeSize are provided, and the preferredRelativeSize exceeds these, the minRelativeSize or maxRelativeSize will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size provided calculateSizeThatFits: + +- `-[ASLayoutElementStyle minRelativeSize]`: An optional property that provides a minimum RELATIVE size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s minimum relative size is smaller than its child’s minimum relative size, the child’s minimum relative size will be enforced and its size will extend out of the layout spec’s. + +- `-[ASLayoutElementStyle maxRelativeSize]`: An optional property that provides a maximum RELATIVE size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s maximum relative size is smaller than its child’s maximum relative size, the child’s maximum relative size will be enforced and its size will extend out of the layout spec’s. + +For example, if you want to set a `width` of an `ASDisplayNode`: + +
+SwiftObjective-C + +
+
+// 1.x:
+// no good way to set an intrinsic size
+
+// 2.0:
+ASDisplayNode *ASDisplayNode = ...;
+
+// width 100 points, height: auto
+displayNode.style.width = ASDimensionMakeWithPoints(100);
+
+// width 50%, height: auto
+displayNode.style.width = ASDimensionMakeWithFraction(0.5);
+
+ASLayoutSpec *layoutSpec = ...;
+
+// width 100 points, height 100 points
+layoutSpec.style.preferredSize = CGSizeMake(100, 100);
+
+ +
+
+ +If you previously wrapped an `ASLayoutElement` with an `ASStaticLayoutSpec` just to give it a specific size (without setting the `layoutPosition` property on the element too), you don't have to do that anymore. + +
+SwiftObjective-C + +
+
+ASStackLayoutSpec *stackLayoutSpec = ...;
+id<ASLayoutElement> *layoutElement = ...;
+
+// 1.x:
+layoutElement.sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(50, 50));
+ASStaticLayoutSpec *staticLayoutSpec = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[layoutElement]];
+stackLayoutSpec.children = @[staticLayoutSpec];
+
+// 2.0:
+layoutElement.style.preferredSizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(50, 50));
+stackLayoutSpec.children = @[layoutElement];
+
+ +
+
+ +If you previously wrapped a `ASLayoutElement` within a `ASStaticLayoutSpec` just to return any layout spec from within `layoutSpecThatFits:` there is a new layout spec now that is called `ASWrapperLayoutSpec`. `ASWrapperLayoutSpec` is an `ASLayoutSpec` subclass that can wrap a `ASLayoutElement` and calculates the layout of the child based on the size given to the `ASLayoutElement`: + +
+SwiftObjective-C + +
+
+// 1.x - ASStaticLayoutSpec used as a "wrapper" to return subnode from layoutSpecThatFits: 
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[subnode]];
+}
+
+// 2.0
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  return [ASWrapperLayoutSpec wrapperWithLayoutElement:subnode];
+}
+
+// 1.x - ASStaticLayoutSpec used to set size (but not position) of subnode
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  ASDisplayNode *subnode = ...;
+  subnode.preferredSize = ...;
+  return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[subnode]];
+}
+
+// 2.0
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  ASDisplayNode *subnode = ...;
+  subnode.style.preferredSize = CGSizeMake(constrainedSize.max.width, constrainedSize.max.height / 2.0);
+  return [ASWrapperLayoutSpec wrapperWithLayoutElement:subnode];
+}
+
+ +
+
+ +## Deprecation of `-[ASDisplayNode preferredFrameSize]` + +With the introduction of new sizing properties there is no need anymore for the `-[ASDisplayNode preferredFrameSize]` property. Therefore it is deprecated in **2.0**. Instead, use the size values on the `style` object of an `ASDisplayNode`: + +
+SwiftObjective-C + +
+
 
+ASDisplayNode *ASDisplayNode = ...;
+
+// 1.x:
+displayNode.preferredFrameSize = CGSize(100, 100);
+
+// 2.0
+displayNode.style.preferredSize = CGSize(100, 100);
+
+ +
+
+ +`-[ASDisplayNode preferredFrameSize]` was not supported properly and was often more confusing than helpful. The new sizing methods should be easier and more clear to implment. + +## Deprecation of `-[ASLayoutElement measureWithSizeRange:]` + +`-[ASLayoutElement measureWithSizeRange:]` is deprecated in **2.0**. + +#### Calling `measureWithSizeRange:` + +If you previously called `-[ASLayoutElement measureWithSizeRange:]` to receive an `ASLayout`, call `-[ASLayoutElement layoutThatFits:]` now instead. + +
+SwiftObjective-C + +
+
+// 1.x:
+ASLayout *layout = [layoutElement measureWithSizeRange:someSizeRange];
+
+// 2.0:
+ASLayout *layout = [layoutElement layoutThatFits:someSizeRange];
+
+ +
+
+ +#### Implementing `measureWithSizeRange:` + +If you are implementing a custom `class` that conforms to `ASLayoutElement` (e.g. creating a custom `ASLayoutSpec`) , replace `-measureWithSizeRange:` with `-calculateLayoutThatFits:` + +
+SwiftObjective-C + +
+
+// 1.x:
+- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize {}
+
+// 2.0:
+- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize {}
+
+ +
+
+ +`-calculateLayoutThatFits:` takes an `ASSizeRange` that specifies a min size and a max size of type `CGSize`. Choose any size in the given range, to calculate the children's size and position and return a `ASLayout` structure with the layout of child components. + +Besides `-calculateLayoutThatFits:` there are two additional methods on `ASLayoutElement` that you should know about if you are implementing classes that conform to `ASLayoutElement`: + +
+SwiftObjective-C + +
+
+- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
+                     restrictedToSize:(ASLayoutElementSize)size
+                 relativeToParentSize:(CGSize)parentSize;
+
+ +
+
+ +In certain advanced cases, you may want to override this method. Overriding this method allows you to receive the `layoutElement`'s size, parent size, and constrained size. With these values you could calculate the final constrained size and call `-calculateLayoutThatFits:` with the result. + +
+SwiftObjective-C + +
+
+- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize
+                  parentSize:(CGSize)parentSize;
+
+ +
+
+ +Call this on children`layoutElements` to compute their layouts within your implementation of `-calculateLayoutThatFits:`. + +For sample implementations of layout specs and the usage of the `calculateLayoutThatFits:` family of methods, check out the layout specs in Texture itself! + +## Deprecation of `-[ASDisplayNode measure:]` + +Use `-[ASDisplayNode layoutThatFits:]` instead to get an `ASLayout` and call `size` on the returned `ASLayout`: + +
+SwiftObjective-C + +
+
+// 1.x:
+CGSize size = [displayNode measure:CGSizeMake(100, 100)];
+
+// 2.0:
+// Creates an ASSizeRange with min and max sizes.
+ASLayout *layout = [displayNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))];
+// Or an exact size
+// ASLayout *layout = [displayNode layoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))];
+CGSize size = layout.size;
+
+ +
+
+ +## Remove of `-[ASAbsoluteLayoutElement sizeRange]` + +The `sizeRange` property was removed from the `ASAbsoluteLayoutElement` protocol. Instead set the one of the following: + +- `-[ASLayoutElement width]` +- `-[ASLayoutElement height]` +- `-[ASLayoutElement minWidth]` +- `-[ASLayoutElement minHeight]` +- `-[ASLayoutElement maxWidth]` +- `-[ASLayoutElement maxHeight]` + +
+SwiftObjective-C + +
+
+id<ASLayoutElement> layoutElement = ...;
+
+// 1.x:
+layoutElement.sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(50, 50));
+
+// 2.0:
+layoutElement.style.preferredSizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(50, 50));
+
+ +
+
+ +Due to the removal of `-[ASAbsoluteLayoutElement sizeRange]`, we also removed the `ASRelativeSizeRange`, as the type was no longer needed. + +## Rename `ASRelativeDimension` to `ASDimension` + +To simplify the naming and support the fact that dimensions are widely used in Texture now, `ASRelativeDimension` was renamed to `ASDimension`. Having a shorter name and handy functions to create it was an important goal for us. + +`ASRelativeDimensionTypePercent` and associated functions were renamed to use `Fraction` to be consistent with Apple terminology. + +
+SwiftObjective-C + +
+
+// 2.0:
+// Handy functions to create ASDimensions
+ASDimension dimensionInPoints;
+dimensionInPoints = ASDimensionMake(ASDimensionTypePoints, 5.0)
+dimensionInPoints = ASDimensionMake(5.0)
+dimensionInPoints = ASDimensionMakeWithPoints(5.0)
+dimensionInPoints = ASDimensionMake("5.0pt");
+
+ASDimension dimensionInFractions;
+dimensionInFractions = ASDimensionMake(ASDimensionTypeFraction, 0.5)
+dimensionInFractions = ASDimensionMakeWithFraction(0.5)
+dimensionInFractions = ASDimensionMake("50%");
+
+ +
+
+ +## Introduction of `ASDimensionUnitAuto` + +Previously `ASDimensionUnitPoints` and `ASDimensionUnitFraction` were the only two `ASDimensionUnit` enum values available. A new dimension type called `ASDimensionUnitAuto` now exists. All of the ``ASLayoutElementStyle` sizing properties are set to `ASDimensionAuto` by default. + +`ASDimensionUnitAuto` means more or less: *"I have no opinion" and may be resolved in whatever way makes most sense given the circumstances.* + +Most of the time this is the intrinsic content size of the `ASLayoutElement`. + +For example, if an `ASImageNode` has a `width` set to `ASDimensionUnitAuto`, the width of the linked image file will be used. For an `ASTextNode` the intrinsic content size will be calculated based on the text content. If an `ASLayoutElement` cannot provide any intrinsic content size like `ASVideoNode` for example the size needs to set explicitly. + +
+SwiftObjective-C + +
+
+// 2.0:
+// No specific size needs to be set as the imageNode's size 
+// will be calculated from the content (the image in this case)
+ASImageNode *imageNode = [ASImageNode new];
+imageNode.image = ...;
+
+// Specific size must be set for ASLayoutElement objects that
+// do not have an intrinsic content size (ASVideoNode does not
+// have a size until it's video downloads)
+ASVideoNode *videoNode = [ASVideoNode new];
+videoNode.style.preferredSize = CGSizeMake(200, 100);
+
+ +
+
+ diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-layout-element-properties.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-layout-element-properties.md new file mode 100755 index 0000000000..18735a8cdf --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-layout-element-properties.md @@ -0,0 +1,146 @@ +--- +title: Layout Element Properties +layout: docs +permalink: /docs/layout2-layout-element-properties.html +prevPage: layout2-layoutspec-types.html +nextPage: layout2-api-sizing.html +--- + +- ASStackLayoutElement Properties - will only take effect on a node or layout spec that is the child of a stack spec +- ASAbsoluteLayoutElement Properties - will only take effect on a node or layout spec that is the child of a absolute spec +- ASLayoutElement Properties - applies to all nodes & layout specs + +## ASStackLayoutElement Properties + +
+Please note that the following properties will only take effect if set on the child of an STACK layout spec. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
CGFloat .style.spacingBeforeAdditional space to place before this object in the stacking direction.
CGFloat .style.spacingAfterAdditional space to place after this object in the stacking direction.
CGFloat .style.flexGrowIf the sum of childrens' stack dimensions is less than the minimum size, should this object grow?
CGFloat .style.flexShrinkIf the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink?
ASDimension .style.flexBasisSpecifies the initial size for this object, in the stack dimension (horizontal or vertical), before the flexGrow / flexShrink properties are applied and the remaining space is distributed.
ASStackLayoutAlignSelf .style.alignSelfOrientation of the object along cross axis, overriding alignItems. Options include: +
    +
  • ASStackLayoutAlignSelfAuto
  • +
  • ASStackLayoutAlignSelfStart
  • +
  • ASStackLayoutAlignSelfEnd
  • +
  • ASStackLayoutAlignSelfCenter
  • +
  • ASStackLayoutAlignSelfStretch
  • +
CGFloat .style.ascenderUsed for baseline alignment. The distance from the top of the object to its baseline.
CGFloat .style.descenderUsed for baseline alignment. The distance from the baseline of the object to its bottom.
+ + +## ASAbsoluteLayoutElement Properties + +
+Please note that the following properties will only take effect if set on the child of an ABSOLUTE layout spec. +
+ + + + + + + + + + +
PropertyDescription
CGPoint .style.layoutPositionThe CGPoint position of this object within its ASAbsoluteLayoutSpec parent spec.
+ +## ASLayoutElement Properties + +
+Please note that the following properties apply to ALL layout elements. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
ASDimension .style.widthThe width property specifies the width of the content area of an ASLayoutElement. The minWidth and maxWidth properties override width. Defaults to ASDimensionAuto.
ASDimension .style.heightThe height property specifies the height of the content area of an ASLayoutElement. The minHeight and maxHeight properties override height. Defaults to ASDimensionAuto.
ASDimension .style.minWidthThe minWidth property is used to set the minimum width of a given element. It prevents the used value of the width property from becoming smaller than the value specified for minWidth. The value of minWidth overrides both maxWidth and width. Defaults to ASDimensionAuto.
ASDimension .style.maxWidthThe maxWidth property is used to set the maximum width of a given element. It prevents the used value of the width property from becoming larger than the value specified for maxWidth. The value of maxWidth overrides width, but minWidth overrides maxWidth. Defaults to ASDimensionAuto.
ASDimension .style.minHeightThe minHeight property is used to set the minimum height of a given element. It prevents the used value of the height property from becoming smaller than the value specified for minHeight. The value of minHeight overrides both maxHeight and height. Defaults to ASDimensionAuto.
ASDimension .style.maxHeightThe maxHeight property is used to set the maximum height of a given element. It prevents the used value of the height property from becoming larger than the value specified for maxHeight. The value of maxHeight overrides height, but minHeight overrides maxHeight. Defaults to ASDimensionAuto
CGSize .style.preferredSize

Provides a suggested size for a layout element. If the optional minSize or maxSize are provided, and the preferredSize exceeds these, the minSize or maxSize will be enforced. If this optional value is not provided, the layout element’s size will default to it’s intrinsic content size provided calculateSizeThatFits:

+

This method is optional, but one of either preferredSize or preferredLayoutSize is required for nodes that either have no intrinsic content size or should be laid out at a different size than its intrinsic content size. For example, this property could be set on an ASImageNode to display at a size different from the underlying image size.

+

Warning: calling the getter when the size's width or height are relative will cause an assert.

CGSize .style.minSize

An optional property that provides a minimum size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s minimum size is smaller than its child’s minimum size, the child’s minimum size will be enforced and its size will extend out of the layout spec’s.

+

For example, if you set a preferred relative width of 50% and a minimum width of 200 points on an element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, since 160 pts is lower than the minimum width of 200 pts, the minimum width would be used.

CGSize .style.maxSize

An optional property that provides a maximum size bound for a layout element. If provided, this restriction will always be enforced. If a child layout element’s maximum size is smaller than its parent, the child’s maximum size will be enforced and its size will extend out of the layout spec’s.

+

For example, if you set a preferred relative width of 50% and a maximum width of 120 points on an element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, since 160 pts is higher than the maximum width of 120 pts, the maximum width would be used.

ASLayoutSize .style.preferredLayoutSizeProvides a suggested RELATIVE size for a layout element. An ASLayoutSize uses percentages rather than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minLayoutSize or maxLayoutSize are provided, and the preferredLayoutSize exceeds these, the minLayoutSize or maxLayoutSize will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size provided calculateSizeThatFits:
ASLayoutSize .style.minLayoutSizeAn optional property that provides a minimum RELATIVE size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s minimum relative size is smaller than its child’s minimum relative size, the child’s minimum relative size will be enforced and its size will extend out of the layout spec’s.
ASLayoutSize .style.maxLayoutSizeAn optional property that provides a maximum RELATIVE size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s maximum relative size is smaller than its child’s maximum relative size, the child’s maximum relative size will be enforced and its size will extend out of the layout spec’s.
diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutSpecThatFits.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutSpecThatFits.md new file mode 100755 index 0000000000..cd3e3097cc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutSpecThatFits.md @@ -0,0 +1,120 @@ +--- +title: Composing Layout Specs +layout: docs +permalink: /docs/layout2-layoutSpecThatFits.html +--- + +The composing of layout specs and layoutables are happening within the `layoutSpecThatFits:` method. This is where you will put the majority of your layout code. It defines the layout and does the heavy calculation on a background thread. + +Every `ASDisplayNode` that would like to layout it's subnodes should should do this by implementing the `layoutSpecThatFits:` method. This method is where you build out a layout spec object that will produce the size of the node, as well as the size and position of all subnodes. + +The following `layoutSpecThatFits:` implementation is from the Kittens example and will implement an easy stack layout with an image with a constrained size on the left and a text to the right. The great thing is, by using a `ASStackLayoutSpec` the height is dynamically calculated based on the image height and the height of the text. + +
+ + Swift + Objective-C + + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  // Set an intrinsic size for the image node
+  CGSize imageSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize)
+                                      : CGSizeMake(kImageSize, kImageSize);
+  [_imageNode setSizeFromCGSize:imageSize];
+
+  // Shrink the text node in case the image + text gonna be too wide
+  _textNode.flexShrink = YES;
+
+  // Configure stack
+  ASStackLayoutSpec *stackLayoutSpec =
+  [ASStackLayoutSpec
+   stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
+   spacing:kInnerPadding
+   justifyContent:ASStackLayoutJustifyContentStart
+   alignItems:ASStackLayoutAlignItemsStart
+   children:_swappedTextAndImage ? @[_textNode, _imageNode] : @[_imageNode, _textNode]];
+
+  // Add inset
+  return [ASInsetLayoutSpec
+          insetLayoutSpecWithInsets:UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding)
+          child:stackLayoutSpec];
+}
+  
+ + +
+
+ + +The result looks like the following: +![Kittens Node](https://d3vv6lp55qjaqc.cloudfront.net/items/2l133Y2B3r1F231a310q/Screen%20Shot%202016-08-23%20at%202.29.12%20PM.png) + +Let's look at some more advanced composition of layout spec and layoutable implementation from the `ASDKGram` example that should give you a feel how layout specs and layoutables can be combined to compose a difficult layout. You can also find this code in the `examples/ASDKGram` folder. + +
+ + Swift + Objective-C + + +
+
+  
+ + +
+
+ +After the layout pass happened the result will look like the following: +![ASDKGram](https://d3vv6lp55qjaqc.cloudfront.net/items/1l0t352p441K3k0C3y1l/layout-example-2.png) + +The layout spec object that you create in `layoutSpecThatFits:` is mutable up until the point that it is return in this method. After this point, it will be immutable. It's important to remember not to cache layout specs for use later but instead to recreate them when necessary. + +Note: Because it is run on a background thread, you should not set any node.view or node.layer properties here. Also, unless you know what you are doing, do not create any nodes in this method. Additionally, it is not necessary to begin this method with a call to super, unlike other method overrides. \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutspec-types-examples.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutspec-types-examples.md new file mode 100755 index 0000000000..6b851879ff --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutspec-types-examples.md @@ -0,0 +1,26 @@ +--- +title: Layout Spec Composition Examples +layout: docs +permalink: /docs/layout2-layoutspec-types-examples.html +--- + +## Text Overlaid on an Image + + +
+SwiftObjective-C + +
+
+- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
+{
+  ...
+  UIEdgeInsets *insets = UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER);
+  ASInsetLayoutSpec *headerWithInset = [ASInsetLayoutSpec alloc] initWithInsets:insets child:textNode];
+  ...
+}
+
+ +
+
\ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutspec-types.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutspec-types.md new file mode 100755 index 0000000000..a5ab39b14f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-layoutspec-types.md @@ -0,0 +1,517 @@ +--- +title: Layout Specs +layout: docs +permalink: /docs/layout2-layoutspec-types.html +prevPage: automatic-layout-examples-2.html +nextPage: layout2-layout-element-properties.html +--- + +The following `ASLayoutSpec` subclasses can be used to compose simple or very complex layouts. + + +
  • ASCornerLayoutSpec
  • + +You may also subclass `ASLayoutSpec` in order to make your own, custom layout specs. + +## ASWrapperLayoutSpec + +`ASWrapperLayoutSpec` is a simple `ASLayoutSpec` subclass that can wrap a `ASLayoutElement` and calculate the layout of the child based on the size set on the layout element. + +`ASWrapperLayoutSpec` is ideal for easily returning a single subnode from `-layoutSpecThatFits:`. Optionally, this subnode can have sizing information set on it. However, if you need to set a position in addition to a size, use `ASAbsoluteLayoutSpec` instead. + +
    + + Swift + Objective-C + + +
    +
    +// return a single subnode from layoutSpecThatFits: 
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  return [ASWrapperLayoutSpec wrapperWithLayoutElement:_subnode];
    +}
    +
    +// set a size (but not position)
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  _subnode.style.preferredSize = CGSizeMake(constrainedSize.max.width,
    +                                            constrainedSize.max.height / 2.0);
    +  return [ASWrapperLayoutSpec wrapperWithLayoutElement:subnode];
    +}
    +
    + + +
    +
    + +## ASStackLayoutSpec (Flexbox Container) +Of all the layoutSpecs in Texture, `ASStackLayoutSpec` is the most useful and powerful. `ASStackLayoutSpec` uses the flexbox algorithm to determine the position and size of its children. Flexbox is designed to provide a consistent layout on different screen sizes. In a stack layout you align items in either a vertical or horizontal stack. A stack layout can be a child of another stack layout, which makes it possible to create almost any layout using a stack layout spec. + +`ASStackLayoutSpec` has 7 properties in addition to its `` properties: + +- `direction`. Specifies the direction children are stacked in. If horizontalAlignment and verticalAlignment were set, +they will be resolved again, causing justifyContent and alignItems to be updated accordingly. +- `spacing`. The amount of space between each child. +- `horizontalAlignment`. Specifies how children are aligned horizontally. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties. +- `verticalAlignment`. Specifies how children are aligned vertically. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties. +- `justifyContent`. It defines the alignment along the main axis. +- `alignItems`. Orientation of children along cross axis. +- `flexWrap`. Whether children are stacked into a single or multiple lines. Defaults to single line. +- `alignContent`. Orientation of lines along cross axis if there are multiple lines. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ASStackLayoutSpec *mainStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
    +                       spacing:6.0
    +                justifyContent:ASStackLayoutJustifyContentStart
    +                    alignItems:ASStackLayoutAlignItemsCenter
    +                      children:@[_iconNode, _countNode]];
    +
    +  // Set some constrained size to the stack
    +  mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0);
    +  mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0);
    +
    +  return mainStack;
    +}
    +
    + + +
    +
    + +Flexbox works the same way in Texture as it does in CSS on the web, with a few exceptions. For example, the defaults are different and there is no `flex` parameter. See Web Flexbox Differences for more information. + +
    + +## ASInsetLayoutSpec +During the layout pass, the `ASInsetLayoutSpec` passes its `constrainedSize.max` `CGSize` to its child, after subtracting its insets. Once the child determines it's final size, the inset spec passes its final size up as the size of its child plus its inset margin. Since the inset layout spec is sized based on the size of it's child, the child **must** have an instrinsic size or explicitly set its size. + + + +If you set `INFINITY` as a value in the `UIEdgeInsets`, the inset spec will just use the intrinisic size of the child. See an example of this. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ...
    +  UIEdgeInsets *insets = UIEdgeInsetsMake(10, 10, 10, 10);
    +  ASInsetLayoutSpec *headerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:textNode];
    +  ...
    +}
    +
    + + +
    +
    + +## ASOverlayLayoutSpec +`ASOverlayLayoutSpec` lays out its child (blue), stretching another component on top of it as an overlay (red). + + + +The overlay spec's size is calculated from the child's size. In the diagram below, the child is the blue layer. The child's size is then passed as the `constrainedSize` to the overlay layout element (red layer). Thus, it is important that the child (blue layer) **must** have an intrinsic size or a size set on it. + +
    +When using Automatic Subnode Management with the ASOverlayLayoutSpec, the nodes may sometimes appear in the wrong order. This is a known issue that will be fixed soon. The current workaround is to add the nodes manually, with the overlay layout element (red) must added as a subnode to the parent node after the child layout element (blue). +
    + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor blueColor]);
    +  ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]);
    +  return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:backgroundNode overlay:foregroundNode];
    +}
    +
    + + +
    +
    + +## ASBackgroundLayoutSpec +`ASBackgroundLayoutSpec` lays out a component (blue), stretching another component behind it as a backdrop (red). + + + +The background spec's size is calculated from the child's size. In the diagram below, the child is the blue layer. The child's size is then passed as the `constrainedSize` to the background layout element (red layer). Thus, it is important that the child (blue layer) **must** have an intrinsic size or a size set on it. + +
    +When using Automatic Subnode Management with the ASOverlayLayoutSpec, the nodes may sometimes appear in the wrong order. This is a known issue that will be fixed soon. The current workaround is to add the nodes manually, with the child layout element (blue) must added as a subnode to the parent node after the child background element (red). +
    + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]);
    +  ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor blueColor]);
    +
    +  return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:foregroundNode background:backgroundNode];
    +}
    +
    + + +
    +
    + +Note: The order in which subnodes are added matters for this layout spec; the background object must be added as a subnode to the parent node before the foreground object. Using ASM does not currently guarantee this order! + +## ASCenterLayoutSpec +`ASCenterLayoutSpec` centers its child within its max `constrainedSize`. + + + +If the center spec's width or height is unconstrained, it shrinks to the size of the child. + +`ASCenterLayoutSpec` has two properties: + +- `centeringOptions`. Determines how the child is centered within the center spec. Options include: None, X, Y, XY. +- `sizingOptions`. Determines how much space the center spec will take up. Options include: Default, minimum X, minimum Y, minimum XY. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ASStaticSizeDisplayNode *subnode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], CGSizeMake(70, 100));
    +  return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY
    +                                                    sizingOptions:ASCenterLayoutSpecSizingOptionDefault
    +                                                            child:subnode]
    +}
    +
    + + +
    +
    + +## ASRatioLayoutSpec +`ASRatioLayoutSpec` lays out a component at a fixed aspect ratio which can scale. This spec **must** have a width or a height passed to it as a constrainedSize as it uses this value to scale itself. + + + +It is very common to use a ratio spec to provide an intrinsic size for `ASNetworkImageNode` or `ASVideoNode`, as both do not have an intrinsic size until the content returns from the server. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  // Half Ratio
    +  ASStaticSizeDisplayNode *subnode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], CGSizeMake(100, 100));
    +  return [ASRatioLayoutSpec ratioLayoutSpecWithRatio:0.5 child:subnode];
    +}
    +
    + + +
    +
    + +## ASRelativeLayoutSpec +Lays out a component and positions it within the layout bounds according to vertical and horizontal positional specifiers. Similar to the “9-part” image areas, a child can be positioned at any of the 4 corners, or the middle of any of the 4 edges, as well as the center. + +This is a very powerful class, but too complex to cover in this overview. For more information, look into `ASRelativeLayoutSpec`'s `-calculateLayoutThatFits:` method + properties. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ...
    +  ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]);
    +  ASStaticSizeDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor greenColor], CGSizeMake(70, 100));
    +
    +  ASRelativeLayoutSpec *relativeSpec = [ASRelativeLayoutSpec relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionStart
    +                                  verticalPosition:ASRelativeLayoutSpecPositionStart
    +                                      sizingOption:ASRelativeLayoutSpecSizingOptionDefault
    +                                             child:foregroundNode]
    +
    +  ASBackgroundLayoutSpec *backgroundSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:relativeSpec background:backgroundNode];
    +  ...
    +}
    +
    + + +
    +
    + +## ASAbsoluteLayoutSpec +Within `ASAbsoluteLayoutSpec` you can specify exact locations (x/y coordinates) of its children by setting their `layoutPosition` property. Absolute layouts are less flexible and harder to maintain than other types of layouts. + +`ASAbsoluteLayoutSpec` has one property: + +- `sizing`. Determines how much space the absolute spec will take up. Options include: Default, and Size to Fit. *Note* that the Size to Fit option will replicate the behavior of the old `ASStaticLayoutSpec`. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  CGSize maxConstrainedSize = constrainedSize.max;
    +
    +  // Layout all nodes absolute in a static layout spec
    +  guitarVideoNode.style.layoutPosition = CGPointMake(0, 0);
    +  guitarVideoNode.style.preferredSize = CGSizeMake(maxConstrainedSize.width, maxConstrainedSize.height / 3.0);
    +
    +  nicCageVideoNode.style.layoutPosition = CGPointMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0);
    +  nicCageVideoNode.style.preferredSize = CGSizeMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0);
    +
    +  simonVideoNode.style.layoutPosition = CGPointMake(0.0, maxConstrainedSize.height - (maxConstrainedSize.height / 3.0));
    +  simonVideoNode.style.preferredSize = CGSizeMake(maxConstrainedSize.width/2, maxConstrainedSize.height / 3.0);
    +
    +  hlsVideoNode.style.layoutPosition = CGPointMake(0.0, maxConstrainedSize.height / 3.0);
    +  hlsVideoNode.style.preferredSize = CGSizeMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0);
    +
    +  return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[guitarVideoNode, nicCageVideoNode, simonVideoNode, hlsVideoNode]];
    +}
    +
    + + +
    +
    + +## ASCornerLayoutSpec +`ASCornerLayoutSpec` is a new convenient layout spec for fast corner element layout. The easy way to position an element in corner is to use declarative code expression rather than manual coordinate calculation, and ASCornerLayoutSpec can achieve this goal. + + + +`ASCornerLayoutSpec` takes good care of its own size calculation. The best scenario to explain this would be the case that adding a small badge view at the corner of user's avatar image and there is no need to worry about the fact that little-exceeded badge frame (which out of avatar image frame) may affect the whole layout size. By default, the size of corner element will not be added to layout size, only if you manually turn on the `wrapsCorner` property. + +`ASCornerLayoutSpec` is introduced from version 2.7 and above. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ...
    +  // Layout the center of badge to the top right corner of avatar.
    +  ASCornerLayoutSpec *cornerSpec = [ASCornerLayoutSpec cornerLayoutSpecWithChild:self.avatarNode corner:self.badgeNode location:ASCornerLayoutLocationTopRight];
    +  // Slightly shift center of badge inside of avatar.
    +  cornerSpec.offset = CGPointMake(-3, 3);
    +  ...
    +}
    +
    + + +
    +
    + +## ASLayoutSpec +`ASLayoutSpec` is the main class from that all layout spec's are subclassed. It's main job is to handle all the children management, but it also can be used to create custom layout specs. Only the super advanced should want / need to create a custom subclasses of `ASLayoutSpec` though. Instead try to use provided layout specs and compose them together to create more advanced layouts. + +Another use of `ASLayoutSpec` is to be used as a spacer in a `ASStackLayoutSpec` with other children, when `.flexGrow` and/or `.flexShrink` is applied. + +
    + + Swift + Objective-C + + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    +{
    +  ...
    +  // ASLayoutSpec as spacer
    +  ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init];
    +  spacer.style.flexGrow = 1.0;
    +
    +  stack.children = @[imageNode, spacer, textNode];
    +  ...
    +}
    +
    + + +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-manual-layout.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-manual-layout.md new file mode 100755 index 0000000000..fef0ccf505 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-manual-layout.md @@ -0,0 +1,174 @@ +--- +title: Manual Layout +layout: docs +permalink: /docs/layout2-manual-layout.html +--- + +## Manual Layout +After diving in to the automatic way for layout in Texture there is still the _old_ way to layout manually available. For the sake of completness here is a short description how to accomplish that within Texture. + +### Manual Layout UIKit + +Sizing and layout of custom view hierarchies are typically done all at once on the main thread. For example, a custom UIView that minimally encloses a text view and an image view might look like this: + +
    + + Swift + Objective-C + + +
    +
    +- (CGSize)sizeThatFits:(CGSize)size
    +{
    +  // size the image
    +  CGSize imageSize = [_imageView sizeThatFits:size];
    +
    +  // size the text view
    +  CGSize maxTextSize = CGSizeMake(size.width - imageSize.width, size.height);
    +  CGSize textSize = [_textView sizeThatFits:maxTextSize];
    +
    +  // make sure everything fits
    +  CGFloat minHeight = MAX(imageSize.height, textSize.height);
    +  return CGSizeMake(size.width, minHeight);
    +}
    +
    +- (void)layoutSubviews
    +{
    +  CGSize size = self.bounds.size; // convenience
    +
    +  // size and layout the image
    +  CGSize imageSize = [_imageView sizeThatFits:size];
    +  _imageView.frame = CGRectMake(size.width - imageSize.width, 0.0f,
    +                                imageSize.width, imageSize.height);
    +
    +  // size and layout the text view
    +  CGSize maxTextSize = CGSizeMake(size.width - imageSize.width, size.height);
    +  CGSize textSize = [_textView sizeThatFits:maxTextSize];
    +  _textView.frame = (CGRect){ CGPointZero, textSize };
    +}
    +  
    + + +
    +
    + +This isn't ideal. We're sizing our subviews twice — once to figure out how big our view needs to be and once when laying it out — and while our layout arithmetic is cheap and quick, we're also blocking the main thread on expensive text sizing. + +We could improve the situation by manually cacheing our subviews' sizes, but that solution comes with its own set of problems. Just adding `_imageSize` and `_textSize` ivars wouldn't be enough: for example, if the text were to change, we'd need to recompute its size. The boilerplate would quickly become untenable. + +Further, even with a cache, we'll still be blocking the main thread on sizing *sometimes*. We could try to shift sizing to a background thread with `dispatch_async()`, but even if our own code is thread-safe, UIView methods are documented to [only work on the main thread](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/index.html): + +> Manipulations to your application’s user interface must occur on the main +> thread. Thus, you should always call the methods of the UIView class from +> code running in the main thread of your application. The only time this may +> not be strictly necessary is when creating the view object itself but all +> other manipulations should occur on the main thread. + +This is a pretty deep rabbit hole. We could attempt to work around the fact that UILabels and UITextViews cannot safely be sized on background threads by manually creating a TextKit stack and sizing the text ourselves... but that's a laborious duplication of work. Further, if UITextView's layout behaviour changes in an iOS update, our sizing code will break. (And did we mention that TextKit isn't thread-safe either?) + +### Manual Layout Texture + +Manual layout within Texture are realized within two methods: + +#### `calculateSizeThatFits` and `layout` + +Within `calculateSizeThatFits:` you should provide a intrinsic content size for the node based on the given `constrainedSize`. This method is called on a background thread so perform expensive sizing operations within it. + +
    + + Swift + Objective-C + + +
    +
    +- [ASDisplayNode calculateSizeThatFits:]
    +  
    + + +
    +
    + +After measurement and layout pass happens further layout can be done in `layout`. This method is called on the main thread. In there, layout operations can be done for nodes that are not playing within the automatic layout system and are referenced within `layoutSpecThatFits:`. + +
    + + Swift + Objective-C + + +
    +
    +- [ASDisplayNode layout]
    +  
    + + +
    +
    + +#### Example +Our custom node looks like this: + +
    + + Swift + Objective-C + + +
    +
    +#import 
    +
    +...
    +
    +// perform expensive sizing operations on a background thread
    +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
    +{
    +  // size the image
    +  CGSize imageSize = [_imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size;
    +
    +  // size the text node
    +  CGSize maxTextSize = CGSizeMake(constrainedSize.width - imageSize.width,
    +                                  constrainedSize.height);
    +
    +  CGSize textSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, maxTextSize)].size;
    +
    +  // make sure everything fits
    +  CGFloat minHeight = MAX(imageSize.height, textSize.height);
    +  return CGSizeMake(constrainedSize.width, minHeight);
    +}
    +
    +// do as little work as possible in main-thread layout
    +- (void)layout
    +{
    +  // layout the image using its cached size
    +  CGSize imageSize = _imageNode.calculatedSize;
    +  _imageNode.frame = CGRectMake(self.bounds.size.width - imageSize.width, 0.0f,
    +                                imageSize.width, imageSize.height);
    +
    +  // layout the text view using its cached size
    +  CGSize textSize = _textNode.calculatedSize;
    +  _textNode.frame = (CGRect){ CGPointZero, textSize };
    +}
    +  
    + + +
    +
    + +`ASImageNode` and `ASTextNode`, like the rest of Texture, are thread-safe, so we can size them on background threads. The `-layoutThatFits:` method is like `-sizeThatFits:`, but with side effects: it caches the (`calculatedSize`) for quick access later on — like in our now-snappy `-layout` implementation. + +As you can see, node hierarchies are sized and laid out in much the same way as their view counterparts. Manually layed out nodes do need to be written with a few things in mind: + +* Nodes must recursively measure all of their subnodes in their `-calculateSizeThatFits:` implementations. Note that the `-layoutThatFits:` machinery will only call `-calculateSizeThatFits:` if a new measurement pass is needed (e.g., if the constrained size has changed) and `layoutSpecThatFits:` is *not* implemented. + +* Nodes should perform any other expensive pre-layout calculations in `-calculateSizeThatFits:`, caching useful intermediate results in ivars as appropriate. + +* Nodes should call `[self invalidateCalculatedSize]` when necessary. For example, `ASTextNode` invalidates its calculated size when its `attributedString` property is changed. + +As already mentioned, automatic layout is preferred over manual layout and should be the way to go in most cases. \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-quickstart.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-quickstart.md new file mode 100755 index 0000000000..798e0a2ec4 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-quickstart.md @@ -0,0 +1,100 @@ +--- +title: Quickstart +layout: docs +permalink: /docs/layout2-quickstart.html +prevPage: multiplex-image-node.html +nextPage: automatic-layout-examples-2.html +--- + +## Motivation & Benefits + +The Layout API was created as a performant alternative to UIKit's Auto Layout, which becomes exponentially expensive for complicated view hierarchies. Texture's Layout API has many benefits over using UIKit's Auto Layout: + +- **Fast**: As fast as manual layout code and significantly faster than Auto Layout +- **Asynchronous & Concurrent:** Layouts can be computed on background threads so user interactions are not interrupted. +- **Declarative**: Layouts are declared with immutable data structures. This makes layout code easier to develop, document, code review, test, debug, profile, and maintain. +- **Cacheable**: Layout results are immutable data structures so they can be precomputed in the background and cached to increase user perceived performance. +- **Extensible**: Easy to share code between classes. + +## Inspired by CSS Flexbox + +Those who are familiar with Flexbox will notice many similarities in the two systems. However, Texture's Layout API does not re-implement all of CSS. + +## Basic Concepts + +Texture's layout system is centered around two basic concepts: + +1. Layout Specs +2. Layout Elements + + +### Layout Specs + +A layout spec, short for "layout specification", has no physical presence. Instead, layout specs act as containers for other layout elements by understanding how these children layout elements relate to each other. + +Texture provides several subclasses of `ASLayoutSpec`, from a simple layout specification that insets a single child, to a more complex layout specification that arranges multiple children in varying stack configurations. + +### Layout Elements + +Layout specs contain and arrange layout elements. + +All `ASDisplayNode`s and `ASLayoutSpec`s conform to the `` protocol. This means that you can compose layout specs from both nodes and other layout specs. Cool! + +The `ASLayoutElement` protocol has several properties that can be used to create very complex layouts. In addition, layout specs have their own set of properties that can be used to adjust the arrangment of the layout elements. + +### Combine Layout Specs & Layout Elements to Make Complex UI + +Here you can see how `ASTextNode`s (highlighted in yellow), an `ASVideoNode` (top image) and an `ASStackLayoutSpec` ("stack layout spec") can be combined to create a complex layout. + + + +The play button on top of the `ASVideoNode` (top image) is placed using an `ASCenterLayoutSpec` ("center layout spec") and an `ASOverlayLayoutSpec` ("overlay layout spec"). + + + +### Some nodes need Sizes Set + + + +Some elements have an "intrinsic size" based on their immediately available content. For example, ASTextNode can calculate its size based on its attributed string. Other nodes that have an intrinsic size include + +- `ASImageNode` +- `ASTextNode` +- `ASButtonNode` + +All other nodes either do not have an intrinsic size or lack an intrinsic size until their external resource is loaded. For example, an `ASNetworkImageNode` does not know its size until the image has been downloaded from the URL. These sorts of elements include + +- `ASVideoNode` +- `ASVideoPlayerNode` +- `ASNetworkImageNode` +- `ASEditableTextNode` + +These nodes that lack an initial intrinsic size must have an initial size set for them using an `ASRatioLayoutSpec`, an `ASAbsoluteLayoutSpec` or the size properties on the style object. + +### Layout Debugging + +Calling `-asciiArtString` on any `ASDisplayNode` or `ASLayoutSpec` returns an ascii-art representation of the object and its children. Optionally, if you set the `.debugName` on any node or layout spec, that will also be included in the ascii art. An example is seen below. + +
    +
    +
    +-----------------------ASStackLayoutSpec----------------------
    +|  -----ASStackLayoutSpec-----  -----ASStackLayoutSpec-----  |
    +|  |       ASImageNode       |  |       ASImageNode       |  |
    +|  |       ASImageNode       |  |       ASImageNode       |  |
    +|  ---------------------------  ---------------------------  |
    +--------------------------------------------------------------
    +
    +
    +
    + +You can also print out the style object on any `ASLayoutElement` (node or layout spec). This is especially useful when debugging the sizing properties. + +
    +
    +
    +(lldb) po _photoImageNode.style
    +Layout Size = min {414pt, 414pt} <= preferred {20%, 50%} <= max {414pt, 414pt}
    +
    +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/_docs/layout2-web-flexbox-differences.md b/submodules/AsyncDisplayKit/docs/_docs/layout2-web-flexbox-differences.md new file mode 100755 index 0000000000..73dac79fe7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/layout2-web-flexbox-differences.md @@ -0,0 +1,21 @@ +--- +title: Web Flexbox Differences +layout: docs +permalink: /docs/layout2-web-flexbox-differences.html +--- + +The goal of Texture's Layout API is *not* to re-implement all of CSS. It only targets a subset of CSS and Flexbox container, and there are no plans to implement support for tables, floats, or any other CSS concepts. The Texture Layout API also does not plan to support styling properties which do not affect layout such as color or background properties. + +The layout system tries to stay as close as possible to CSS. There are, however, certain cases where it differs from the web, these include: + +### Naming properties + +Certain properties have a different naming as on the web. For example `min-height` equivalent is the `minHeight` property. The full list of properties that control layout is documented in the Layout Properties section. + +### No margin / padding properties + +Layoutables don't have a padding or margin property. Instead wrapping a layoutable within an `ASInsetLayoutSpec` to apply padding or margin to the layoutable is the recommended way. See `ASInsetLayout` section for more information. + +### Missing features + +Certain features are not supported currently. See Layout Properties for the full list of properties that are supported. diff --git a/submodules/AsyncDisplayKit/docs/_docs/map-node.md b/submodules/AsyncDisplayKit/docs/_docs/map-node.md new file mode 100755 index 0000000000..6245306193 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/map-node.md @@ -0,0 +1,146 @@ +--- +title: ASMapNode +layout: docs +permalink: /docs/map-node.html +prevPage: video-node.html +nextPage: control-node.html +--- + +`ASMapNode` allows you to easily specify a geographic region to show to your users. + +### Basic Usage + +Let's say you'd like to show a snapshot of San Francisco. All you need are the coordinates. + +
    +SwiftObjective-C + +
    +
    +ASMapNode *mapNode = [[ASMapNode alloc] init];
    +mapNode.style.preferredSize = CGSizeMake(300.0, 300.0);
    +
    +// San Francisco
    +CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(37.7749, -122.4194);
    +
    +// show 20,000 square meters
    +mapNode.region = MKCoordinateRegionMakeWithDistance(coord, 20000, 20000);
    +
    + + +
    +
    + + + +The region value is actually just one piece of a property called `options` of type `MKMapSnapshotOptions`. + + +### MKMapSnapshotOptions + +A map node's main components can be defined directly through its `options` property. The snapshot options object contains the following: + +
      +
    • An MKMapCamera: used to configure altitude and pitch of the camera
    • +
    • An MKMapRect: basically a CGRect
    • +
    • An MKMapRegion: Controls the coordinate of focus, and the size around that focus to show
    • +
    • An MKMapType: Can be set to Standard, Satellite, etc.
    • +
    + +To do something like changing your map to a satellite map, you just need to create an options object and set its properties accordingly. + +
    +SwiftObjective-C + +
    +
    +MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
    +options.mapType = MKMapTypeSatellite;
    +options.region = MKCoordinateRegionMakeWithDistance(coord, 20000, 20000);
    +
    +mapNode.options = options;
    +
    + +
    +
    + +Results in: + + + +One thing to note is that setting the options value will overwrite a previously set region. + +### Annotations + +To set annotations, all you need to do is assign an array of annotations to your `ASMapNode`. + +Say you want to show a pin directly in the middle of your map of San Francisco. + +
    +SwiftObjective-C + +
    +
    +MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
    +annotation.coordinate = CLLocationCoordinate2DMake(37.7749, -122.4194);
    +
    +mapNode.annotations = @[annotation];
    +
    + +
    +
    + + + +No problem. + +### Live Map Mode + +Chaning your map node from a static view of some region, into a fully interactable cartographic playground is as easy as: + +
    +SwiftObjective-C + +
    +
    +mapNode.liveMap = YES;
    +
    + +
    +
    + +This enables "live map mode" in which the node will use an MKMapView to render an interactive version of your map. + + + +As with UIKit views, the `MKMapView` used in live map mode is not thread-safe. + +### MKMapView Delegate + +If live map mode has been enabled and you need to react to any events associated with the map node, you can set the `mapDelegate` property. This delegate should conform to the MKMapViewDelegate protocol. + + + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/multiplex-image-node.md b/submodules/AsyncDisplayKit/docs/_docs/multiplex-image-node.md new file mode 100755 index 0000000000..d219becb80 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/multiplex-image-node.md @@ -0,0 +1,109 @@ +--- +title: ASMultiplexImageNode +layout: docs +permalink: /docs/multiplex-image-node.html +prevPage: editable-text-node.html +--- + +Let's say your API is out of your control and the images in your app can't be progressive jpegs but you can retrieve a few different sizes of the image asset you want to display. This is where you would use an `ASMultiplexImageNode` instead of an ASNetworkImageNode. + +In the following example, you're using a multiplex image node in an `ASCellNode` subclass. After initialization, you typically need to do two things. First, make sure to set `downloadsIntermediateImages` to `YES` so that the lesser quality images will be downloaded. + +Then, assign an array of keys to the property `imageIdentifiers`. This list should be in descending order of image quality and will be used by the node to determine what URL to call for each image it will try to load. + +
    +SwiftObjective-C + +
    +
    +- (instancetype)initWithURLs:(NSDictionary *)urls
    +{
    +    ...
    +     _imageURLs = urls;          // something like @{@"thumb": "/smallImageUrl", @"medium": ...}
    +
    +    _multiplexImageNode = [[ASMultiplexImageNode alloc] initWithCache:nil 
    +                                                           downloader:[ASBasicImageDownloader sharedImageDownloader]];
    +    _multiplexImageNode.downloadsIntermediateImages = YES;
    +    _multiplexImageNode.imageIdentifiers = @[ @"original", @"medium", @"thumb" ];
    +
    +    _multiplexImageNode.dataSource = self;
    +    _multiplexImageNode.delegate   = self;
    +    ...
    +}
    +    
    + + +
    +
    + + +Then, if you've set up a simple dictionary that holds the keys you provided earlier pointing to URLs of the various versions of your image, you can simply return the URL for the given key in: + +
    +SwiftObjective-C + +
    +
    +#pragma mark Multiplex Image Node Datasource
    +
    +- (NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode 
    +        URLForImageIdentifier:(id)imageIdentifier
    +{
    +    return _imageURLs[imageIdentifier];
    +}
    +
    + + +
    +
    + +There are also delegate methods provided to update you on things such as the progress of an image's download, when it has finished displaying etc. They're all optional so feel free to use them as necessary. + +For example, in the case that you want to react to the fact that a new image arrived, you can use the following delegate callback. + +
    +SwiftObjective-C + +
    +
    +#pragma mark Multiplex Image Node Delegate
    +
    +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode 
    +            didUpdateImage:(UIImage *)image 
    +            withIdentifier:(id)imageIdentifier 
    +                 fromImage:(UIImage *)previousImage 
    +            withIdentifier:(id)previousImageIdentifier;
    +{    
    +        // this is optional, in case you want to react to the fact that a new image came in
    +}
    +
    + + +
    +
    + diff --git a/submodules/AsyncDisplayKit/docs/_docs/network-image-node.md b/submodules/AsyncDisplayKit/docs/_docs/network-image-node.md new file mode 100755 index 0000000000..2753afe3ed --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/network-image-node.md @@ -0,0 +1,119 @@ +--- +title: ASNetworkImageNode +layout: docs +permalink: /docs/network-image-node.html +prevPage: image-node.html +nextPage: video-node.html +--- + +`ASNetworkImageNode` can be used any time you need to display an image that is being hosted remotely. All you have to do is set the `.URL` property with the appropriate `NSURL` instance and the image will be asynchonously loaded and concurrently rendered for you. + +
    +SwiftObjective-C + +
    +
    +ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init];
    +imageNode.URL = [NSURL URLWithString:@"https://someurl.com/image_uri"];
    +	
    + + +
    +
    + +### Laying Out a Network Image Node + +Since an `ASNetworkImageNode` has no intrinsic content size when it is created, it is necessary for you to explicitly specify how they should be laid out. + +

    Option 1: .style.preferredSize

    + +If you have a standard size you want the image node's frame size to be you can use the `.style.preferredSize` property. + +
    +SwiftObjective-C + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constraint
    +{
    +	imageNode.style.preferredSize = CGSizeMake(100, 200);
    +	...
    +	return finalLayoutSpec;
    +}
    +
    + + +
    +
    + +

    Option 2: ASRatioLayoutSpec

    + +This is also a perfect place to use `ASRatioLayoutSpec`. Instead of assigning a static size for the image, you can assign a ratio and the image will maintain that ratio when it has finished loading and is displayed. + +
    +SwiftObjective-C + +
    +
    +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constraint
    +{
    +	CGFloat ratio = 3.0/1.0;
    +	ASRatioLayoutSpec *imageRatioSpec = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:ratio child:self.imageNode];
    +	...
    +	return finalLayoutSpec;
    +}
    +
    + + +
    +
    + +### Under the Hood + +
    If you choose not to include the PINRemoteImage and PINCache dependencies you will lose progressive jpeg support and be required to include your own custom cache that conforms to ASImageCacheProtocol.
    + +#### Progressive JPEG Support + +Thanks to the inclusion of PINRemoteImage, network image nodes now offer full support for loading progressive JPEGs. This means that if your server provides them, your images will display quickly at a lower quality that will scale up as more data is loaded. + +To enable progressive loading, just set `shouldRenderProgressImages` to `YES` like so: + +
    +SwiftObjective-C + +
    +
    +networkImageNode.shouldRenderProgressImages = YES;
    +
    + + +
    +
    + +It's important to remember that this is using one image that is progressively loaded. If your server is constrained to using regular JPEGs, but provides you with multiple versions of increasing quality, you should check out ASMultiplexImageNode instead. + +#### Automatic Caching + +`ASNetworkImageNode` now uses PINCache under the hood by default to cache network images automatically. + +#### GIF Support + +`ASNetworkImageNode` provides GIF support through `PINRemoteImage`'s beta `PINAnimatedImage`. Of note! This support will not work for local files unless `shouldCacheImage` is set to `NO`. diff --git a/submodules/AsyncDisplayKit/docs/_docs/node-overview.md b/submodules/AsyncDisplayKit/docs/_docs/node-overview.md new file mode 100755 index 0000000000..0b699e2daf --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/node-overview.md @@ -0,0 +1,81 @@ +--- +title: Node Subclasses +layout: docs +permalink: /docs/node-overview.html +prevPage: containers-overview.html +nextPage: subclassing.html +--- + +Texture offers the following nodes. + +A key advantage of using nodes over UIKit components is that **all nodes perform layout and display off of the main thread**, so that the main thread is available to immediately respond to user interaction events. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Texture NodeUIKit Equivalent
    ASDisplayNodein place of UIKit's UIView
    + The root Texture node, from which all other nodes inherit.
    ASCellNodein place of UIKit's UITableViewCell & UICollectionViewCell
    + ASCellNodes are used in ASTableNode, ASCollectionNode and ASPagerNode.
    ASScrollNodein place of UIKit's UIScrollView +

    This node is useful for creating a customized scrollable region that contains other nodes.

    ASEditableTextNode
    + ASTextNode
    in place of UIKit's UITextView
    + in place of UIKit's UILabel
    ASImageNode
    + ASNetworkImageNode
    + ASMultiplexImageNode
    in place of UIKit's UIImageView
    ASVideoNode
    + ASVideoPlayerNode
    in place of UIKit's AVPlayerLayer
    + in place of UIKit's UIMoviePlayer
    ASControlNodein place of UIKit's UIControl
    ASButtonNodein place of UIKit's UIButton
    ASMapNodein place of UIKit's MKMapView
    + +
    +Despite having rough equivalencies to UIKit components, in general, Texture nodes offer more advanced features and conveniences. For example, an `ASNetworkImageNode` does automatic loading and cache management, and even supports progressive jpeg and animated gifs. + +The `AsyncDisplayKitOverview` example app gives basic implementations of each of the nodes listed above. + + +# Node Inheritance Hierarchy + +All Texture nodes inherit from `ASDisplayNode`. + +node inheritance flowchart + +The nodes highlighted in blue are synchronous wrappers of UIKit elements. For example, `ASScrollNode` wraps a `UIScrollView`, and `ASCollectionNode` wraps a `UICollectionView`. An `ASMapNode` in `liveMapMode` is a synchronous wrapper of `UIMapView`. + + + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/overlay-layout-spec.md b/submodules/AsyncDisplayKit/docs/_docs/overlay-layout-spec.md new file mode 100755 index 0000000000..3ddbe8231c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/overlay-layout-spec.md @@ -0,0 +1,7 @@ +--- +title: ASOverlayLayoutSpec +layout: docs +permalink: /docs/overlay-layout-spec.html +--- + +
    😑 This page is coming soon...
    \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/philosophy.md b/submodules/AsyncDisplayKit/docs/_docs/philosophy.md new file mode 100755 index 0000000000..48481180af --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/philosophy.md @@ -0,0 +1,47 @@ +--- +title: Philosophy +layout: docs +permalink: /docs/philosophy.html +prevPage: getting-started.html +nextPage: installation.html +--- + +#Asynchronous Performance Gains + +Texture is a UI framework that was originally born from Facebook’s Paper app. It came as an answer to one of the core questions the Paper team faced. **How can you keep the main thread as clear as possible?** + +Nowadays, many apps have a user experience that relies heavily upon continuous gestures and physics based animations. At the very least, your UI is probably dependent on some form of scroll view. These types of user interfaces depend entirely on the main thread and are extremely sensitive to main thread stalls. **A clogged main thread means dropped frames and an unpleasant user experience.** + +Texture Nodes are a thread-safe abstraction layer over UIViews and CALayers: + +logo + +You can access most view and layer properties when using nodes, the difference is that nodes are rendered concurrently by default, and measured and laid out asynchronously when used correctly! + +Too see asynchronous performance gains in action, check out the `examples/ASDKgram` app which compares a UIKit-implemented social media feed with an Texture-implemented social media feed! + +On an iPhone 6+, the performance may not be radically different, but on a 4S, the difference is dramatic! Which leads us to Texture's next priority... + +#A Great App Experience for All Users + +Texture's performance gains allow you to easily design a great experience for every app user - across all devices, on all network connections. + +##A Great Developer Experience + +Texture also strives to make the developer experience great +- platform compatability: iOS & tvOS +- language compatability: Objective-C & Swift +- requires fewer lines of code to build advanced apps (see `examples/ASDKgram` for a direct comparison of a UIKit implemention of an app vs. an equivalent Texture implementation) +- cleaner architecture patterns +- robust code (some really brilliant minds have worked on this for 3+ years). + +#Advanced Developer Tools + +As Texture has grown, some of the brightest iOS engineers have contributed advanced technologies that will save you, as a developer using Texture, development time. + +###Advanced Technology +- ASRunLoopQueue +- ASRangeController with Intelligent Preloading + +###Network Code Savings +- automatic batch fetching (e.g. JSON payloads) diff --git a/submodules/AsyncDisplayKit/docs/_docs/placeholder-fade-duration.md b/submodules/AsyncDisplayKit/docs/_docs/placeholder-fade-duration.md new file mode 100755 index 0000000000..0b5ca8b131 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/placeholder-fade-duration.md @@ -0,0 +1,32 @@ +--- +title: Placeholders +layout: docs +permalink: /docs/placeholder-fade-duration.html +prevPage: image-modification-block.html +nextPage: accessibility.html +--- + +## ASDisplayNodes may Implement Placeholders + +Any `ASDisplayNode` subclass may implement the `-placeholderImage` method to provide a placeholder that covers content until a node's contents are finished displaying. To use placeholders, set `.placeholderEnabled = YES` and optionally set a `.placeholderFadeDuration`; + +For image drawing, use the node's `.calculatedSize` property. + +
    +The `-placeholderImage` function may be called on a background thread, so it is important that this function is thread safe. Note that `-[UIImage imageNamed:]` is not thread safe when using image assets. Instead use `-[UIImage imageWithContentsOfFile:]`. +
    + + +An ideal resource for creating placeholder images, including rounded rect solid colored ones or simple square corner ones is the `UIImage+ASConvenience` category methods in Texture. + +See our ancient Placeholders sample app to see this concept, first invented by the Facebook Paper team, in action. + +## `.neverShowPlaceholders` + +Hear Scott Goodson explain placeholders, `.neverShowPlaceholders` and why UIKit doesn't have them. + +## ASNetworkImageNode also have Default Images + +In _addition_ to placeholders, `ASNetworkImageNode`s also have a `.defaultImage` property. While placeholders are meant to be transient, default images will persist if the image node's `.URL` property is `nil` or if the URL fails to load. + +We suggest using default images for avatars, while using placeholder images for photos. diff --git a/submodules/AsyncDisplayKit/docs/_docs/principles.md b/submodules/AsyncDisplayKit/docs/_docs/principles.md new file mode 100755 index 0000000000..aec7fb2dfd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/principles.md @@ -0,0 +1,33 @@ +--- +title: Principles +layout: docs +permalink: /docs/principles.html +--- + +The following principles guide the design and development of the Texture framework. + +## 1. Reliable + +- **What:** Behavior should match the documentation. The framework shouldn't crash in production, even when used incorrectly. +- **Why:** If the framework is not reliable, then it cannot be used in production apps. More importantly, it will drain the morale of the engineers working on it. +- **How:** Meaningful, stable unit tests. We will devote a significant chunk of our resources to build unit tests. + +## 2. Familiar + +- **What:** Interfaces should match industry standards such as UIKit and CSS when possible. When we diverge from these standards, the interfaces should as be intuitive and direct as possible. +- **Why:** If the framework is not familiar, then companies will be wary about adopting it. Engineers trained in UIKit, especially junior ones, will be frustrated and unproductive. +- **How:** Compare API to other mature frameworks, reach out to users when developing new API to get feedback. Be generous with abstraction layers – as long as we don't sacrifice Reliable. + +## 3. Lean + +- **What:** Speed and memory conservation should be industry-leading, the API should be concise, and implementation code should be short and organized. +- **Why:** Performance is at the heart of Texture. It's what we do and we do it better than anyone else. In addition, a concise codebase and API are easier to maintain and learn. Plus it's just the right thing to do. +- **How:** Look for opportunities to improve performance. Think about the performance implications of each line of code. Dedicate resources to refactoring. Build tools to gather and expose performance metrics. + +## 4. Bold + +- **What:** Ambitious features, such as animated layout transitioning or our visibility-depth system, should be added from time to time. +- **Why:** Cutting-edge, never-before-seen tech gets people excited about the framework, and can raise the bar for the entire industry. They really move the needle on the user experience in subtle ways. Plus it's fun! +- **How:** Propose crazy ideas. See them through – ensure they get into the workflow and get resources allocated for them. + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/relative-layout-spec.md b/submodules/AsyncDisplayKit/docs/_docs/relative-layout-spec.md new file mode 100755 index 0000000000..a9e6a157b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/relative-layout-spec.md @@ -0,0 +1,7 @@ +--- +title: ASRelativeLayoutSpec +layout: docs +permalink: /docs/relative-layout-spec.html +--- + +
    😑 This page is coming soon...
    \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/resources.md b/submodules/AsyncDisplayKit/docs/_docs/resources.md new file mode 100755 index 0000000000..51a388a7d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/resources.md @@ -0,0 +1,50 @@ +--- +title: Resources +layout: docs +permalink: /docs/resources.html +prevPage: getting-started.html +nextPage: installation.html +--- + +### Slack + +Join 700+ Texture developers and the Texture core team on Slack for real-time debugging, the latest updates, and asynchronous banter. Signup here. + +### Examples +Browse through our many example projects. + +If you are new to Texture, we recommend that you start with the ASDKgram example app which compares a photo feed implemented with UIKit to an identical feed implemented with Texture. The app features: +
      +
    1. An infinitely scrolling home feed that demonstrates Texture's smoother scrolling performance.
    2. +
    3. A significantly sized code base to demonstrate how much less code it takes to design apps using Texture.
    4. +
    + +### Videos + + +### Tutorials / Articles + + + +### Layout Resources +Texture's powerful layout system is based on the CSS FlexBox model. These sites are useful for learning the basics of this system. + diff --git a/submodules/AsyncDisplayKit/docs/_docs/roadmap.md b/submodules/AsyncDisplayKit/docs/_docs/roadmap.md new file mode 100755 index 0000000000..2ef0873623 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/roadmap.md @@ -0,0 +1,49 @@ +--- +title: Roadmap +layout: docs +permalink: /docs/roadmap.html +--- + +This document outlines some of the upcoming plans for Texture. Since Texture is a fast-moving project with a small core team, this roadmap will change over time. + +The Texture roadmap is driven by the framework's four key qualities. You can read read more about the principles here. + +## 2.1 Release + +#### Familiar + +- Increase investment in Swift over time. +- Adopt a more regular release cadence. +- Reversible 0-100% transitions for our Layout Transition API. + +#### Bold + +- Declarative collection node API. [Try it out]() and give us feedback! + +## 2.5+ Release + +#### Reliable + +- Audit typography features. + +#### Familiar + +- Better supplementary node support. + +#### Lean + +- True asynchronous layout. + +#### Bold + +- First class transitions with the Layout Transition API. +- Extreme debuggability. +- AsyncKit? + +## Ways to Get Involved + +- Connect on GitHub, Slack and Twitter. +- Vet our documentation. Submit or suggest ways to improve. +- Share your experience using Texture. Thanks Buffer! +- Contribute layout examples. +- Contribute code. Try to implement one of our "Needs Volunteer" issues. diff --git a/submodules/AsyncDisplayKit/docs/_docs/scroll-node.md b/submodules/AsyncDisplayKit/docs/_docs/scroll-node.md new file mode 100755 index 0000000000..465ac00e62 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/scroll-node.md @@ -0,0 +1,78 @@ +--- +title: ASScrollNode +layout: docs +permalink: /docs/scroll-node.html +prevPage: control-node.html +nextPage: editable-text-node.html +--- + +`ASScrollNode` is an `ASDisplayNode` whose underlying view is an `UIScrollView`. This class offers the ability to automatically adopt its `ASLayoutSpec`'s size as the scrollable `contentSize`. + +### automaticallyManagesContentSize + +When enabled, the size calculated by the `ASScrolNode`'s layout spec defines the `.contentSize` of the scroll view. This is in contrast to most nodes, where the `layoutSpec` size is applied to the bounds (and in turn, frame). In this mode, the bounds of the scroll view always fills the parent's size. + +`automaticallyManagesContentSize` is useful both for subclasses of `ASScrollNode` implementing `layoutSpecThatFits:` or may also be used as the base class with `.layoutSpecBlock` set. In both cases, it is common use `.automaticallyManagesSubnodes` so that the nodes in the layout spec are added to the scrollable area automatically. + +With this approach there is no need to capture the layout size, use an absolute layout spec as a wrapper, or set `contentSize` anywhere in the code and it will update as the layout changes! Instead, it is very common and useful to simply return an `ASStackLayoutSpec` and the scrollable area will allow you to see all of it. + +### scrollableDirections + +This option is useful when using `automaticallyManagesContentSize`, especially if you want horizontal content (because the default is vertical). + +This property controls how the `constrainedSize` is interpreted when sizing the content. Options include: + + + + + + + + + + + + + + +
    VerticalThe `constrainedSize` is interpreted as having unbounded `.height` (`CGFLOAT_MAX`), allowing stacks and other content in the layout spec to expand and result in scrollable content.
    HorizontalThe `constrainedSize` is interpreted as having unbounded `.width` (`CGFLOAT_MAX`).
    Vertical & HorizontalThe `constrainedSize` is interpreted as unbounded in both directions.
    + +### Example + +In case you're not familiar with scroll views, they are basically windows into content that would take up more space than can fit in that area. + +Say you have a giant image, but you only want to take up 200x200 pts on the screen. + +
    +SwiftObjective-C + +
    +
    +// NOTE: If you are using a horizontal stack, set scrollNode.scrollableDirections.
    +ASScrollNode *scrollNode = [[ASScrollNode alloc] init];
    +scrollNode.automaticallyManagesSubnodes = YES;
    +scrollNode.automaticallyManagesContentSize = YES;
    +
    +scrollNode.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange constrainedSize){
    +  ASStackLayoutSpec *stack = [ASStackLayoutSpec verticalStackLayoutSpec];
    +  // Add children to the stack.
    +  return stack;
    +};
    +
    +
    + +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/_docs/static-layout-spec.md b/submodules/AsyncDisplayKit/docs/_docs/static-layout-spec.md new file mode 100755 index 0000000000..01318bf4ac --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/static-layout-spec.md @@ -0,0 +1,7 @@ +--- +title: ASStaticLayoutSpec +layout: docs +permalink: /docs/static-layout-spec.html +--- + +
    😑 This page is coming soon...
    \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/subclassing.md b/submodules/AsyncDisplayKit/docs/_docs/subclassing.md new file mode 100755 index 0000000000..8052e20937 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/subclassing.md @@ -0,0 +1,110 @@ +--- +title: Subclassing +layout: docs +permalink: /docs/subclassing.html +prevPage: containers-overview.html +nextPage: faq.html +--- +The most important distinction when creating a subclass is whether you writing an ASViewController or an ASDisplayNode. This sounds obvious, but because some of these differences are subtle, it is important to keep this top of mind. + +## ASDisplayNode +
    +While subclassing nodes is similar to writing a UIView subclass, there are a few guidelines to follow to ensure that both that you're utilizing the framework to its full potential and that your nodes behave as expected. + +### `-init` + +This method is called on a **background thread** when using nodeBlocks. However, because no other method can run until -init is finished, it should never be necessary to have a lock in this method. + +The most important thing to remember is that your init method must be capable of being called on any queue. Most notably, this means you should never initialize any UIKit objects, touch the view or layer of a node (e.g. `node.layer.X` or `node.view.X`) or add any gesture recognizers in your initializer. Instead, do these things in `-didLoad`. + +### `-didLoad` + +This method is conceptually similar to UIViewController's `-viewDidLoad` method; it’s called once and is the point where the backing view has been loaded. It is guaranteed to be called on the **main thread** and is the appropriate place to do any UIKit things (such as adding gesture recognizers, touching the view / layer, initializing UIKit objects). + +### `-layoutSpecThatFits:` + +This method defines the layout and does the heavy calculation on a **background thread**. This method is where you build out a layout spec object that will produce the size of the node, as well as the size and position of all subnodes. This is where you will put the majority of your layout code. + +The layout spec object that you create is malleable up until the point that it is return in this method. After this point, it will be immutable. It's important to remember not to cache layout specs for use later but instead to recreate them when necessary. + +Because it is run on a background thread, you should not set any `node.view` or `node.layer` properties here. Also, unless you know what you are doing, do not create any nodes in this method. Additionally, it is not necessary to begin this method with a call to super, unlike other method overrides. + +### `-layout` + +The call to super in this method is where the results of the layoutSpec are applied; Right after the call to super in this method, the layout spec will have been calculated and all subnodes will have been measured and positioned. + +`-layout` is conceptually similar to UIViewController's `-viewWillLayoutSubviews`. This is a good spot to change the hidden property, set view based properties if needed (not layoutable properties) or set background colors. You could put background color setting in -layoutSpecThatFits:, but there may be timing problems. If you happen to be using any UIViews, you can set their frames here. However, you can always create a node wrapper with `-initWithViewBlock:` and then size this on the background thread elsewhere. + +This method is called on the **main thread**. However, if you are using layout Specs, you shouldn't rely on this method too much, as it is much preferable to do layout off the main thread. Less than 1 in 10 subclasses will need this. + +One great use of `-layout` is for the specific case in which you want a subnode to be your exact size. E.g. when you want a collectionNode to take up the full screen. This case is not supported well by layout specs and it is often easiest to set the frame manually with a single line in this method: + +``` +subnode.frame = self.bounds; +``` + +If you desire the same effect in a ASViewController, you can do the same thing in -viewWillLayoutSubviews, unless your node is the node in initWithNode: and in that case it will do this automatically. + +## ASViewController +
    +An `ASViewController` is a regular `UIViewController` subclass that has special features to manage nodes. Since it is a UIViewController subclass, all methods are called on the **main thread** (and you should always create an ASViewController on the main thread). + +### `-init` + +This method is called once, at the very beginning of an ASViewController's lifecycle. As with UIViewController initialization, it is best practice to **never access** `self.view` or `self.node.view` in this method as it will force the view to be created early. Instead, do any view access in -viewDidLoad. + +ASViewController's designated initializer is `initWithNode:`. A typical initializer will look something like the code below. Note how the ASViewController's node is created _before_ calling super. An ASViewController manages a node similarly to how a UIViewController manages a view, but the initialization is slightly different. + + +
    +SwiftObjective-C + +
    +
    +- (instancetype)init
    +{
    +  _pagerNode = [[ASPagerNode alloc] init];
    +  self = [super initWithNode:_pagerNode];
    +  
    +  // setup any instance variables or properties here
    +  if (self) {
    +    _pagerNode.dataSource = self;
    +    _pagerNode.delegate = self;
    +  }
    +  
    +  return self;
    +}
    +
    + +
    +
    + +### `-loadView` + +We recommend that you do not use this method because it is has no particular advantages over `-viewDidLoad` and has some disadvantages. However, it is safe to use as long as you do not set the `self.view` property to a different value. The call to [super loadView] will set it to the `node.view` for you. + +### `-viewDidLoad` + +This method is called once in a ASViewController's lifecycle, immediately after `-loadView`. This is the earliest time at which you should access the node's view. It is a great spot to put any **setup code that should only be run once and requires access to the view/layer**, such as adding a gesture recognizer. + +Layout code should never be put in this method, because it will not be called again when geometry changes. Note this is equally true for UIViewController; it is bad practice to put layout code in this method even if you don't currently expect geometry changes. + +### `-viewWillLayoutSubviews` + +This method is called at the exact same time as a node's `-layout` method and it may be called multiple times in a ASViewController's lifecycle; it will be called whenever the bounds of the ASViewController's node are changed (including rotation, split screen, keyboard presentation) as well as when there are changes to the hierarchy (children being added, removed, or changed in size). + +For consistency, it is best practice to put all layout code in this method. Because it is not called very frequently, even code that does not directly depend on the size belongs here. + +### `-viewWillAppear:` / `-viewDidDisappear:` + +These methods are called just before the ASViewController's node appears on screen (the earliest time that it is visible) and just after it is removed from the view hierarchy (the earliest time that it is no longer visible). These methods provide a good opportunity to start or stop animations related to the presentation or dismissal of your controller. This is also a good place to make a log of a user action. + +Although these methods may be called multiple times and geometry information is available, they are not called for all geometry changes and so should not be used for core layout code (beyond setup required for specific animations). diff --git a/submodules/AsyncDisplayKit/docs/_docs/subtree-rasterization.md b/submodules/AsyncDisplayKit/docs/_docs/subtree-rasterization.md new file mode 100755 index 0000000000..20aa0761bd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/subtree-rasterization.md @@ -0,0 +1,26 @@ +--- +title: Subtree Rasterization +layout: docs +permalink: /docs/subtree-rasterization.html +prevPage: layer-backing.html +nextPage: synchronous-concurrency.html +--- + +Flattening an entire view hierarchy into a single layer improves performance, but with UIKit, comes with a hit to maintainability and hierarchy-based reasoning. + +With all Texture nodes, enabling precompositing is as simple as: + +
    +SwiftObjective-C +
    +
    +[rootNode enableSubtreeRasterization];
    +
    + +
    +
    +
    + +This line will cause the entire node hierarchy from that point on to be rendered into one layer. diff --git a/submodules/AsyncDisplayKit/docs/_docs/synchronous-concurrency.md b/submodules/AsyncDisplayKit/docs/_docs/synchronous-concurrency.md new file mode 100755 index 0000000000..d4430b7516 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/synchronous-concurrency.md @@ -0,0 +1,32 @@ +--- +title: Synchronous Concurrency +layout: docs +permalink: /docs/synchronous-concurrency.html +prevPage: subtree-rasterization.html +nextPage: corner-rounding.html +--- + +Both `ASViewController` and `ASCellNode` have a property called `neverShowPlaceholders`. + +By setting this property to YES, the main thread will be blocked until display has completed for the cell or view controller's view. + +Using this option does not eliminate all of the performance advantages of Texture. Normally, a given node has been preloading and is almost done when it reaches the screen, so the blocking time is very short. Even if the rangeTuningParameters are set to 0 this option outperforms UIKit. While the main thread is waiting, all subnode display executes concurrently, thus synchronous concurrency. + +See the NSSpain 2015 talk video for a visual walkthrough of this behavior. + +
    +SwiftObjective-C +
    +
    +node.neverShowPlaceholders = YES;
    +
    + +
    +
    +
    + +Usually, if a cell hasn't finished its display pass before it has reached the screen it will show placeholders until it has drawing its content. Setting this option to YES makes your scrolling node or ASViewController act more like UIKit, and in fact makes Texture scrolling visually indistinguishable from UIKit's, except that it's faster. + + diff --git a/submodules/AsyncDisplayKit/docs/_docs/team.md b/submodules/AsyncDisplayKit/docs/_docs/team.md new file mode 100755 index 0000000000..e3a9643912 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/team.md @@ -0,0 +1,44 @@ +--- +title: Pinterest Team +layout: docs +permalink: /docs/team.html +--- + + + + + + + + + + + + + + + + + + + + + + +

    Scott Goodson (@appleguy) is an original author of Texture and, most recently, a driving force behind making Pinterest's design vision a reality with the recent rewrite of the iOS app.

    +

    Previously, Scott managed the Facebook Paper and Instagram iOS engineering teams, and helped lead the native code rewrite of the core Facebook iOS app. He also spent four years at Apple where he was one of the first ten engineers to work on iPhone OS 1.0, and developed apps like Stocks and Calculator.

    +

    Scott is deeply passionate about building Texture into a framework that allows effortless development of polished and performant apps that serve all users, regardless of device age, internet connection, or language.

    Michael Schneider (@maicki) is especially passionate about API design and recently led the re-architecture of the layout API for the 2.0 release. As our resident layout expert, Michael volunteers much of his own time to help developers on Texture's public slack channel. Before he joined Pinterest, Michael worked on Pocket for iOS, Mac and Chrome and Read Later an Instapaper and Pocket Mac app.

    Huy Nguyen (@nguyenhuy ) joined the Pinterest team after authoring Texture's automatic layout feature, which has become the foundation for the Texture's 2.0 release. To date, the Layout API has been the largest contribution to the framework by a community member!

    Garrett Moon (@garrettmoon ) is the fearless leader of Pinterest's framework team. He also authored PINRemoteImage - a threadsafe, performant, feature rich image fetcher, and PINCache, a non-deadlocking fork of TMCache. Both are used as the backing store for ASNetworkImageNode.

    Adlai ("Ad-lee") Holler (@adlai-holler) joined the Pinterest team after making major contributions to the framework while writing Tripstr in Swift with Texture.

    +
    + +# Join us! + +We are looking for senior developers familiar with Texture to join our team! + +We have an exciting roadmap that we believe will continue to push the boundaries of what is possible on the iOS platform, while making the framework easier to use than ever before. + +As part of the team, you would work on Texture, [PINRemoteImage](https://github.com/pinterest/PINRemoteImage), and [PINCache](https://github.com/pinterest/PINCache) (the backing store for ASNetworkImageNode), while using all three in Pinterest's [app](https://itunes.apple.com/us/app/pinterest/id429047995). + +One interesting thing to note is that Pinterest does not have an internal fork of Texture. Everything is developed on master, with release branches cut from master only a few weeks before our public application launches. This allows us to move exceptionally quickly in developing and launching improvements to millions of users. + +Sound interesting? +Send us an email at textureframework@gmail.com. diff --git a/submodules/AsyncDisplayKit/docs/_docs/text-cell-node.md b/submodules/AsyncDisplayKit/docs/_docs/text-cell-node.md new file mode 100755 index 0000000000..e9d0438b53 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/text-cell-node.md @@ -0,0 +1,45 @@ +--- +title: ASTextCellNode +layout: docs +permalink: /docs/text-cell-node.html +prevPage: cell-node.html +nextPage: control-node.html +--- + +ASTextCellNode is a simple ASCellNode subclass you can use when all you need is a cell with styled text. + +
    +SwiftObjective-C +
    +
    +ASTextCellNode *textCell = [[ASTextCellNode alloc]
    +            initWithAttributes:@{NSFontAttributeName: [UIFont fontWithName:@"SomeFont" size:16.0]} 												  insets:UIEdgeInsetsMake(8, 16, 8, 16)];
    +  
    + +
    +
    + +The text can be configured on initialization or after the fact. + +
    +SwiftObjective-C +
    +
    +ASTextCellNode *textCell = [[ASTextCellNode alloc] init];
    +
    +textCellNode.text         = @"Some dang ol' text";
    +textCellNode.attributes   = @{NSFontAttributeName: [UIFont fontWithName:@"SomeFont" size:16.0]};
    +textCellNode.insets       = UIEdgeInsetsMake(8, 16, 8, 16);
    +  
    + +
    +
    \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_docs/text-node.md b/submodules/AsyncDisplayKit/docs/_docs/text-node.md new file mode 100755 index 0000000000..176a721999 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/text-node.md @@ -0,0 +1,228 @@ +--- +title: ASTextNode +layout: docs +permalink: /docs/text-node.html +prevPage: button-node.html +nextPage: image-node.html +--- + +`ASTextNode` is Texture's main text node and can be used any time you would normally use a `UILabel`. It includes full rich text support and is a subclass of `ASControlNode` meaning it can be used any time you would normally create a UIButton with just its titleLabel set. + +### Basic Usage +`ASTextNode`'s interface should be familiar to anyone who's used a `UILabel`. The first difference you may notice, is that text node's only use attributed strings instead of having the option of using a plain string. + +
    +SwiftObjective-C + +
    +
    +NSDictionary *attrs = @{ NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue" size:12.0f] };
    +NSAttributedString *string = [[NSAttributedString alloc] initWithString:@"Hey, here's some text." attributes:attrs];
    +
    +_node = [[ASTextNode alloc] init];
    +_node.attributedText = string;
    +
    + + +
    +
    + +As you can see, to create a basic text node, all you need to do is use a standard alloc-init and then set up the attributed string for the text you wish to display. + +### Truncation + +In any case where you need your text node to fit into a space that is smaller than what would be necessary to display all the text it contains, as much as possible will be shown, and whatever is cut off will be replaced with a truncation string. + + +
    +SwiftObjective-C + +
    +
    +_textNode = [[ASTextNode alloc] init];
    +_textNode.attributedText = string;
    +_textNode.truncationAttributedText = [[NSAttributedString alloc]
    +												initWithString:@"¶¶¶"];
    +
    + + +
    +
    + +This results in something like: + + + +By default, the truncation string will be "…" so you don't need to set it if that's all you need. + + +### Link Attributes + +In order to designate chunks of your text as a link, you first need to set the `linkAttributes` array to an array of strings which will be used as keys of links in your attributed string. Then, when setting up the attributes of your string, you can use these keys to point to appropriate `NSURL`s. + +
    +SwiftObjective-C + +
    +
    +_textNode.linkAttributeNames = @[ kLinkAttributeName ];
    +
    +NSString *blurb = @"kittens courtesy placekitten.com \U0001F638";
    +NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb];
    +[string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)];
    +[string addAttributes:@{
    +                      kLinkAttributeName: [NSURL URLWithString:@"http://placekitten.com/"],
    +                      NSForegroundColorAttributeName: [UIColor grayColor],
    +                      NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot),
    +                      }
    +              range:[blurb rangeOfString:@"placekitten.com"]];
    +_textNode.attributedText = string;
    +_textNode.userInteractionEnabled = YES;
    +
    + + +
    +
    + +Which results in a light gray link with a dash-dot style underline! + + + +As you can see, it's relatively convenient to apply various styles to each link given its range in the attributed string. + +### ASTextNodeDelegate + +Conforming to `ASTextNodeDelegate` allows your class to react to various events associated with a text node. For example, if you want to react to one of your links being tapped: + +
    +SwiftObjective-C + +
    +
    +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange
    +{
    +  // the link was tapped, open it
    +  [[UIApplication sharedApplication] openURL:URL];
    +}
    +
    + + +
    +
    + +In a similar way, you can react to long presses and highlighting with the following methods: + +`– textNode:longPressedLinkAttribute:value:atPoint:textRange:` + +`– textNode:shouldHighlightLinkAttribute:value:atPoint:` + +`– textNode:shouldLongPressLinkAttribute:value:atPoint:` + + +### Incorrect maximum number of lines with line spacing + +Using a `NSParagraphStyle` with a non-default `lineSpacing` can cause problems if multiline text with a maximum number of lines is wanted. For example see the following code: + +
    +SwiftObjective-C + +
    +
    +// ...
    +NSString *someLongString = @"...";
    +
    +NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    +paragraphStyle.lineSpacing = 10.0;
    +
    +UIFont *font = [UIFont fontWithName:@"SomeFontName" size:15];
    +
    +NSDictionary *attributes = @{
    +	NSFontAttributeName : font,
    +	NSParagraphStyleAttributeName: paragraphStyle
    +};
    +
    +ASTextNode *textNode = [[ASTextNode alloc] init];
    +textNode.maximumNumberOfLines = 4;
    +textNode.attributedText = [[NSAttributedString	alloc] initWithString:someLongString
    +																												   attributes:attributes];
    +// ...
    +
    + + +
    +
    + +`ASTextNode` uses Text Kit internally to calculate the amount to shrink needed to result in the specified maximum number of lines. Unfortunately, in certain cases this will result in the text shrinking too much in the above example; Instead of 4 lines of text, 3 lines of text and a weird gap at the bottom will show up. To get around this issue for now, you have to set the `truncationMode` explicitly to `NSLineBreakByTruncatingTail` on the text node: + +
    +SwiftObjective-C + +
    +
    +// ...
    +ASTextNode *textNode = [[ASTextNode alloc] init];
    +textNode.maximumNumberOfLines = 4;
    +textNode.truncationMode = NSLineBreakByTruncatingTail;
    +textNode.attributedText = [[NSAttributedString	alloc] initWithString:someLongString
    +																												   attributes:attributes];
    +// ...
    +
    + + +
    +
    +``` diff --git a/submodules/AsyncDisplayKit/docs/_docs/tip-1-nodeBlocks.md b/submodules/AsyncDisplayKit/docs/_docs/tip-1-nodeBlocks.md new file mode 100755 index 0000000000..bcefc9c512 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/tip-1-nodeBlocks.md @@ -0,0 +1,133 @@ +--- +title: Prefer `nodeBlocks` for Performance +layout: docs +permalink: /docs/tip-1-nodeBlocks.html +--- + +Texture’s `ASCollectionNode` replaces `UICollectionView`’s required method + +
    + + Swift + Objective-C + + +
    +
    +collectionView:cellForItemAtIndexPath:
    +  
    + + +
    +
    + +
    +with your choice of **one** of the two following methods + +
    + + Swift + Objective-C + + +
    +
    +// called on main thread, ASCellNode initialized on main and then returned 
    +collectionNode:nodeForItemAtIndexPath: 
    +
    +OR
    +
    +// called on main thread, ASCellNodeBlock returned, then
    +// ASCellNode initialized in background when block is called by system
    +collectionNode:nodeBlockForItemAtIndexPath: 
    +  
    + + +
    +
    + +
    +`ASTableNode` has the same options: + +
    + + Swift + Objective-C + + +
    +
    +`tableNode:nodeForRow:`
    +`tableNode:nodeBlockforRow:`    // preferred
    +  
    + + +
    +
    + +`ASPagerNode` does as well: + +
    + + Swift + Objective-C + + +
    +
    +`pagerNode:nodeAtIndex:`
    +`pagerNode:nodeBlockAtIndex:`   // preferred
    +  
    + + +
    +
    + + +We recommend that you use nodeBlocks. Using the nodeBlock method allows table and collections to request blocks for each cell node, and execute them **concurrently** across multiple threads, which allows us to **parallelize the allocation costs** (in addition to layout measurement). + +This leaves our main thread more free to handle touch events and other time sensitive work, keeping our user's taps happy and responsive. + +### Access your data source outside of the nodeBlock + +Because nodeBlocks are executed on a background thread, it is very important they be thread-safe. + +The most important aspect to consider is accessing properties on self that may change, such as an array of data models. This can be handled safely by ensuring that any immutable state is collected above the node block. + +**Using the indexPath parameter to access a mutable collection inside the node block is not safe.** This is because by the time the block runs, the dataSource may have changed. + +Here's an example of a simple nodeBlock: + +
    + + Swift + Objective-C + + +
    +
    +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
    +{
    +    // data model is accessed outside of the node block
    +    Board *board = [self.boards objectAtIndex:indexPath.item];
    +    return ^{
    +        BoardScrubberCellNode *node = [[BoardScrubberCellNode alloc] initWithBoard:board];
    +        return node;
    +    };
    +}
    +  
    + +
    +
    + +
    +Note that it is okay to use the indexPath if it is used strictly for its integer values and not to index a value from a mutable data source. + +## Do not return nil from a nodeBlock + +Just as when UIKit requests a cell, returning `nil` will crash the app, so it is important to ensure a valid ASCellNode is returned for either the node or nodeBlock method. Your code should ensure that at least a blank ASCellNode is returned, but ideally the number of items reported to the collection would prevent the method from being called when there is no data to display. diff --git a/submodules/AsyncDisplayKit/docs/_docs/uicollectionview-challenges.md b/submodules/AsyncDisplayKit/docs/_docs/uicollectionview-challenges.md new file mode 100755 index 0000000000..4916bf3e94 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/uicollectionview-challenges.md @@ -0,0 +1,160 @@ +--- +title: UICollectionView Challenges +layout: docs +permalink: /docs/uicollectionview-challenges.html +--- + +`UICollectionView` is one of the most commonly used classes and many challenges with iOS development are related to its architecture. + +## How `UICollectionView` Works + +There are two important methods that `UICollectionView` requires. + +

    Cell Measurement

    + +For each item in the data source, the collection must know its size to understand which items should be visible at a given momement. This is provided by: + +
    +SwiftObjective-C +
    +
    +- (CGSize)collectionView:(UICollectionView *)collectionView 
    +                  layout:(UICollectionViewLayout *)collectionViewLayout 
    +  sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
    +
    + +
    +
    + +Although not formally named by Apple, we refer to this process as "measuring". Implementing this method is always difficult, because the view that implements the cell layout is never available at the time of this method call. + +This means that logic must be duplicated between the implementation of this method and the `-layoutSubviews` implementation of the cell subclass. This presents a tremendous maintainence burden, as the implementations must always match their behavior for any combination of content displayed. + +Additionally, once measurement is complete, there's no easy way to cache that information to use it during the layout process. As a result, expensive text measurements must be repeated. + +

    Cell Allocation

    + +Once an item reaches the screen, a view representing it is requested: + +
    +SwiftObjective-C +
    +
    +- (UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath;
    +
    + +
    +
    + +In order to provide a cell, all subviews must be configured with the data that they are intended to display. Immediately afterwards, the layout of the cell is calculated, and finally the display (rendering) of the individual elements (text, images) contained within. + +
    +For those who are curious, this extremely detailed diagram shows the full process of UICollectionView communicating with its data source and delegate to display itself. +
    + +

    Limitations in `UICollectionView`'s Architecture

    + +There are several issues with the architecture outlined above: + +Lots of main thread work, which may degrade the user's experience, including + +
      +
    • cell measurement
    • +
    • cell creation + setup / reuse
    • +
    • layout
    • +
    • display (rendering)
    + +Duplicated layout logic + +You must have duplicate copies of your cell sizing logic for the cell measurement and cell layout stages. For example, if you want to add a price tag to your cell, both -sizeForItemAtIndexPath and the cell's own -layoutSubviews must be aware of how to size the tag. + +No automatic content loading + +There is no easy, universal way to handle loading content such as: +
      +
    • data pages - such as JSON fetching
    • +
    • other info - such as images or secondary JSON requests
    • +
    + +## How `ASCollectionNode` works + +

    Unified Cell Measurement & Allocation

    + +Texture takes both of the important collection methods explained above: + +
    +SwiftObjective-C +
    +
    +- (UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath;
    +
    +- (CGSize)collectionView:(UICollectionView *)collectionView 
    +                  layout:(UICollectionViewLayout *)collectionViewLayout 
    +  sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
    +
    + +
    +
    + +and replaces them with a single method*: + +
    +SwiftObjective-C +
    +
    +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath;
    +
    + +
    +
    + +or with the asynchronous versions + +
    +SwiftObjective-C +
    +
    +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath;
    +
    + +
    +
    + +*Note that there is an optional method to provide a constrained size for the cell, but it is not needed by most apps. + +ASCellNode, is Texture's universal cell class. They are light-weight enough to be created an an earlier time in the program (concurrently in the background) and they understand how to calculate their own size. `ASCellNode` automatically caches its measurement so that it can be quickly applied during the layout pass. + +
    +As a comparison to the diagram above, this detailed diagram shows the full process of an ASCollectionView communicating with its data source and delegate to display itself.. Note that ASCollectionView is ASCollectionNode's underlying UICollectionView subclass. +
    + +

    Benefits of Texture's Architecture

    + +Elimination of all of the types of main thread work described above (cell allocation, measurement, layout, display)! In addition, all of this work is preformed concurrently on multiple threads. + +Because `ASCollectionNode` is aware of the position of all of its nodes, it can automatically determine when content loading is needed. The Batch Fetching API handles loading of data pages (like JSON) and Intelligent Preloading automatically manages the loading of images and text. Additionally, convenient callbacks allow implementing accurate visibility logging and secondary data model requests. + +Lastly, almost all of the concepts we've discussed here apply to `UITableView` / `ASTableNode` and `UIPageViewController` / `ASPagerNode`. + +## iOS 10 Cell Pre-fetching +Inspired by Texture, iOS 10 introduced a cell pre-fetching. This API increases the number of cells that the collection tracks at any given time, which helps, but isn't anywhere as performance centric as being aware of all cells in the data source. + +Additionally, iOS9 still constitutes a substantial precentage of most app's userbase and will not reduce in number anywhere close to as quickly as the sunset trajectory of iOS 7 and iOS 8 devices. Whereas iOS 9 is the last supported version for about a half-dozen devices, there were zero devices that were deprecated on iOS 8 and only one deivce deprecated on iOS 7. + +Unfortunately, these iOS 9 devices are the ones in which performance is most key! diff --git a/submodules/AsyncDisplayKit/docs/_docs/uicollectionviewinterop.md b/submodules/AsyncDisplayKit/docs/_docs/uicollectionviewinterop.md new file mode 100755 index 0000000000..26fc1942e7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/uicollectionviewinterop.md @@ -0,0 +1,81 @@ +--- +title: UICollectionViewCell Interoperability +layout: docs +permalink: /docs/uicollectionviewinterop.html +prevPage: placeholder-fade-duration.html +nextPage: accessibility.html +--- + +Texture's `ASCollectionNode` offers compatibility with synchronous, standard `UICollectionViewCell` objects alongside native `ASCellNodes`. + +Note that these UIKit cells will **not** have the performance benefits of `ASCellNodes` (like preloading, async layout, and async drawing), even when mixed within the same `ASCollectionNode`. + +However, this interoperability allows developers the flexibility to test out the framework without needing to convert all of their cells at once. + +## Implementing Interoperability + +In order to use this feature, you must: + +
      +
    1. Conform to ASCollectionDataSourceInterop and, optionally, ASCollectionDelegateInterop.
    2. +
    3. Call registerCellClass: on the collectionNode.view (in viewDidLoad, or register an onDidLoad: block).
    4. +
    5. Return nil from the nodeBlockForItem...: or nodeForItem...: method. Note: it is an error to return nil from within a nodeBlock, if you have returned a nodeBlock object.
    6. +
    7. Lastly, you must implement a method to provide the size for the cell. There are two ways this is done:
    8. +
        +
      1. UICollectionViewFlowLayout (incl. ASPagerNode). Implement + collectionNode:constrainedSizeForItemAtIndexPath:.
      2. +
      3. Custom collection layouts. Set .view.layoutInspector and have it implement + collectionView:constrainedSizeForNodeAtIndexPath:.
      4. +
      +
    + +By default, the interop data source will only be consulted in cases where no `ASCellNode` is provided to Texture. However, if .dequeuesCellsForNodeBackedItems is enabled, then the interop data source will always be consulted to dequeue cells, and will be expected to return _ASCollectionViewCells in cases where a node was provided. + +## CustomCollectionView Example App + +The [CustomCollectionView](https://github.com/texturegroup/texture/tree/master/examples/CustomCollectionView) example project demonstrates how to use raw `UIKit` cells alongside native `ASCellNodes`. + +Open the app and verify that `kShowUICollectionViewCells` is enabled in `Sample/ViewController.m`. + +For this example, the data source method `collectionNode:nodeBlockForItemAtIndexPath:` is setup to return nil for every third cell. When nil is returned, `ASCollectionNode` will automatically query the `cellForItemAtIndexPath:` data source method. + +
    + + Swift + Objective-C + + +
    +
    +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode 
    +      nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
    +{
    +  if (kShowUICollectionViewCells && indexPath.item % 3 == 1) {
    +    // When enabled, return nil for every third cell and then 
    +    // cellForItemAtIndexPath: will be called.
    +    return nil;
    +  }
    +  
    +  UIImage *image = _sections[indexPath.section][indexPath.item];
    +  return ^{
    +    return [[ImageCellNode alloc] initWithImage:image];
    +  };
    +}
    +
    +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView 
    +                  cellForItemAtIndexPath:(NSIndexPath *)indexPath
    +{
    +  return [_collectionNode.view dequeueReusableCellWithReuseIdentifier:kReuseIdentifier 
    +                                                         forIndexPath:indexPath];
    +}
    +  
    + + +
    +
    + +Run the app to see the orange `UICollectionViewCells` interspersed every 3rd cell among the `ASCellNodes` containing images. + diff --git a/submodules/AsyncDisplayKit/docs/_docs/video-node.md b/submodules/AsyncDisplayKit/docs/_docs/video-node.md new file mode 100755 index 0000000000..6a81074a85 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_docs/video-node.md @@ -0,0 +1,87 @@ +--- +title: ASVideoNode +layout: docs +permalink: /docs/video-node.html +prevPage: network-image-node.html +nextPage: map-node.html +--- + +`ASVideoNode` provides a convenient and performant way to display videos in your app. + +
    Note: If you use `ASVideoNode` in your application, you must link `AVFoundation` since it uses `AVPlayerLayer` and other `AVFoundation` classes under the hood.
    + +### Basic Usage + +The easiest way to use `ASVideoNode` is to assign it an `AVAsset`. + +
    +SwiftObjective-C + +
    +
    +ASVideoNode *videoNode = [[ASVideoNode alloc] init];
    +
    +AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"http://www.w3schools.com/html/mov_bbb.mp4"]];
    +videoNode.asset = asset;
    +
    + + +
    +
    + +### Autoplay, Autorepeat, and Muting + +You can configure the way your video node reacts to various events with a few simple `BOOL`s. + +If you'd like your video to automaticaly play when it enters the visible range, set the `shouldAutoplay` property to `YES`. Setting `shouldAutoRepeat` to `YES` will cause the video to loop indefinitely, and, of course, setting `muted` to `YES` will turn the video's sound off. + +To set up a node that automatically plays once silently, you would just do the following. + +
    +SwiftObjective-C + +
    +
    +videoNode.shouldAutoplay = YES;
    +videoNode.shouldAutorepeat = NO;
    +videoNode.muted = YES;
    +
    + +
    +
    + +### Placeholder Image + +Since video nodes inherit from `ASNetworkImageNode`, you can use the `URL` property to assign a placeholder image. If you decide not to, the first frame of your video will automatically decoded and used as the placeholder instead. + + + + +### ASVideoNode Delegate + +There are a ton of delegate methods available to you that allow you to react to what's happening with your video. For example, if you want to react to the player's state changing, you can use: + +
    +SwiftObjective-C + +
    +
    +- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState;
    +
    + +
    +
    + +The easiest way to see them all is to take a look at the `ASVideoNode` header file. + diff --git a/submodules/AsyncDisplayKit/docs/_includes/analytics.html b/submodules/AsyncDisplayKit/docs/_includes/analytics.html new file mode 100755 index 0000000000..71dbfbce70 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_includes/analytics.html @@ -0,0 +1,10 @@ + diff --git a/submodules/AsyncDisplayKit/docs/_includes/footer.html b/submodules/AsyncDisplayKit/docs/_includes/footer.html new file mode 100755 index 0000000000..f24db5c9ab --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_includes/footer.html @@ -0,0 +1,19 @@ + + + + + + diff --git a/submodules/AsyncDisplayKit/docs/_includes/header.html b/submodules/AsyncDisplayKit/docs/_includes/header.html new file mode 100755 index 0000000000..e22b49e1cd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_includes/header.html @@ -0,0 +1,64 @@ + + + + + Texture | {{ page.title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if jekyll.environment == 'production' %} + {% include analytics.html %} + {% endif %} + + +
    +
    +

    AsyncDisplayKit is now Texture! LEARN MORE

    +
    +
    +
    +
    +

    + + Texture + +

    + +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/_includes/hero.html b/submodules/AsyncDisplayKit/docs/_includes/hero.html new file mode 100755 index 0000000000..e94f6b5f1b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_includes/hero.html @@ -0,0 +1,9 @@ +
    +
    +
    +

    Texture

    + +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/_includes/nav_development.html b/submodules/AsyncDisplayKit/docs/_includes/nav_development.html new file mode 100644 index 0000000000..248eb4388a --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_includes/nav_development.html @@ -0,0 +1,22 @@ + diff --git a/submodules/AsyncDisplayKit/docs/_includes/nav_docs.html b/submodules/AsyncDisplayKit/docs/_includes/nav_docs.html new file mode 100755 index 0000000000..ac4b089557 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_includes/nav_docs.html @@ -0,0 +1,22 @@ + diff --git a/submodules/AsyncDisplayKit/docs/_layouts/apidiff.html b/submodules/AsyncDisplayKit/docs/_layouts/apidiff.html new file mode 100755 index 0000000000..8b404ac0dc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_layouts/apidiff.html @@ -0,0 +1,14 @@ +--- +sectionid: appledocs +--- + +{% include header.html %} + +
    +
    + +
    + {{ content }} +
    +
    +
    \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/_layouts/appledocs.html b/submodules/AsyncDisplayKit/docs/_layouts/appledocs.html new file mode 100755 index 0000000000..25b9391d93 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_layouts/appledocs.html @@ -0,0 +1,15 @@ +--- +sectionid: appledocs +--- + +{% include header.html %} + +
    +
    + +
    + {{ content }} +
    +
    +
    +{% include footer.html %} diff --git a/submodules/AsyncDisplayKit/docs/_layouts/default.html b/submodules/AsyncDisplayKit/docs/_layouts/default.html new file mode 100755 index 0000000000..c9b8ad18ec --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_layouts/default.html @@ -0,0 +1,16 @@ +{% include header.html %} + +{% if page.hero %} + {% include hero.html %} +{% endif %} + +
    +
    +
    + +{{ content }} + +
    +
    +
    +{% include footer.html %} diff --git a/submodules/AsyncDisplayKit/docs/_layouts/docs.html b/submodules/AsyncDisplayKit/docs/_layouts/docs.html new file mode 100755 index 0000000000..42e7a8d4f6 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_layouts/docs.html @@ -0,0 +1,40 @@ +--- +sectionid: docs +--- + +{% include header.html %} + +
    + + + +
    +

    + {{ page.title }} +

    +

    {{ page.description }}

    + + {{ content }} + +

    Edit on GitHub

    + + +
    + {% if page.prevPage %} + ← Prev + {% endif %} + {% if page.nextPage %} + Next → + {% endif %} +
    + + +
    + +
    +
    + +{% include footer.html %} diff --git a/submodules/AsyncDisplayKit/docs/_layouts/redirect.html b/submodules/AsyncDisplayKit/docs/_layouts/redirect.html new file mode 100755 index 0000000000..c24f817484 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_layouts/redirect.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/submodules/AsyncDisplayKit/docs/_layouts/slack.html b/submodules/AsyncDisplayKit/docs/_layouts/slack.html new file mode 100755 index 0000000000..95d73c3484 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_layouts/slack.html @@ -0,0 +1,14 @@ +--- +sectionid: slack +--- + +{% include header.html %} + +
    +
    + +
    + {{ content }} +
    +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/_sass/_code.scss b/submodules/AsyncDisplayKit/docs/_sass/_code.scss new file mode 100644 index 0000000000..dd4df0e23e --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/_sass/_code.scss @@ -0,0 +1,221 @@ +code { + letter-spacing: 0.02em; + padding: 2px 4px; + /*font-weight: 700; + color: #008ED4; + font-size: 14px; + vertical-align: baseline; + font-family: 'Droid Sans Mono',sans-serif;*/ +} + +p code, span code, li code { + background-color: rgba(135, 215, 255, 0.2); +} + +.paddingBetweenCols { + th { + text-align: left; + padding: 15px 15px 15px 15px; + } + td { + padding: 15px 15px 15px 15px; + /* border: 1 px solid black;*/ + } +} + +.highlight pre, .redhighlight pre { + font-size: 13px; + line-height: 20px; + padding: 0 10px 20px 10px; +} + +.highlighttable { + margin-left: 10px; + margin-bottom: 20px; + margin-right: 10px; + border-collapse: separate !important; + .highlight pre, .redhighlight pre { + padding: 0; + } + code { + padding-right: 10px; + margin: 0; + padding-left: 2px; + font-size: 12px; + display: block; + line-height: 20px; + white-space: pre; + color: hsl(210, 100%, 8%); + } +} + +.highlight pre code, .redhighlight pre code, .highlighttable { + overflow-x: scroll; + display: block; + padding: 0; + font-size: 13px; + border: 1px solid rgb(220, 220, 220); + padding: 5px 10px; + box-sizing: border-box; + border-radius: 3; +} + +.highlight pre code, .highlighttable { + background-color: rgba(90, 140, 140, 0.1); +} + +.redhighlight pre code { + background-color: rgba(180, 80, 80, 0.1); +} + +.showcasetable code { + padding: 10px 10px 30px 30px; + margin: 10; +} + +.highlight pre code, .redhighlight pre code { + width: 100%; + white-space: pre; +} + +.lineno { + color: rgb(214, 139, 0); + &::after { + content: ';'; + font-size: 0; + } +} + +.highlight + .highlight + .highlight, .highlighttable + .highlight + .highlight, .redhighlight + .redhighlight + .redhighlight .highlighttable + .redhighlight + .redhighlight { + margin-top: -20px; +} + +/** + * Syntax highlighting styles + */ +/* not official Xcode colors, but looks better on the web */ + +.highlight { + background: #fff; + .c { + color: #008d14; + font-style: italic; + } + .err { + color: #a61717; + background-color: #e3d2d2; + } + .k { + color: #103ffb; + } + .cm { + color: #008d14; + font-style: italic; + } + .cp { + color: #b72748; + } + .c1 { + color: #008d14; + font-style: italic; + } + .cs { + color: #008d14; + font-weight: bold; + font-style: italic; + } + .gd { + color: #000; + background-color: #fdd; + .x { + color: #000; + background-color: #faa; + } + } + .ge { + font-style: italic; + } + .gr { + color: #a00; + } + .gh { + color: #999; + } + .gi { + color: #000; + background-color: #dfd; + .x { + color: #000; + background-color: #afa; + } + } + .go { + color: #888; + } + .gp { + color: #555; + } + .gs { + font-weight: bold; + } + .gu { + color: #aaa; + } + .gt { + color: #a00; + } + .kc, .kd { + color: orange; + } + .kp, .kr { + color: #008d14; + } + .kt { + color: #103ffb; + } + .m { + color: orange; + } + .s { + color: #b72748; + } + .na { + color: orange; + } + .nb { + color: #103ffb; + } + .nc { + color: #3a95ba; + } + .no, .ni, .ne, .nn, .nt { + color: orange; + } + .w { + color: #bbb; + } + .mh, .mi, .mo, .il { + color: black; + } + .sb, .sc, .sd, .s2, .se, .sh, .si, .sx { + color: #d14; + } + .sr { + color: orange; + } + .s1, .ss { + color: #b72748; + } + .bp, .vc { + color: #3a95ba; + } + .vg { + color: black; + } + .vi { + color: orange; + } + .nl { + color: #3a95ba; + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/apidiff/ASDK_API_Diff_1.9.92_to_2.0.html b/submodules/AsyncDisplayKit/docs/apidiff/ASDK_API_Diff_1.9.92_to_2.0.html new file mode 100755 index 0000000000..991484bb9c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/apidiff/ASDK_API_Diff_1.9.92_to_2.0.html @@ -0,0 +1,2224 @@ + + + + + + +
    +
    ASAbsoluteLayoutElement.h
    + +
    +
    Added ASAbsoluteLayoutElement
    +
    Added ASAbsoluteLayoutElement.layoutPosition
    +
    Added ASAbsoluteLayoutElement.sizeRange
    +
    + +
    + +
    +
    ASAbsoluteLayoutSpec.h
    + +
    +
    Added ASAbsoluteLayoutSpecSizing
    +
    Added ASAbsoluteLayoutSpecSizingDefault
    +
    Added ASAbsoluteLayoutSpecSizingSizeToFit
    +
    Added ASAbsoluteLayoutSpec
    +
    Added ASAbsoluteLayoutSpec.sizing
    +
    Added +[ASAbsoluteLayoutSpec absoluteLayoutSpecWithSizing:children:]
    +
    Added +[ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:]
    +
    + + +
    +
    Modified ASStaticLayoutSpec
    + + + + +
    HeaderSuperclass
    FromASStaticLayoutSpec.hASLayoutSpec
    ToASAbsoluteLayoutSpec.hASAbsoluteLayoutSpec
    +
    +
    Modified +[ASStaticLayoutSpec staticLayoutSpecWithChildren:]
    + + + + +
    Availability
    FromAvailable
    ToDeprecated
    +
    +
    + +
    + +
    +
    ASAbstractLayoutController.h
    + +
    +
    Added ASAbstractLayoutController (Unavailable)
    +
    + +
    + +
    +
    ASAsciiArtBoxCreator.h
    + +
    +
    Removed ASLayoutableAsciiArtProtocol
    +
    Removed -[ASLayoutableAsciiArtProtocol asciiArtString]
    +
    Removed -[ASLayoutableAsciiArtProtocol asciiArtName]
    +
    + + +
    +
    Added ASLayoutElementAsciiArtProtocol
    +
    Added -[ASLayoutElementAsciiArtProtocol asciiArtString]
    +
    Added -[ASLayoutElementAsciiArtProtocol asciiArtName]
    +
    + +
    + +
    +
    ASAvailability.h
    + +
    +
    Added #def kCFCoreFoundationVersionNumber_iOS_10_0
    +
    Added #def AS_AT_LEAST_IOS10
    +
    + +
    + +
    +
    ASBaseDefines.h
    + +
    +
    Added #def ASDISPLAYNODE_DEPRECATED_MSG
    +
    Added #def AS_UNAVAILABLE
    +
    Added #def AS_WARN_UNUSED_RESULT
    +
    Added #def ASOVERLOADABLE
    +
    + +
    + +
    +
    ASBasicImageDownloader.h
    + +
    +
    Modified ASBasicImageDownloader
    + + + + +
    Protocols
    FromASImageDownloaderProtocolDeprecated, ASImageDownloaderProtocol
    ToASImageDownloaderProtocol
    +
    +
    + +
    + +
    +
    ASButtonNode.h
    + +
    +
    Added ASButtonNodeImageAlignment
    +
    Added ASButtonNodeImageAlignmentBeginning
    +
    Added ASButtonNodeImageAlignmentEnd
    +
    Added ASButtonNode.imageAlignment
    +
    + +
    + +
    +
    ASCellNode.h
    + +
    +
    Added ASCellNode.supplementaryElementKind
    +
    Added ASCellNode.layoutAttributes
    +
    Added ASCellNode.indexPath
    +
    Added ASCellNode.owningNode
    +
    Added ASCellNode (Unavailable)
    +
    + +
    + +
    +
    ASCollectionNode.h
    + +
    +
    Added ASCollectionNode.allowsSelection
    +
    Added ASCollectionNode.allowsMultipleSelection
    +
    Added -[ASCollectionNode scrollToItemAtIndexPath:atScrollPosition:animated:]
    +
    Added -[ASCollectionNode registerSupplementaryNodeOfKind:]
    +
    Added -[ASCollectionNode performBatchAnimated:updates:completion:]
    +
    Added -[ASCollectionNode performBatchUpdates:completion:]
    +
    Added -[ASCollectionNode waitUntilAllUpdatesAreCommitted]
    +
    Added -[ASCollectionNode insertSections:]
    +
    Added -[ASCollectionNode deleteSections:]
    +
    Added -[ASCollectionNode reloadSections:]
    +
    Added -[ASCollectionNode moveSection:toSection:]
    +
    Added -[ASCollectionNode insertItemsAtIndexPaths:]
    +
    Added -[ASCollectionNode deleteItemsAtIndexPaths:]
    +
    Added -[ASCollectionNode reloadItemsAtIndexPaths:]
    +
    Added -[ASCollectionNode moveItemAtIndexPath:toIndexPath:]
    +
    Added -[ASCollectionNode relayoutItems]
    +
    Added ASCollectionNode.indexPathsForSelectedItems
    +
    Added -[ASCollectionNode selectItemAtIndexPath:animated:scrollPosition:]
    +
    Added -[ASCollectionNode deselectItemAtIndexPath:animated:]
    +
    Added -[ASCollectionNode numberOfItemsInSection:]
    +
    Added ASCollectionNode.numberOfSections
    +
    Added ASCollectionNode.visibleNodes
    +
    Added -[ASCollectionNode nodeForItemAtIndexPath:]
    +
    Added -[ASCollectionNode indexPathForNode:]
    +
    Added ASCollectionNode.indexPathsForVisibleItems
    +
    Added -[ASCollectionNode indexPathForItemAtPoint:]
    +
    Added -[ASCollectionNode cellForItemAtIndexPath:]
    +
    Added -[ASCollectionNode contextForSection:]
    +
    Added ASCollectionNode (Deprecated)
    +
    Added -[ASCollectionDataSource collectionNode:numberOfItemsInSection:]
    +
    Added -[ASCollectionDataSource numberOfSectionsInCollectionNode:]
    +
    Added -[ASCollectionDataSource collectionNode:nodeBlockForItemAtIndexPath:]
    +
    Added -[ASCollectionDataSource collectionNode:nodeForItemAtIndexPath:]
    +
    Added -[ASCollectionDataSource collectionNode:nodeForSupplementaryElementOfKind:atIndexPath:]
    +
    Added -[ASCollectionDataSource collectionNode:contextForSection:]
    +
    Added -[ASCollectionDelegate collectionNode:constrainedSizeForItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:willDisplayItemWithNode:]
    +
    Added -[ASCollectionDelegate collectionNode:didEndDisplayingItemWithNode:]
    +
    Added -[ASCollectionDelegate collectionNode:willDisplaySupplementaryElementWithNode:]
    +
    Added -[ASCollectionDelegate collectionNode:didEndDisplayingSupplementaryElementWithNode:]
    +
    Added -[ASCollectionDelegate collectionNode:shouldHighlightItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:didHighlightItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:didUnhighlightItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:shouldSelectItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:shouldDeselectItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:didSelectItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:didDeselectItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:shouldShowMenuForItemAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionNode:canPerformAction:forItemAtIndexPath:sender:]
    +
    Added -[ASCollectionDelegate collectionNode:performAction:forItemAtIndexPath:sender:]
    +
    Added -[ASCollectionDelegate collectionNode:willBeginBatchFetchWithContext:]
    +
    Added -[ASCollectionDelegate shouldBatchFetchForCollectionNode:]
    +
    Added -[ASCollectionDelegate collectionView:constrainedSizeForNodeAtIndexPath:]
    +
    Added -[ASCollectionDelegate collectionView:willDisplayNode:forItemAtIndexPath:]
    +
    + + +
    +
    Modified -[ASCollectionNode reloadDataImmediately]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -reloadData / -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreCommitted instead.
    +
    +
    Modified ASCollectionDataSource
    + + + + +
    HeaderProtocols
    FromASCollectionView.hASCommonCollectionViewDataSource
    ToASCollectionNode.hASCommonCollectionDataSource
    +
    +
    Modified -[ASCollectionDataSource collectionView:nodeForItemAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    Modified -[ASCollectionDataSource collectionView:nodeBlockForItemAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    Modified -[ASCollectionDataSource collectionView:nodeForSupplementaryElementOfKind:atIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    Modified ASCollectionDelegate
    + + + + +
    HeaderProtocols
    FromASCollectionView.hASCommonCollectionViewDelegate, NSObject
    ToASCollectionNode.hASCommonCollectionDelegate, NSObject
    +
    +
    Modified -[ASCollectionDelegate collectionView:didEndDisplayingNode:forItemAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    Modified -[ASCollectionDelegate collectionView:willBeginBatchFetchWithContext:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    Modified -[ASCollectionDelegate shouldBatchFetchForCollectionView:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    Modified -[ASCollectionDelegate collectionView:willDisplayNodeForItemAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    + +
    + +
    +
    ASCollectionNode+Beta.h
    + +
    +
    Modified -[ASCollectionNode beginUpdates]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -performBatchUpdates:completion: instead.
    +
    +
    Modified -[ASCollectionNode endUpdatesAnimated:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -performBatchUpdates:completion: instead.
    +
    +
    Modified -[ASCollectionNode endUpdatesAnimated:completion:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -performBatchUpdates:completion: instead.
    +
    +
    + +
    + +
    +
    ASCollectionView.h
    + +
    +
    Removed -[ASCollectionView clearContents]
    +
    Removed -[ASCollectionView clearFetchedData]
    +
    Removed #def ASCollectionViewDataSource
    +
    Removed -[ASCollectionDataSource collectionView:constrainedSizeForNodeAtIndexPath:]
    +
    Removed #def ASCollectionViewDelegate
    +
    Removed -[ASCollectionDelegate collectionView:didEndDisplayingNodeForItemAtIndexPath:]
    +
    Removed -[ASCollectionView initWithFrame:collectionViewLayout:asyncDataFetching:]
    +
    + + +
    +
    Added -[ASCollectionView contextForSection:]
    +
    Added -[ASCollectionView cellForItemAtIndexPath:]
    +
    Added -[ASCollectionView scrollToItemAtIndexPath:atScrollPosition:animated:]
    +
    Added -[ASCollectionView selectItemAtIndexPath:animated:scrollPosition:]
    +
    Added ASCollectionView.indexPathsForVisibleItems
    +
    Added ASCollectionView.indexPathsForSelectedItems
    +
    Added ASCollectionViewDataSource
    +
    Added ASCollectionViewDelegate
    +
    + + +
    +
    Modified -[ASCollectionView initWithCollectionViewLayout:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedPlease use ASCollectionNode instead of ASCollectionView.
    +
    +
    Modified -[ASCollectionView initWithFrame:collectionViewLayout:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedPlease use ASCollectionNode instead of ASCollectionView.
    +
    +
    Modified -[ASCollectionView tuningParametersForRangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView setTuningParameters:forRangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView tuningParametersForRangeMode:rangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView setTuningParameters:forRangeMode:rangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView performBatchAnimated:updates:completion:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView performBatchUpdates:completion:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView reloadDataWithCompletion:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView reloadData]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView reloadDataImmediately]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreCommitted instead.
    +
    +
    Modified -[ASCollectionView relayoutItems]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView waitUntilAllUpdatesAreCommitted]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView registerSupplementaryNodeOfKind:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView insertSections:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView deleteSections:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView reloadSections:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView moveSection:toSection:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView insertItemsAtIndexPaths:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView deleteItemsAtIndexPaths:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView reloadItemsAtIndexPaths:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified ASCollectionDataSource
    + + + + +
    HeaderProtocols
    FromASCollectionView.hASCommonCollectionViewDataSource
    ToASCollectionNode.hASCommonCollectionDataSource
    +
    +
    Modified -[ASCollectionView moveItemAtIndexPath:toIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView calculatedSizeForNodeAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedCall -calculatedSize on the node of interest instead.
    +
    +
    Modified -[ASCollectionView visibleNodes]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified -[ASCollectionView indexPathForNode:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode method instead.
    +
    +
    Modified ASCollectionDelegate
    + + + + +
    HeaderProtocols
    FromASCollectionView.hASCommonCollectionViewDelegate, NSObject
    ToASCollectionNode.hASCommonCollectionDelegate, NSObject
    +
    +
    + +
    + +
    +
    ASCollectionViewFlowLayoutInspector.h
    + +
    +
    Added -[ASCollectionViewLayoutInspecting scrollableDirections]
    +
    + + +
    +
    Modified -[ASCollectionViewLayoutInspecting collectionView:numberOfSectionsForSupplementaryNodeOfKind:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASCollectionNode's method instead.
    +
    +
    + +
    + +
    +
    ASCollectionViewProtocols.h
    + +
    +
    Removed ASCommonCollectionViewDataSource
    +
    Removed -[ASCommonCollectionViewDataSource collectionView:numberOfItemsInSection:]
    +
    Removed -[ASCommonCollectionViewDataSource numberOfSectionsInCollectionView:]
    +
    Removed -[ASCommonCollectionViewDataSource collectionView:viewForSupplementaryElementOfKind:atIndexPath:]
    +
    Removed ASCommonCollectionViewDelegate
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:transitionLayoutForOldLayout:newLayout:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:shouldHighlightItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:didHighlightItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:didUnhighlightItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:shouldSelectItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:didSelectItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:shouldDeselectItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:didDeselectItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:shouldShowMenuForItemAtIndexPath:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:canPerformAction:forItemAtIndexPath:withSender:]
    +
    Removed -[ASCommonCollectionViewDelegate collectionView:performAction:forItemAtIndexPath:withSender:]
    +
    + + +
    +
    Added ASCommonCollectionDataSource
    +
    Added -[ASCommonCollectionDataSource collectionView:numberOfItemsInSection:]
    +
    Added -[ASCommonCollectionDataSource numberOfSectionsInCollectionView:]
    +
    Added -[ASCommonCollectionDataSource collectionView:viewForSupplementaryElementOfKind:atIndexPath:]
    +
    Added ASCommonCollectionDelegate
    +
    Added -[ASCommonCollectionDelegate collectionView:transitionLayoutForOldLayout:newLayout:]
    +
    Added -[ASCommonCollectionDelegate collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:shouldHighlightItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:didHighlightItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:didUnhighlightItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:shouldSelectItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:didSelectItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:shouldDeselectItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:didDeselectItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:shouldShowMenuForItemAtIndexPath:]
    +
    Added -[ASCommonCollectionDelegate collectionView:canPerformAction:forItemAtIndexPath:withSender:]
    +
    Added -[ASCommonCollectionDelegate collectionView:performAction:forItemAtIndexPath:withSender:]
    +
    + +
    + +
    +
    ASDataController.h
    + +
    +
    Added -[ASDataController initWithDataSource:]
    +
    Added -[ASDataController completedNumberOfSections]
    +
    Added -[ASDataController completedNumberOfRowsInSection:]
    +
    Added -[ASDataController nodeAtCompletedIndexPath:]
    +
    Added -[ASDataController completedIndexPathForNode:]
    +
    Added -[ASDataController moveCompletedNodeAtIndexPath:toIndexPath:]
    +
    + +
    + +
    +
    ASDimension.h
    + +
    +
    Removed ASRelativeDimensionTypePercent
    +
    Removed ASRelativeDimension
    +
    Removed ASRelativeDimensionUnconstrained
    +
    Removed #def isValidForLayout
    +
    Removed ASRelativeDimensionMakeWithPoints()
    +
    Removed ASRelativeDimensionMakeWithPercent()
    +
    Removed ASRelativeDimensionCopy()
    +
    Removed ASRelativeDimensionEqualToRelativeDimension()
    +
    Removed NSStringFromASRelativeDimension()
    +
    Removed ASRelativeDimensionResolve()
    +
    + + +
    +
    Added ASPointsValidForLayout()
    +
    Added ASIsCGSizeValidForLayout()
    +
    Added ASPointsValidForSize()
    +
    Added ASIsCGSizeValidForSize()
    +
    Added ASDimensionUnit
    +
    Added ASDimensionUnitAuto
    +
    Added ASDimensionUnitPoints
    +
    Added ASDimensionUnitFraction
    +
    Added ASDimension
    +
    Added ASLayoutElementSize
    +
    Added ASDimensionAuto
    +
    Added ASDimensionMake()
    +
    Added ASDimensionMakeWithPoints()
    +
    Added ASDimensionMakeWithFraction()
    +
    Added ASDimensionEqualToDimension()
    +
    Added NSStringFromASDimension()
    +
    Added ASDimensionResolve()
    +
    Added NSNumber (ASDimension)
    +
    Added NSNumber.as_pointDimension
    +
    Added NSNumber.as_fractionDimension
    +
    Added ASLayoutSize
    +
    Added ASLayoutSizeAuto
    +
    Added ASLayoutSizeMake()
    +
    Added NSStringFromASLayoutSize()
    +
    Added ASLayoutElementSizeMake()
    +
    Added ASLayoutElementSizeMakeFromCGSize()
    +
    Added ASLayoutElementSizeEqualToLayoutElementSize()
    +
    Added NSStringFromASLayoutElementSize()
    +
    Added ASLayoutElementSizeResolveAutoSize()
    +
    Added ASLayoutElementSizeResolve()
    +
    Added ASRelativeDimensionTypeAuto
    +
    Added ASRelativeDimensionTypeFraction
    +
    Added #def ASRelativeDimension
    +
    Added #def ASRelativeSize
    +
    Added #def ASRelativeDimensionMakeWithPoints
    +
    Added #def ASRelativeDimensionMakeWithFraction
    +
    Added ASRelativeSizeMakeWithFraction()
    +
    Added ASRelativeSizeRangeMakeWithExactFraction()
    +
    + + +
    +
    Modified ASSizeRangeMakeExactSize()
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASSizeRangeMake instead.
    +
    +
    Modified ASRelativeSizeRange
    + + + + +
    Header
    FromASRelativeSize.h
    ToASDimension.h
    +
    +
    Modified ASRelativeSizeRangeUnconstrained
    + + + + +
    Header
    FromASRelativeSize.h
    ToASDimension.h
    +
    +
    Modified ASRelativeDimensionMake()
    + + + + +
    Availability
    FromAvailable
    ToDeprecated
    +
    +
    Modified ASRelativeSizeMake()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeMakeWithCGSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeEqualToRelativeSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified NSStringFromASRelativeSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMake()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMakeWithExactRelativeSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMakeWithExactCGSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMakeWithExactRelativeDimensions()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeEqualToRelativeSizeRange()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeResolve()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    + +
    + +
    +
    ASDisplayNode.h
    + +
    +
    Removed ASInterfaceStateFetchData
    +
    Removed -[ASDisplayNode measureWithSizeRange:]
    +
    Removed ASDisplayNode (ASDisplayNodeAsyncTransactionContainer)
    +
    Removed -[ASDisplayNode reclaimMemory]
    +
    Removed -[ASDisplayNode recursivelyReclaimMemory]
    +
    Removed ASDisplayNode.placeholderFadesOut
    +
    + + +
    +
    Added ASInterfaceStatePreload
    +
    Added -[ASDisplayNode onDidLoad:]
    +
    Added ASDisplayNode.visible
    +
    Added ASDisplayNode.inPreloadState
    +
    Added ASDisplayNode.inDisplayState
    +
    Added -[ASDisplayNode layoutThatFits:]
    +
    Added ASDisplayNode.allowsGroupOpacity
    +
    Added ASDisplayNode (LayoutTransitioning)
    +
    Added ASDisplayNode.defaultLayoutTransitionDuration
    +
    Added ASDisplayNode.defaultLayoutTransitionDelay
    +
    Added ASDisplayNode.defaultLayoutTransitionOptions
    +
    Added -[ASDisplayNode cancelLayoutTransition]
    +
    Added ASDisplayNode (AutomaticSubnodeManagement)
    +
    Added ASDisplayNode.automaticallyManagesSubnodes
    +
    Added ASDisplayNode (ASAsyncTransactionContainer)
    +
    + + +
    +
    Modified ASDisplayNode
    + + + + +
    Protocols
    FromASLayoutable
    ToASLayoutElement
    +
    +
    Modified ASDisplayNode (Debugging)
    + + + + +
    Protocols
    FromASLayoutableAsciiArtProtocol
    ToASLayoutElementAsciiArtProtocol
    +
    +
    Modified ASDisplayNode (Deprecated)
    + + + + +
    Header
    FromASDisplayNode.h
    ToASDisplayNode+Deprecated.h
    +
    +
    + +
    + +
    +
    ASDisplayNode+Beta.h
    + +
    +
    Removed +[ASDisplayNode usesImplicitHierarchyManagement]
    +
    Removed +[ASDisplayNode setUsesImplicitHierarchyManagement:]
    +
    + + +
    +
    Added #def ASDISPLAYNODE_EVENTLOG_CAPACITY
    +
    Added #def ASDISPLAYNODE_EVENTLOG_ENABLE
    +
    Added #def ASDisplayNodeLogEvent
    +
    Added ASDisplayNodePerformanceMeasurementOptions
    +
    Added ASDisplayNodePerformanceMeasurementOptionLayoutSpec
    +
    Added ASDisplayNodePerformanceMeasurementOptionLayoutComputation
    +
    Added ASDisplayNodePerformanceMeasurements
    +
    Added ASDisplayNode.measurementOptions
    +
    Added ASDisplayNode.performanceMeasurements
    +
    + +
    + +
    +
    ASDisplayNode+Deprecated.h
    + +
    +
    Added ASDisplayNode.alignSelf
    +
    Added ASDisplayNode.ascender
    +
    Added ASDisplayNode.descender
    +
    Added ASDisplayNode.flexBasis
    +
    Added ASDisplayNode.flexGrow
    +
    Added ASDisplayNode.flexShrink
    +
    Added ASDisplayNode.layoutPosition
    +
    Added ASDisplayNode.sizeRange
    +
    Added ASDisplayNode.spacingAfter
    +
    Added ASDisplayNode.spacingBefore
    +
    + + +
    +
    Modified ASDisplayNode (Deprecated)
    + + + + +
    Header
    FromASDisplayNode.h
    ToASDisplayNode+Deprecated.h
    +
    +
    Modified ASDisplayNode.name
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse .debugName instead.
    +
    +
    Modified ASDisplayNode.preferredFrameSize
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse .style.preferredSize instead OR set individual values with .style.height and .style.width.
    +
    +
    Modified -[ASDisplayNode measure:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout.
    +
    +
    Modified -[ASDisplayNode visibilityDidChange:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -didEnterVisibleState / -didExitVisibleState instead.
    +
    +
    Modified -[ASDisplayNode visibleStateDidChange:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -didEnterVisibleState / -didExitVisibleState instead.
    +
    +
    Modified -[ASDisplayNode displayStateDidChange:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -didEnterDisplayState / -didExitDisplayState instead.
    +
    +
    Modified -[ASDisplayNode loadStateDidChange:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -didEnterPreloadState / -didExitPreloadState instead.
    +
    +
    Modified -[ASDisplayNode cancelLayoutTransitionsInProgress]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse -cancelLayoutTransition instead.
    +
    +
    Modified ASDisplayNode.usesImplicitHierarchyManagement
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedSet .automaticallyManagesSubnodes instead.
    +
    +
    + +
    + +
    +
    ASDisplayNode+Subclasses.h
    + +
    +
    Added -[ASDisplayNode calculateLayoutThatFits:restrictedToSize:relativeToParentSize:]
    +
    Added -[ASDisplayNode displayWillStartAsynchronously:]
    +
    Added -[ASDisplayNode didEnterVisibleState]
    +
    Added -[ASDisplayNode didExitVisibleState]
    +
    Added -[ASDisplayNode didEnterDisplayState]
    +
    Added -[ASDisplayNode didExitDisplayState]
    +
    Added -[ASDisplayNode didEnterPreloadState]
    +
    Added -[ASDisplayNode didExitPreloadState]
    +
    + +
    + +
    +
    ASDisplayNodeExtras.h
    + +
    +
    Removed ASInterfaceStateIncludesFetchData()
    +
    + + +
    +
    Added ASInterfaceStateIncludesPreload()
    +
    + +
    + +
    +
    ASEditableTextNode.h
    + +
    +
    Added ASEditableTextNode (Unavailable)
    +
    + +
    + +
    +
    ASEnvironment.h
    + +
    +
    Removed -[ASEnvironment supportsUpwardPropagation]
    +
    + + +
    +
    Added NSStringFromASEnvironmentTraitCollection()
    +
    + +
    + +
    +
    ASImageNode.h
    + +
    +
    Added ASImageNode.forcedSize
    +
    Added ASImageNode (Unavailable)
    +
    + +
    + +
    +
    ASImageProtocols.h
    + +
    +
    Removed ASImageDownloaderProtocolDeprecated
    +
    Removed -[ASImageDownloaderProtocolDeprecated downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:]
    +
    Removed ASImageCacheProtocolDeprecated
    +
    Removed -[ASImageCacheProtocolDeprecated fetchCachedImageWithURL:callbackQueue:completion:]
    +
    + +
    + +
    +
    ASLayout.h
    + +
    +
    Removed ASLayout.constrainedSizeRange
    +
    Removed ASLayout.dirty
    +
    Removed -[ASLayout initWithLayoutableObject:constrainedSizeRange:size:position:sublayouts:]
    +
    Removed +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:position:sublayouts:]
    +
    Removed +[ASLayout flattenedLayoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:]
    +
    + + +
    +
    Added ASCalculateRootLayout()
    +
    Added ASCalculateLayout()
    +
    Added ASLayout.layoutElement
    +
    Added -[ASLayout initWithLayoutElement:size:position:sublayouts:]
    +
    Added +[ASLayout layoutWithLayoutElement:size:position:sublayouts:]
    +
    Added +[ASLayout layoutWithLayoutElement:size:sublayouts:]
    +
    Added +[ASLayout layoutWithLayoutElement:size:]
    +
    Added ASLayout (Unavailable)
    +
    Added ASLayout (Deprecated)
    +
    + + +
    +
    Modified -[ASLayout layoutableObject]
    + + + + +
    Availability
    FromAvailable
    ToDeprecated
    +
    +
    Modified +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:]
    + + + + +
    Availability
    FromAvailable
    ToDeprecated
    +
    +
    Modified +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:]
    + + + + +
    Availability
    FromAvailable
    ToDeprecated
    +
    +
    + +
    + +
    +
    ASLayoutable.h
    + +
    +
    Removed ASLayoutableType
    +
    Removed ASLayoutableTypeLayoutSpec
    +
    Removed ASLayoutableTypeDisplayNode
    +
    Removed ASLayoutable
    +
    Removed ASLayoutable.layoutableType
    +
    Removed ASLayoutable.canLayoutAsynchronous
    +
    Removed -[ASLayoutable measureWithSizeRange:]
    +
    Removed ASLayoutable.spacingBefore
    +
    Removed ASLayoutable.spacingAfter
    +
    Removed ASLayoutable.flexGrow
    +
    Removed ASLayoutable.flexShrink
    +
    Removed ASLayoutable.flexBasis
    +
    Removed ASLayoutable.alignSelf
    +
    Removed ASLayoutable.ascender
    +
    Removed ASLayoutable.descender
    +
    Removed ASLayoutable.sizeRange
    +
    Removed ASLayoutable.layoutPosition
    +
    + +
    + +
    +
    ASLayoutableExtensibility.h
    + +
    +
    Removed ASLayoutableExtensibility
    +
    Removed -[ASLayoutableExtensibility setLayoutOptionExtensionBool:atIndex:]
    +
    Removed -[ASLayoutableExtensibility layoutOptionExtensionBoolAtIndex:]
    +
    Removed -[ASLayoutableExtensibility setLayoutOptionExtensionInteger:atIndex:]
    +
    Removed -[ASLayoutableExtensibility layoutOptionExtensionIntegerAtIndex:]
    +
    Removed -[ASLayoutableExtensibility setLayoutOptionExtensionEdgeInsets:atIndex:]
    +
    Removed -[ASLayoutableExtensibility layoutOptionExtensionEdgeInsetsAtIndex:]
    +
    + +
    + +
    +
    ASLayoutablePrivate.h
    + +
    +
    Removed ASLayoutableContextInvalidTransitionID
    +
    Removed ASLayoutableContextDefaultTransitionID
    +
    Removed ASLayoutableContextNull
    +
    Removed ASLayoutableContextIsNull()
    +
    Removed ASLayoutableContextMake()
    +
    Removed ASLayoutableSetCurrentContext()
    +
    Removed ASLayoutableGetCurrentContext()
    +
    Removed ASLayoutableClearCurrentContext()
    +
    Removed ASLayoutablePrivate
    +
    Removed -[ASLayoutablePrivate finalLayoutable]
    +
    Removed ASLayoutablePrivate.isFinalLayoutable
    +
    Removed #def ASEnvironmentLayoutOptionsForwarding
    +
    + + +
    +
    Modified #def ASEnvironmentLayoutExtensibilityForwarding
    + + + + +
    Header
    FromASLayoutablePrivate.h
    ToASLayoutElementPrivate.h
    +
    +
    + +
    + +
    +
    ASLayoutElement.h
    + +
    +
    Added ASLayoutElementParentDimensionUndefined
    +
    Added ASLayoutElementParentSizeUndefined
    +
    Added ASLayoutElementType
    +
    Added ASLayoutElementTypeLayoutSpec
    +
    Added ASLayoutElementTypeDisplayNode
    +
    Added ASLayoutElement
    +
    Added ASLayoutElement.layoutElementType
    +
    Added ASLayoutElement.canLayoutAsynchronous
    +
    Added ASLayoutElement.style
    +
    Added ASLayoutElement.debugName
    +
    Added -[ASLayoutElement layoutThatFits:]
    +
    Added -[ASLayoutElement layoutThatFits:parentSize:]
    +
    Added -[ASLayoutElement calculateLayoutThatFits:]
    +
    Added -[ASLayoutElement calculateLayoutThatFits:restrictedToSize:relativeToParentSize:]
    +
    Added -[ASLayoutElement measureWithSizeRange:]
    +
    Added ASLayoutElementStyleWidthProperty
    +
    Added ASLayoutElementStyleMinWidthProperty
    +
    Added ASLayoutElementStyleMaxWidthProperty
    +
    Added ASLayoutElementStyleHeightProperty
    +
    Added ASLayoutElementStyleMinHeightProperty
    +
    Added ASLayoutElementStyleMaxHeightProperty
    +
    Added ASLayoutElementStyleSpacingBeforeProperty
    +
    Added ASLayoutElementStyleSpacingAfterProperty
    +
    Added ASLayoutElementStyleFlexGrowProperty
    +
    Added ASLayoutElementStyleFlexShrinkProperty
    +
    Added ASLayoutElementStyleFlexBasisProperty
    +
    Added ASLayoutElementStyleAlignSelfProperty
    +
    Added ASLayoutElementStyleAscenderProperty
    +
    Added ASLayoutElementStyleDescenderProperty
    +
    Added ASLayoutElementStyleLayoutPositionProperty
    +
    Added ASLayoutElementStyleDelegate
    +
    Added -[ASLayoutElementStyleDelegate style:propertyDidChange:]
    +
    Added ASLayoutElementStyle
    +
    Added -[ASLayoutElementStyle initWithDelegate:]
    +
    Added ASLayoutElementStyle.delegate
    +
    Added ASLayoutElementStyle.width
    +
    Added ASLayoutElementStyle.height
    +
    Added ASLayoutElementStyle.minHeight
    +
    Added ASLayoutElementStyle.maxHeight
    +
    Added ASLayoutElementStyle.minWidth
    +
    Added ASLayoutElementStyle.maxWidth
    +
    Added ASLayoutElementStyle.preferredSize
    +
    Added ASLayoutElementStyle.minSize
    +
    Added ASLayoutElementStyle.maxSize
    +
    Added ASLayoutElementStyle.preferredLayoutSize
    +
    Added ASLayoutElementStyle.minLayoutSize
    +
    Added ASLayoutElementStyle.maxLayoutSize
    +
    Added ASLayoutElementStylability
    +
    Added -[ASLayoutElementStylability styledWithBlock:]
    +
    + +
    + +
    +
    ASLayoutElementExtensibility.h
    + +
    +
    Added ASLayoutElementExtensibility
    +
    Added -[ASLayoutElementExtensibility setLayoutOptionExtensionBool:atIndex:]
    +
    Added -[ASLayoutElementExtensibility layoutOptionExtensionBoolAtIndex:]
    +
    Added -[ASLayoutElementExtensibility setLayoutOptionExtensionInteger:atIndex:]
    +
    Added -[ASLayoutElementExtensibility layoutOptionExtensionIntegerAtIndex:]
    +
    Added -[ASLayoutElementExtensibility setLayoutOptionExtensionEdgeInsets:atIndex:]
    +
    Added -[ASLayoutElementExtensibility layoutOptionExtensionEdgeInsetsAtIndex:]
    +
    + +
    + +
    +
    ASLayoutElementPrivate.h
    + +
    +
    Added ASLayoutElementContextInvalidTransitionID
    +
    Added ASLayoutElementContextDefaultTransitionID
    +
    Added ASLayoutElementContextNull
    +
    Added ASLayoutElementContextIsNull()
    +
    Added ASLayoutElementContextMake()
    +
    Added ASLayoutElementSetCurrentContext()
    +
    Added ASLayoutElementGetCurrentContext()
    +
    Added ASLayoutElementClearCurrentContext()
    +
    Added ASLayoutElementPrivate
    +
    Added -[ASLayoutElementPrivate finalLayoutElement]
    +
    Added ASLayoutElementPrivate.isFinalLayoutElement
    +
    Added #def ASLayoutElementStyleForwardingDeclaration
    +
    Added #def ASLayoutElementStyleForwarding
    +
    + + +
    +
    Modified #def ASEnvironmentLayoutExtensibilityForwarding
    + + + + +
    Header
    FromASLayoutablePrivate.h
    ToASLayoutElementPrivate.h
    +
    +
    + +
    + +
    +
    ASLayoutRangeType.h
    + +
    +
    Removed ASLayoutRangeTypeFetchData
    +
    + + +
    +
    Added ASLayoutRangeTypePreload
    +
    + +
    + +
    +
    ASLayoutSpec.h
    + +
    +
    Removed -[ASLayoutSpec init]
    +
    Removed -[ASLayoutSpec setChild:forIndex:]
    +
    Removed -[ASLayoutSpec childForIndex:]
    +
    + + +
    +
    Added ASWrapperLayoutSpec
    +
    Added +[ASWrapperLayoutSpec wrapperWithLayoutElement:]
    +
    Added +[ASWrapperLayoutSpec wrapperWithLayoutElements:]
    +
    Added -[ASWrapperLayoutSpec initWithLayoutElement:]
    +
    Added -[ASWrapperLayoutSpec initWithLayoutElements:]
    +
    Added ASLayoutSpec (Deprecated)
    +
    Added ASLayoutSpec.alignSelf
    +
    Added ASLayoutSpec.ascender
    +
    Added ASLayoutSpec.descender
    +
    Added ASLayoutSpec.flexBasis
    +
    Added ASLayoutSpec.flexGrow
    +
    Added ASLayoutSpec.flexShrink
    +
    Added ASLayoutSpec.layoutPosition
    +
    Added ASLayoutSpec.sizeRange
    +
    Added ASLayoutSpec.spacingAfter
    +
    Added ASLayoutSpec.spacingBefore
    +
    + + +
    +
    Modified ASLayoutSpec
    + + + + +
    Protocols
    FromASLayoutable
    ToASLayoutElement
    +
    +
    Modified ASLayoutSpec (Debugging)
    + + + + +
    Protocols
    FromASLayoutableAsciiArtProtocol
    ToASLayoutElementAsciiArtProtocol
    +
    +
    + +
    + +
    +
    ASLog.h
    + +
    +
    Added #def ASProfilingSignpost
    +
    Added #def ASProfilingSignpostStart
    +
    Added #def ASProfilingSignpostEnd
    +
    + +
    + +
    +
    ASMapNode.h
    + +
    +
    Added ASMapNode.imageForStaticMapAnnotationBlock
    +
    + +
    + +
    +
    ASObjectDescriptionHelpers.h
    + +
    +
    Added ASDebugDescriptionProvider
    +
    Added -[ASDebugDescriptionProvider propertiesForDebugDescription]
    +
    Added ASDescriptionProvider
    +
    Added -[ASDescriptionProvider propertiesForDescription]
    +
    Added ASObjectDescriptionMakeWithoutObject()
    +
    Added ASObjectDescriptionMake()
    +
    Added ASObjectDescriptionMakeTiny()
    +
    Added ASStringWithQuotesIfMultiword()
    +
    + +
    + +
    +
    ASPagerNode.h
    + +
    +
    Removed -[ASPagerDataSource pagerNode:constrainedSizeForNodeAtIndexPath:]
    +
    + + +
    +
    Added -[ASPagerDelegate pagerNode:constrainedSizeForNodeAtIndex:]
    +
    + +
    + +
    +
    ASRangeController.h
    + +
    +
    Added -[ASRangeControllerDataSource nameForRangeControllerDataSource]
    +
    + +
    + +
    +
    ASRelativeLayoutSpec.h
    + +
    +
    Added ASRelativeLayoutSpecPositionNone
    +
    + +
    + +
    +
    ASRelativeSize.h
    + +
    +
    Removed ASRelativeSize
    +
    Removed ASRelativeSizeMakeWithPercent()
    +
    Removed ASRelativeSizeResolveSize()
    +
    Removed ASRelativeSizeRangeMakeWithExactPercent()
    +
    + + +
    +
    Modified ASRelativeSizeRange
    + + + + +
    Header
    FromASRelativeSize.h
    ToASDimension.h
    +
    +
    Modified ASRelativeSizeRangeUnconstrained
    + + + + +
    Header
    FromASRelativeSize.h
    ToASDimension.h
    +
    +
    Modified ASRelativeSizeMake()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeMakeWithCGSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeEqualToRelativeSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified NSStringFromASRelativeSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMake()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMakeWithExactRelativeSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMakeWithExactCGSize()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeMakeWithExactRelativeDimensions()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeEqualToRelativeSizeRange()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    Modified ASRelativeSizeRangeResolve()
    + + + + +
    HeaderAvailability
    FromASRelativeSize.hAvailable
    ToASDimension.hDeprecated
    +
    +
    + +
    + +
    +
    ASRunLoopQueue.h
    + +
    +
    Added ASRunLoopQueue.ensureExclusiveMembership
    +
    Added ASDeallocQueue
    +
    Added +[ASDeallocQueue sharedDeallocationQueue]
    +
    Added -[ASDeallocQueue releaseObjectInBackground:]
    +
    + +
    + +
    +
    ASSectionContext.h
    + +
    +
    Added ASSectionContext
    +
    Added ASSectionContext.sectionName
    +
    Added ASSectionContext.collectionView
    +
    + +
    + +
    +
    ASStackLayoutable.h
    + +
    +
    Removed ASStackLayoutable
    +
    Removed ASStackLayoutable.spacingBefore
    +
    Removed ASStackLayoutable.spacingAfter
    +
    Removed ASStackLayoutable.flexGrow
    +
    Removed ASStackLayoutable.flexShrink
    +
    Removed ASStackLayoutable.flexBasis
    +
    Removed ASStackLayoutable.alignSelf
    +
    Removed ASStackLayoutable.ascender
    +
    Removed ASStackLayoutable.descender
    +
    + +
    + +
    +
    ASStackLayoutElement.h
    + +
    +
    Added ASStackLayoutElement
    +
    Added ASStackLayoutElement.spacingBefore
    +
    Added ASStackLayoutElement.spacingAfter
    +
    Added ASStackLayoutElement.flexGrow
    +
    Added ASStackLayoutElement.flexShrink
    +
    Added ASStackLayoutElement.flexBasis
    +
    Added ASStackLayoutElement.alignSelf
    +
    Added ASStackLayoutElement.ascender
    +
    Added ASStackLayoutElement.descender
    +
    + +
    + +
    +
    ASStaticLayoutable.h
    + +
    +
    Removed ASStaticLayoutable
    +
    Removed ASStaticLayoutable.sizeRange
    +
    Removed ASStaticLayoutable.layoutPosition
    +
    + +
    + +
    +
    ASStaticLayoutSpec.h
    + +
    +
    Modified ASStaticLayoutSpec
    + + + + +
    HeaderSuperclass
    FromASStaticLayoutSpec.hASLayoutSpec
    ToASAbsoluteLayoutSpec.hASAbsoluteLayoutSpec
    +
    +
    + +
    + +
    +
    ASTableNode.h
    + +
    +
    Added ASTableNode.allowsSelection
    +
    Added ASTableNode.allowsSelectionDuringEditing
    +
    Added ASTableNode.allowsMultipleSelection
    +
    Added ASTableNode.allowsMultipleSelectionDuringEditing
    +
    Added -[ASTableNode tuningParametersForRangeType:]
    +
    Added -[ASTableNode setTuningParameters:forRangeType:]
    +
    Added -[ASTableNode tuningParametersForRangeMode:rangeType:]
    +
    Added -[ASTableNode setTuningParameters:forRangeMode:rangeType:]
    +
    Added -[ASTableNode scrollToRowAtIndexPath:atScrollPosition:animated:]
    +
    Added -[ASTableNode reloadDataWithCompletion:]
    +
    Added -[ASTableNode reloadData]
    +
    Added -[ASTableNode relayoutItems]
    +
    Added -[ASTableNode performBatchAnimated:updates:completion:]
    +
    Added -[ASTableNode performBatchUpdates:completion:]
    +
    Added -[ASTableNode waitUntilAllUpdatesAreCommitted]
    +
    Added -[ASTableNode insertSections:withRowAnimation:]
    +
    Added -[ASTableNode deleteSections:withRowAnimation:]
    +
    Added -[ASTableNode reloadSections:withRowAnimation:]
    +
    Added -[ASTableNode moveSection:toSection:]
    +
    Added -[ASTableNode insertRowsAtIndexPaths:withRowAnimation:]
    +
    Added -[ASTableNode deleteRowsAtIndexPaths:withRowAnimation:]
    +
    Added -[ASTableNode reloadRowsAtIndexPaths:withRowAnimation:]
    +
    Added -[ASTableNode moveRowAtIndexPath:toIndexPath:]
    +
    Added -[ASTableNode selectRowAtIndexPath:animated:scrollPosition:]
    +
    Added -[ASTableNode deselectRowAtIndexPath:animated:]
    +
    Added -[ASTableNode numberOfRowsInSection:]
    +
    Added ASTableNode.numberOfSections
    +
    Added ASTableNode.visibleNodes
    +
    Added -[ASTableNode nodeForRowAtIndexPath:]
    +
    Added -[ASTableNode indexPathForNode:]
    +
    Added -[ASTableNode rectForRowAtIndexPath:]
    +
    Added -[ASTableNode cellForRowAtIndexPath:]
    +
    Added ASTableNode.indexPathForSelectedRow
    +
    Added ASTableNode.indexPathsForSelectedRows
    +
    Added -[ASTableNode indexPathForRowAtPoint:]
    +
    Added -[ASTableNode indexPathsForRowsInRect:]
    +
    Added -[ASTableNode indexPathsForVisibleRows]
    +
    Added -[ASTableDataSource numberOfSectionsInTableNode:]
    +
    Added -[ASTableDataSource tableNode:numberOfRowsInSection:]
    +
    Added -[ASTableDataSource tableNode:nodeBlockForRowAtIndexPath:]
    +
    Added -[ASTableDataSource tableNode:nodeForRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:willDisplayRowWithNode:]
    +
    Added -[ASTableDelegate tableNode:didEndDisplayingRowWithNode:]
    +
    Added -[ASTableDelegate tableNode:willSelectRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:didSelectRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:willDeselectRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:didDeselectRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:shouldHighlightRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:didHighlightRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:didUnhighlightRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:shouldShowMenuForRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:canPerformAction:forRowAtIndexPath:withSender:]
    +
    Added -[ASTableDelegate tableNode:performAction:forRowAtIndexPath:withSender:]
    +
    Added -[ASTableDelegate tableNode:constrainedSizeForRowAtIndexPath:]
    +
    Added -[ASTableDelegate tableNode:willBeginBatchFetchWithContext:]
    +
    Added -[ASTableDelegate shouldBatchFetchForTableNode:]
    +
    Added -[ASTableDelegate tableView:willDisplayNode:forRowAtIndexPath:]
    +
    + + +
    +
    Modified ASTableDataSource
    + + + + +
    HeaderProtocols
    FromASTableView.hASCommonTableViewDataSource, NSObject
    ToASTableNode.hASCommonTableDataSource, NSObject
    +
    +
    Modified -[ASTableDataSource tableView:nodeForRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    Modified -[ASTableDataSource tableView:nodeBlockForRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    Modified ASTableDelegate
    + + + + +
    Header
    FromASTableView.h
    ToASTableNode.h
    +
    +
    Modified -[ASTableDelegate tableView:didEndDisplayingNode:forRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    Modified -[ASTableDelegate tableView:willBeginBatchFetchWithContext:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    Modified -[ASTableDelegate shouldBatchFetchForTableView:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    Modified -[ASTableDelegate tableView:constrainedSizeForRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    Modified -[ASTableDelegate tableView:willDisplayNodeForRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's method instead.
    +
    +
    + +
    + +
    +
    ASTableView.h
    + +
    +
    Removed -[ASTableDelegate tableView:didEndDisplayingNodeForRowAtIndexPath:]
    +
    Removed -[ASTableView initWithFrame:style:asyncDataFetching:]
    +
    + + +
    +
    Added ASTableView.tableNode
    +
    Added -[ASTableView cellForRowAtIndexPath:]
    +
    Added -[ASTableView scrollToRowAtIndexPath:atScrollPosition:animated:]
    +
    Added -[ASTableView selectRowAtIndexPath:animated:scrollPosition:]
    +
    Added ASTableView.indexPathForSelectedRow
    +
    Added ASTableView.indexPathsForSelectedRows
    +
    Added ASTableView.indexPathsForVisibleRows
    +
    Added -[ASTableView indexPathForRowAtPoint:]
    +
    Added -[ASTableView indexPathsForRowsInRect:]
    +
    + + +
    +
    Modified -[ASTableView initWithFrame:style:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedPlease use ASTableNode instead of ASTableView.
    +
    +
    Modified -[ASTableView tuningParametersForRangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView setTuningParameters:forRangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView tuningParametersForRangeMode:rangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView setTuningParameters:forRangeMode:rangeType:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView visibleNodes]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView indexPathForNode:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView reloadDataWithCompletion:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView reloadData]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView reloadDataImmediately]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's reloadDataWithCompletion: followed by ASTableNode's -waitUntilAllUpdatesAreCommitted instead.
    +
    +
    Modified -[ASTableView relayoutItems]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView beginUpdates]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's -performBatchUpdates:completion: instead.
    +
    +
    Modified -[ASTableView endUpdates]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's -performBatchUpdates:completion: instead.
    +
    +
    Modified -[ASTableView endUpdatesAnimated:completion:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode's -performBatchUpdates:completion: instead.
    +
    +
    Modified -[ASTableView waitUntilAllUpdatesAreCommitted]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView insertSections:withRowAnimation:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView deleteSections:withRowAnimation:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView reloadSections:withRowAnimation:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView moveSection:toSection:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView insertRowsAtIndexPaths:withRowAnimation:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView deleteRowsAtIndexPaths:withRowAnimation:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView reloadRowsAtIndexPaths:withRowAnimation:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView moveRowAtIndexPath:toIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse ASTableNode method instead.
    +
    +
    Modified -[ASTableView clearContents]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedYou should not call this method directly. Intead, rely on the Interstate State callback methods.
    +
    +
    Modified -[ASTableView clearFetchedData]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedYou should not call this method directly. Intead, rely on the Interstate State callback methods.
    +
    +
    Modified ASTableViewDataSource
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedRenamed to ASTableDataSource.
    +
    +
    Modified ASTableViewDelegate
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedRenamed to ASTableDelegate.
    +
    +
    Modified ASTableDataSource
    + + + + +
    HeaderProtocols
    FromASTableView.hASCommonTableViewDataSource, NSObject
    ToASTableNode.hASCommonTableDataSource, NSObject
    +
    +
    Modified ASTableDelegate
    + + + + +
    Header
    FromASTableView.h
    ToASTableNode.h
    +
    +
    + +
    + +
    +
    ASTableViewProtocols.h
    + +
    +
    Removed ASCommonTableViewDataSource
    +
    Removed -[ASCommonTableViewDataSource tableView:numberOfRowsInSection:]
    +
    Removed -[ASCommonTableViewDataSource numberOfSectionsInTableView:]
    +
    Removed -[ASCommonTableViewDataSource tableView:titleForHeaderInSection:]
    +
    Removed -[ASCommonTableViewDataSource tableView:titleForFooterInSection:]
    +
    Removed -[ASCommonTableViewDataSource tableView:canEditRowAtIndexPath:]
    +
    Removed -[ASCommonTableViewDataSource tableView:canMoveRowAtIndexPath:]
    +
    Removed -[ASCommonTableViewDataSource sectionIndexTitlesForTableView:]
    +
    Removed -[ASCommonTableViewDataSource tableView:sectionForSectionIndexTitle:atIndex:]
    +
    Removed -[ASCommonTableViewDataSource tableView:commitEditingStyle:forRowAtIndexPath:]
    +
    Removed -[ASCommonTableViewDataSource tableView:moveRowAtIndexPath:toIndexPath:]
    +
    + + +
    +
    Added ASCommonTableDataSource
    +
    Added -[ASCommonTableDataSource tableView:numberOfRowsInSection:]
    +
    Added -[ASCommonTableDataSource numberOfSectionsInTableView:]
    +
    Added -[ASCommonTableDataSource tableView:titleForHeaderInSection:]
    +
    Added -[ASCommonTableDataSource tableView:titleForFooterInSection:]
    +
    Added -[ASCommonTableDataSource tableView:canEditRowAtIndexPath:]
    +
    Added -[ASCommonTableDataSource tableView:canMoveRowAtIndexPath:]
    +
    Added -[ASCommonTableDataSource sectionIndexTitlesForTableView:]
    +
    Added -[ASCommonTableDataSource tableView:sectionForSectionIndexTitle:atIndex:]
    +
    Added -[ASCommonTableDataSource tableView:commitEditingStyle:forRowAtIndexPath:]
    +
    Added -[ASCommonTableDataSource tableView:moveRowAtIndexPath:toIndexPath:]
    +
    + + +
    +
    Modified -[ASCommonTableViewDelegate tableView:shouldHighlightRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:shouldHighlightRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:didHighlightRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:didHighlightRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:didUnhighlightRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:didUnhighlightRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:willSelectRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:willSelectRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:willDeselectRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:willDeselectRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:didSelectRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:didSelectRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:didDeselectRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:didDeselectRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:shouldShowMenuForRowAtIndexPath:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:shouldShowMenuForRowAtIndexPath: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:canPerformAction:forRowAtIndexPath:withSender:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:canPerformAction:forRowAtIndexPath:withSender: instead.
    +
    +
    Modified -[ASCommonTableViewDelegate tableView:performAction:forRowAtIndexPath:withSender:]
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedImplement -tableNode:performAction:forRowAtIndexPath:withSender: instead.
    +
    +
    + +
    + +
    +
    ASTextNode.h
    + +
    +
    Added ASTextNode (Unavailable)
    +
    + + +
    +
    Modified ASTextNode.attributedString
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse .attributedText instead.
    +
    +
    Modified ASTextNode.truncationAttributedString
    + + + + +
    AvailabilityDeprecation Message
    FromAvailablenone
    ToDeprecatedUse .truncationAttributedText instead.
    +
    +
    + +
    + +
    +
    ASTraceEvent.h
    + +
    +
    Added ASTraceEvent
    +
    Added -[ASTraceEvent initWithObject:backtrace:format:arguments:]
    +
    Added ASTraceEvent.backtrace
    +
    Added ASTraceEvent.message
    +
    Added ASTraceEvent.timestamp
    +
    + +
    + +
    +
    ASVideoNode.h
    + +
    +
    Removed -[ASVideoNodeDelegate videoPlaybackDidFinish:]
    +
    Removed -[ASVideoNodeDelegate videoNodeWasTapped:]
    +
    Removed -[ASVideoNodeDelegate videoNode:didPlayToSecond:]
    +
    + + +
    +
    Added ASVideoNode.playerLayer
    +
    Added ASVideoNode (Unavailable)
    +
    + +
    + +
    +
    ASViewController.h
    + +
    +
    Added ASViewController (Unavailable)
    +
    + +
    + +
    +
    ASWeakSet.h
    + +
    +
    Added ASWeakSet
    +
    Added ObjectType
    +
    Added ASWeakSet.empty
    +
    Added -[ASWeakSet containsObject:]
    +
    Added -[ASWeakSet addObject:]
    +
    Added -[ASWeakSet removeObject:]
    +
    Added -[ASWeakSet removeAllObjects]
    +
    Added -[ASWeakSet allObjects]
    +
    Added ASWeakSet.count
    +
    + +
    + +
    +
    AsyncDisplayKit+Debug.h
    + +
    +
    Added ASRangeController (Debugging)
    +
    Added +[ASRangeController setShouldShowRangeDebugOverlay:]
    +
    Added +[ASRangeController shouldShowRangeDebugOverlay]
    +
    Added +[ASRangeController layoutDebugOverlayIfNeeded]
    +
    Added -[ASRangeController addRangeControllerToRangeDebugOverlay]
    +
    Added -[ASRangeController updateRangeController:withScrollableDirections:scrollDirection:rangeMode:displayTuningParameters:preloadTuningParameters:interfaceState:]
    +
    + +
    + +
    +
    CGRect+ASConvenience.h
    + +
    +
    Modified ASDirectionalScreenfulBuffer
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    Modified ASDirectionalScreenfulBufferHorizontal()
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    Modified ASDirectionalScreenfulBufferVertical()
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    Modified CGRectExpandToRangeWithScrollableDirections()
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    + +
    + +
    +
    CoreGraphics+ASConvenience.h
    + +
    +
    Added #def CGFLOAT_EPSILON
    +
    Added ASCGFloatFromString()
    +
    Added ASCGFloatFromNumber()
    +
    Added CGSizeEqualToSizeWithIn()
    +
    + + +
    +
    Modified ASDirectionalScreenfulBuffer
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    Modified ASDirectionalScreenfulBufferHorizontal()
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    Modified ASDirectionalScreenfulBufferVertical()
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    Modified CGRectExpandToRangeWithScrollableDirections()
    + + + + +
    Header
    FromCGRect+ASConvenience.h
    ToCoreGraphics+ASConvenience.h
    +
    +
    + +
    + +
    +
    NSArray+Diffing.h
    + +
    +
    Added NSArray (Diffing)
    +
    Added -[NSArray asdk_diffWithArray:insertions:deletions:]
    +
    Added -[NSArray asdk_diffWithArray:insertions:deletions:compareBlock:]
    +
    + +
    + +
    +
    UIView+ASConvenience.h
    + +
    +
    Added ASDisplayProperties.allowsGroupOpacity
    +
    + +
    + + diff --git a/submodules/AsyncDisplayKit/docs/apidiff/apidiff.css b/submodules/AsyncDisplayKit/docs/apidiff/apidiff.css new file mode 100755 index 0000000000..822ae40118 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/apidiff/apidiff.css @@ -0,0 +1,87 @@ +body { + font: 12px 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; + margin: 0; + padding: 0 2em 2em 2em; +} + +h1 { + margin-top: 30px; + margin-bottom: 30px; + font-size: 28px; + font-weight: bold; +} + +.headerFile { + margin-left: 20px; +} + +.headerName { + margin: 15px 0px 10px -20px; + padding: 4px 4px 4px 20px; + font-weight: bold; + font-size: 120%; + background-color: #f8f8f8; +} + +.differenceGroup { + margin-top: 5px; +} + +.difference { + padding-left: 20px; + font-family: Courier, Consolas, monospace; + font-size: 110%; +} + +.status { + font-style: italic; + font-size: 80%; +} + +.removed { + color: red; +} + +.added { + color: blue; +} + +.modified { + color: #080; +} + +.declaration { + font-family: Courier, Consolas, monospace; +} + +table { + border: 1px #888 solid; + padding: 2px; + border-spacing: 0px; + border-collapse: collapse; + margin-left: 40px; + margin-top: 7px; +} + +td, th { + font-size: 10px; + border: 1px #888 solid; + padding:3px 6px; +} + +th { + font-size: 10px; + text-align: center; + background-color: #eee; +} + +td { + font-size: 90%; + text-align: left; +} + +.message { + margin-left: 20px; + font-style: italic; + color: #888; +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Blocks/ASDisplayNodeContextModifier.html b/submodules/AsyncDisplayKit/docs/appledoc/Blocks/ASDisplayNodeContextModifier.html new file mode 100755 index 0000000000..f52dec6351 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Blocks/ASDisplayNodeContextModifier.html @@ -0,0 +1,128 @@ + + + + + + ASDisplayNodeContextModifier Block Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNodeContextModifier Block Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + + + + + + + +

    Block Definition

    +

    ASDisplayNodeContextModifier

    + + +
    +

    ASDisplayNode will / did render node content in context.

    +
    + + + +typedef void (^ASDisplayNodeContextModifier) (CGContextRef context) + + + + + + + + + + + +
    +

    Declared In

    + ASDisplayNode.h
    +
    + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Blocks/ASDisplayNodeDidLoadBlock.html b/submodules/AsyncDisplayKit/docs/appledoc/Blocks/ASDisplayNodeDidLoadBlock.html new file mode 100755 index 0000000000..eba4dee2c3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Blocks/ASDisplayNodeDidLoadBlock.html @@ -0,0 +1,128 @@ + + + + + + ASDisplayNodeDidLoadBlock Block Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNodeDidLoadBlock Block Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + + + + + + + +

    Block Definition

    +

    ASDisplayNodeDidLoadBlock

    + + +
    +

    ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread.

    +
    + + + +typedef void (^ASDisplayNodeDidLoadBlock) (__kindof ASDisplayNode, * node) + + + + + + + + + + + +
    +

    Declared In

    + ASDisplayNode.h
    +
    + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCellNode+.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCellNode+.html new file mode 100755 index 0000000000..23e0e26c26 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCellNode+.html @@ -0,0 +1,222 @@ + + + + + + ASCellNode() Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + Texture +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCellNode() Category Reference

    + + +
    + + + + +
    Declared inASCellNode+Internal.h
    + + + + + + +
    + + + + + + +
    +
    + +

      layoutAttributes +

    + +
    +
    + +
    + + +
    +

    This could be declared @c copy, but since this is only settable internally, we can ensure +that it’s always safe simply to retain it, and copy if needed. Since @c UICollectionViewLayoutAttributes +is always mutable, @c copy is never “free” like it is for e.g. NSString.

    +
    + + + +
    @property (nonatomic, strong, nullable) UICollectionViewLayoutAttributes *layoutAttributes
    + + + + + + + + + +
    +

    Discussion

    +

    Note: This could be declared @c copy, but since this is only settable internally, we can ensure +that it’s always safe simply to retain it, and copy if needed. Since @c UICollectionViewLayoutAttributes +is always mutable, @c copy is never “free” like it is for e.g. NSString.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCellNode+Internal.h

    +
    + + +
    +
    +
    + +

      supplementaryElementKind +

    + +
    +
    + +
    + + +
    +

    readwrite variant of the readonly public property.

    +
    + + + +
    @property (nonatomic, copy, nullable) NSString *supplementaryElementKind
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode+Internal.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCollectionNode+Deprecated.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCollectionNode+Deprecated.html new file mode 100755 index 0000000000..3a528cffbd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCollectionNode+Deprecated.html @@ -0,0 +1,176 @@ + + + + + + ASCollectionNode(Deprecated) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionNode(Deprecated) Category Reference

    + + +
    + + + + +
    Declared inASCollectionNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – reloadDataImmediately +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes. (Deprecated: This method is deprecated in 2.0. Use @c reloadDataWithCompletion: and +then @c waitUntilAllUpdatesAreCommitted instead.)

    +
    + + + +
    - (void)reloadDataImmediately
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UICollectionView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCollectionView+Deprecated.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCollectionView+Deprecated.html new file mode 100755 index 0000000000..9d4741fee3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASCollectionView+Deprecated.html @@ -0,0 +1,1616 @@ + + + + + + ASCollectionView(Deprecated) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionView(Deprecated) Category Reference

    + + +
    + + + + +
    Declared inASCollectionView.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – initWithCollectionViewLayout: +

    + +
    +
    + +
    + + +
    +

    Initializes an ASCollectionView

    +
    + + + +
    - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout
    + + + +
    +

    Parameters

    + + + + + + + +
    layout

    The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil.

    +
    + + + + + + + +
    +

    Discussion

    +

    Initializes and returns a newly allocated collection view object with the specified layout.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – initWithFrame:collectionViewLayout: +

    + +
    +
    + +
    + + +
    +

    Initializes an ASCollectionView

    +
    + + + +
    - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    frame

    The frame rectangle for the collection view, measured in points. The origin of the frame is relative to the superview in which you plan to add it. This frame is passed to the superclass during initialization.

    layout

    The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil.

    +
    + + + + + + + +
    +

    Discussion

    +

    Initializes and returns a newly allocated collection view object with the specified frame and layout.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in full mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in full mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in full mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    rangeMode

    The range mode to get the running parameters for.

    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in the given mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeMode

    The range mode to set the running parameters for.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – performBatchAnimated:updates:completion: +

    + +
    +
    + +
    + + +
    +

    Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. +The asyncDataSource must be updated to reflect the changes before the update block completes.

    +
    + + + +
    - (void)performBatchAnimated:(BOOL)animated updates:(nullable __attribute ( ( noescape ) ) void ( ^ ) ( ))updates completion:(nullable void ( ^ ) ( BOOL finished ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    animated

    NO to disable animations for this batch

    updates

    The block that performs the relevant insert, delete, reload, or move operations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – performBatchUpdates:completion: +

    + +
    +
    + +
    + + +
    +

    Perform a batch of updates asynchronously. This method must be called from the main thread. +The asyncDataSource must be updated to reflect the changes before update block completes.

    +
    + + + +
    - (void)performBatchUpdates:(nullable __attribute ( ( noescape ) ) void ( ^ ) ( ))updates completion:(nullable void ( ^ ) ( BOOL finished ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    updates

    The block that performs the relevant insert, delete, reload, or move operations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – reloadDataWithCompletion: +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadDataWithCompletion:(nullable void ( ^ ) ( ))completion
    + + + +
    +

    Parameters

    + + + + + + + +
    completion

    block to run on completion of asynchronous loading or nil. If supplied, the block is run on +the main thread.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UICollectionView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – reloadData +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadData
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UICollectionView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – reloadDataImmediately +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadDataImmediately
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UICollectionView’s version and will block the main thread +while all the cells load.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – relayoutItems +

    + +
    +
    + +
    + + +
    +

    Triggers a relayout of all nodes.

    +
    + + + +
    - (void)relayoutItems
    + + + + + + + + + +
    +

    Discussion

    +

    This method invalidates and lays out every cell node in the collection.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – waitUntilAllUpdatesAreCommitted +

    + +
    +
    + +
    + + +
    +

    Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread.

    +
    + + + +
    - (void)waitUntilAllUpdatesAreCommitted
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – registerSupplementaryNodeOfKind: +

    + +
    +
    + +
    + + +
    +

    Registers the given kind of supplementary node for use in creating node-backed supplementary views.

    +
    + + + +
    - (void)registerSupplementaryNodeOfKind:(NSString *)elementKind
    + + + +
    +

    Parameters

    + + + + + + + +
    elementKind

    The kind of supplementary node that will be requested through the data source.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to register support for the use of supplementary nodes in place of the default +registerClass:forSupplementaryViewOfKind:withReuseIdentifier: and registerNib:forSupplementaryViewOfKind:withReuseIdentifier: +methods. This method will register an internal backing view that will host the contents of the supplementary nodes +returned from the data source.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – insertSections: +

    + +
    +
    + +
    + + +
    +

    Inserts one or more sections.

    +
    + + + +
    - (void)insertSections:(NSIndexSet *)sections
    + + + +
    +

    Parameters

    + + + + + + + +
    sections

    An index set that specifies the sections to insert.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – deleteSections: +

    + +
    +
    + +
    + + +
    +

    Deletes one or more sections.

    +
    + + + +
    - (void)deleteSections:(NSIndexSet *)sections
    + + + +
    +

    Parameters

    + + + + + + + +
    sections

    An index set that specifies the sections to delete.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – reloadSections: +

    + +
    +
    + +
    + + +
    +

    Reloads the specified sections.

    +
    + + + +
    - (void)reloadSections:(NSIndexSet *)sections
    + + + +
    +

    Parameters

    + + + + + + + +
    sections

    An index set that specifies the sections to reload.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – moveSection:toSection: +

    + +
    +
    + +
    + + +
    +

    Moves a section to a new location.

    +
    + + + +
    - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    section

    The index of the section to move.

    newSection

    The index that is the destination of the move for the section.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – insertItemsAtIndexPaths: +

    + +
    +
    + +
    + + +
    +

    Inserts items at the locations identified by an array of index paths.

    +
    + + + +
    - (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of NSIndexPath objects, each representing an item index and section index that together identify an item.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – deleteItemsAtIndexPaths: +

    + +
    +
    + +
    + + +
    +

    Deletes the items specified by an array of index paths.

    +
    + + + +
    - (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of NSIndexPath objects identifying the items to delete.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – reloadItemsAtIndexPaths: +

    + +
    +
    + +
    + + +
    +

    Reloads the specified items.

    +
    + + + +
    - (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of NSIndexPath objects identifying the items to reload.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – moveItemAtIndexPath:toIndexPath: +

    + +
    +
    + +
    + + +
    +

    Moves the item at a specified location to a destination location.

    +
    + + + +
    - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path identifying the item to move.

    newIndexPath

    The index path that is the destination of the move for the item.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – calculatedSizeForNodeAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Query the sized node at @c indexPath for its calculatedSize.

    +
    + + + +
    - (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    The index path for the node of interest.

    + +

    This method is deprecated. Call @c calculatedSize on the node of interest instead. First deprecated in version 2.0.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – visibleNodes +

    + +
    +
    + +
    + + +
    +

    Similar to -visibleCells.

    +
    + + + +
    - (NSArray<__kindofASCellNode*> *)visibleNodes
    + + + + + +
    +

    Return Value

    +

    an array containing the nodes being displayed on screen.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – indexPathForNode: +

    + +
    +
    + +
    + + +
    +

    Similar to -indexPathForCell:.

    +
    + + + +
    - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
    + + + +
    +

    Parameters

    + + + + + + + +
    cellNode

    a cellNode in the collection view

    +
    + + + +
    +

    Return Value

    +

    The index path for this cell node.

    +
    + + + + + +
    +

    Discussion

    +

    This index path returned by this method is in the view’s index space +and should only be used with @c ASCollectionView directly. To get an index path suitable +for use with your data source and @c ASCollectionNode, call @c indexPathForNode: on the +collection node instead.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASControlNode+Debugging.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASControlNode+Debugging.html new file mode 100755 index 0000000000..5f1703f06c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASControlNode+Debugging.html @@ -0,0 +1,187 @@ + + + + + + ASControlNode(Debugging) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASControlNode(Debugging) Category Reference

    + + +
    + + + + +
    Declared inAsyncDisplayKit+Debug.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + setEnableHitTestDebug: +

    + +
    +
    + +
    + + +
    +

    Class method to enable a visualization overlay of the tappable area on the ASControlNode. For app debugging purposes only. +NOTE: GESTURE RECOGNIZERS, (including tap gesture recognizers on a control node) WILL NOT BE VISUALIZED!!! +Overlay = translucent GREEN color, +edges that are clipped by the tappable area of any parent (their bounds + hitTestSlop) in the hierarchy = DARK GREEN BORDERED EDGE, +edges that are clipped by clipToBounds = YES of any parent in the hierarchy = ORANGE BORDERED EDGE (may still receive touches beyond +overlay rect, but can’t be visualized).

    +
    + + + +
    + (void)setEnableHitTestDebug:(BOOL)enable
    + + + +
    +

    Parameters

    + + + + + + + +
    enable

    Specify YES to make this debug feature enabled when messaging the ASControlNode class.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    AsyncDisplayKit+Debug.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASControlNode+Subclassing.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASControlNode+Subclassing.html new file mode 100755 index 0000000000..cfff640cdc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASControlNode+Subclassing.html @@ -0,0 +1,497 @@ + + + + + + ASControlNode(Subclassing) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASControlNode(Subclassing) Category Reference

    + + +
    + + + + +
    Declared inASControlNode+Subclasses.h
    + + + + +
    + +

    Overview

    +

    The subclass header ASControlNode+Subclasses defines methods to be +overridden by custom nodes that subclass ASControlNode.

    + +

    These methods should never be called directly by other classes.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – sendActionsForControlEvents:withEvent: +

    + +
    +
    + +
    + + +
    +

    Sends action messages for the given control events.

    +
    + + + +
    - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)touchEvent
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    controlEvents

    A bitmask whose set flags specify the control events for which action messages are sent. See “Control Events” in ASControlNode.h for bitmask constants.

    touchEvent

    An event object encapsulating the information specific to the user event.

    +
    + + + + + + + +
    +

    Discussion

    +

    ASControlNode implements this method to send all action messages associated with controlEvents. The list of targets is constructed from prior invocations of addTarget:action:forControlEvents:.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – beginTrackingWithTouch:withEvent: +

    + +
    +
    + +
    + + +
    +

    Sent to the control when tracking begins.

    +
    + + + +
    - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)touchEvent
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touch

    The touch on the receiving control.

    touchEvent

    An event object encapsulating the information specific to the user event.

    +
    + + + +
    +

    Return Value

    +

    YES if the receiver should respond continuously (respond when touch is dragged); NO otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – continueTrackingWithTouch:withEvent: +

    + +
    +
    + +
    + + +
    +

    Sent continuously to the control as it tracks a touch within the control’s bounds.

    +
    + + + +
    - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)touchEvent
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touch

    The touch on the receiving control.

    touchEvent

    An event object encapsulating the information specific to the user event.

    +
    + + + +
    +

    Return Value

    +

    YES if touch tracking should continue; NO otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – cancelTrackingWithEvent: +

    + +
    +
    + +
    + + +
    +

    Sent to the control when tracking should be cancelled.

    +
    + + + +
    - (void)cancelTrackingWithEvent:(nullable UIEvent *)touchEvent
    + + + +
    +

    Parameters

    + + + + + + + +
    touchEvent

    An event object encapsulating the information specific to the user event. This parameter may be nil, indicating that the cancelation was caused by something other than an event, such as the display node being removed from its supernode.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – endTrackingWithTouch:withEvent: +

    + +
    +
    + +
    + + +
    +

    Sent to the control when the last touch completely ends, telling it to stop tracking.

    +
    + + + +
    - (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)touchEvent
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touch

    The touch that ended.

    touchEvent

    An event object encapsulating the information specific to the user event.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode+Subclasses.h

    +
    + + +
    +
    +
    + +

      highlighted +

    + +
    +
    + +
    + + +
    +

    Settable version of highlighted property.

    +
    + + + +
    @property (nonatomic, readwrite, assign, getter=isHighlighted) BOOL highlighted
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+AutomaticSubnodeManagement.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+AutomaticSubnodeManagement.html new file mode 100755 index 0000000000..d0bcbb6698 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+AutomaticSubnodeManagement.html @@ -0,0 +1,177 @@ + + + + + + ASDisplayNode(AutomaticSubnodeManagement) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(AutomaticSubnodeManagement) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      automaticallyManagesSubnodes +

    + +
    +
    + +
    + + +
    +

    A boolean that shows whether the node automatically inserts and removes nodes based on the presence or +absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method.

    +
    + + + +
    @property (nonatomic, assign) BOOL automaticallyManagesSubnodes
    + + + + + + + + + +
    +

    Discussion

    +

    If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence +or absence of subnodes is completely determined in its layoutSpecThatFits: method.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Beta.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Beta.html new file mode 100755 index 0000000000..3e7b7af638 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Beta.html @@ -0,0 +1,635 @@ + + + + + + ASDisplayNode(Beta) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(Beta) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode+Beta.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + suppressesInvalidCollectionUpdateExceptions +

    + +
    +
    + +
    + + +
    +

    ASTableView and ASCollectionView now throw exceptions on invalid updates +like their UIKit counterparts. If YES, these classes will log messages +on invalid updates rather than throwing exceptions.

    +
    + + + +
    + (BOOL)suppressesInvalidCollectionUpdateExceptions
    + + + + + + + + + +
    +

    Discussion

    +

    Note that even if AsyncDisplayKit’s exception is suppressed, the app may still crash +as it proceeds with an invalid update.

    + +

    This property defaults to NO. It will be removed in a future release.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

    – recursivelyEnsureDisplaySynchronously: +

    + +
    +
    + +
    + + +
    +

    Recursively ensures node and all subnodes are displayed.

    +
    + + + +
    - (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

      willDisplayNodeContentWithRenderingContext +

    + +
    +
    + +
    + + +
    +

    allow modification of a context before the node’s content is drawn

    +
    + + + +
    @property (nonatomic, copy, nullable) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext
    + + + + + + + + + +
    +

    Discussion

    +

    Set the block to be called after the context has been created and before the node’s content is drawn. +You can override this to modify the context before the content is drawn. You are responsible for saving and +restoring context if necessary. Restoring can be done in contextDidDisplayNodeContent +This block can be called from any thread and it is unsafe to access any UIKit main thread properties from it.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

      didDisplayNodeContentWithRenderingContext +

    + +
    +
    + +
    + + +
    +

    allow modification of a context after the node’s content is drawn

    +
    + + + +
    @property (nonatomic, copy, nullable) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

      measurementOptions +

    + +
    +
    + +
    + + +
    +

    A bitmask representing which actions (layout spec, layout generation) should be measured.

    +
    + + + +
    @property (nonatomic, assign) ASDisplayNodePerformanceMeasurementOptions measurementOptions
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

      performanceMeasurements +

    + +
    +
    + +
    + + +
    +

    A simple struct representing performance measurements collected.

    +
    + + + +
    @property (nonatomic, assign, readonly) ASDisplayNodePerformanceMeasurements performanceMeasurements
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

    – placeholderShouldPersist +

    + +
    +
    + +
    + + +
    +

    Currently used by ASNetworkImageNode and ASMultiplexImageNode to allow their placeholders to stay if they are loading an image from the network. +Otherwise, a display pass is scheduled and completes, but does not actually draw anything - and ASDisplayNode considers the element finished.

    +
    + + + +
    - (BOOL)placeholderShouldPersist
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

    – hierarchyDisplayDidFinish +

    + +
    +
    + +
    + + +
    +

    Indicates that the receiver and all subnodes have finished displaying. May be called more than once, for example if the receiver has +a network image node. This is called after the first display pass even if network image nodes have not downloaded anything (text would be done, +and other nodes that are ready to do their final display). Each render of every progressive jpeg network node would cause this to be called, so +this hook could be called up to 1 + (pJPEGcount * pJPEGrenderCount) times. The render count depends on how many times the downloader calls the +progressImage block.

    +
    + + + +
    - (void)hierarchyDisplayDidFinish
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

    + setRangeModeForMemoryWarnings: +

    + +
    +
    + +
    + + +
    +

    Only ASLayoutRangeModeVisibleOnly or ASLayoutRangeModeLowMemory are recommended. Default is ASLayoutRangeModeVisibleOnly, +because this is the only way to ensure an application will not have blank / flashing views as the user navigates back after +a memory warning. Apps that wish to use the more effective / aggressive ASLayoutRangeModeLowMemory may need to take steps +to mitigate this behavior, including: restoring a larger range mode to the next controller before the user navigates there, +enabling .neverShowPlaceholders on ASCellNodes so that the navigation operation is blocked on redisplay completing, etc.

    +
    + + + +
    + (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

    – _logEventWithBacktrace:format: +

    + +
    +
    + +
    + + +
    +

    The primitive event tracing method. You shouldn’t call this. Use the ASDisplayNodeLogEvent macro instead.

    +
    + + + +
    - (void)_logEventWithBacktrace:(NSArray<NSString*> *)backtrace format:(NSString *)format, ...
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    + +

      eventLog +

    + +
    +
    + +
    + + +
    +

    The most recent trace events for this node. Max count is ASDISPLAYNODE_EVENTLOG_CAPACITY.

    +
    + + + +
    @property (readonly, copy) NSArray *eventLog
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Debugging.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Debugging.html new file mode 100755 index 0000000000..33e97cf949 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Debugging.html @@ -0,0 +1,188 @@ + + + + + + ASDisplayNode(Debugging) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(Debugging) Category Reference

    + + +
    + + + + + + + +
    Conforms toASLayoutElementAsciiArtProtocol
    Declared inASDisplayNode.h
    + + + + +
    + +

    Overview

    +

    Convenience methods for debugging.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – displayNodeRecursiveDescription +

    + +
    +
    + +
    + + +
    +

    Return a description of the node hierarchy.

    +
    + + + +
    - (NSString *)displayNodeRecursiveDescription
    + + + + + + + + + +
    +

    Discussion

    +

    For debugging: (lldb) po [node displayNodeRecursiveDescription]

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Deprecated.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Deprecated.html new file mode 100755 index 0000000000..5eb821eb29 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Deprecated.html @@ -0,0 +1,487 @@ + + + + + + ASDisplayNode(Deprecated) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(Deprecated) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode+Deprecated.h
    + + + + + + +
    + + + + + + +
    +
    + +

      ) +

    + +
    +
    + +
    + + +
    +

    The name of this node, which will be displayed in description. The default value is nil. (Deprecated: Deprecated in version 2.0: Use .debugName instead. This value will display in +results of the -asciiArtString method (@see ASLayoutElementAsciiArtProtocol).)

    +
    + + + +
    @property (nullable, nonatomic, copy) NSString *ASDISPLAYNODE_DEPRECATED_MSG ( "Use .debugName instead." )
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    + +

    – measure: +

    + +
    +
    + +
    + + +
    +

    Asks the node to measure and return the size that best fits its subnodes. (Deprecated: Deprecated in version 2.0: Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout)

    +
    + + + +
    - (CGSize)measure:(CGSize)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The maximum size the receiver should fit in.

    +
    + + + +
    +

    Return Value

    +

    A new size that fits the receiver’s subviews.

    +
    + + + + + +
    +

    Discussion

    +

    Though this method does not set the bounds of the view, it does have side effects–caching both the +constraint and the result.

    Warning: Subclasses must not override this; it calls -measureWithSizeRange: with zero min size. +-measureWithSizeRange: caches results from -calculateLayoutThatFits:. Calling this method may +be expensive if result is not cached.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    + +

    – visibilityDidChange: +

    + +
    +
    + +
    + + +
    +

    Called whenever the visiblity of the node changed. (Deprecated: @see didEnterVisibleState @see didExitVisibleState)

    +
    + + + +
    - (void)visibilityDidChange:(BOOL)isVisible
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when they become visible.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    + +

    – visibleStateDidChange: +

    + +
    +
    + +
    + + +
    +

    Called whenever the visiblity of the node changed. (Deprecated: @see didEnterVisibleState @see didExitVisibleState)

    +
    + + + +
    - (void)visibleStateDidChange:(BOOL)isVisible
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when they become visible.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    + +

    – displayStateDidChange: +

    + +
    +
    + +
    + + +
    +

    Called whenever the the node has entered or exited the display state. (Deprecated: @see didEnterDisplayState @see didExitDisplayState)

    +
    + + + +
    - (void)displayStateDidChange:(BOOL)inDisplayState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when a node should be rendering its content.

    Note: This method can be called from any thread and should therefore be thread safe.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    + +

    – loadStateDidChange: +

    + +
    +
    + +
    + + +
    +

    Called whenever the the node has entered or left the load state. (Deprecated: @see didEnterPreloadState @see didExitPreloadState)

    +
    + + + +
    - (void)loadStateDidChange:(BOOL)inLoadState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor data for a node should be loaded, either from a local or remote source.

    Note: This method can be called from any thread and should therefore be thread safe.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    + +

    – cancelLayoutTransitionsInProgress +

    + +
    +
    + +
    + + +
    +

    Cancels all performing layout transitions. Can be called on any thread. (Deprecated: Deprecated in version 2.0: Use cancelLayoutTransition)

    +
    + + + +
    - (void)cancelLayoutTransitionsInProgress
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Deprecated.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+LayoutTransitioning.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+LayoutTransitioning.html new file mode 100755 index 0000000000..986923fb2a --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+LayoutTransitioning.html @@ -0,0 +1,560 @@ + + + + + + ASDisplayNode(LayoutTransitioning) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(LayoutTransitioning) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      defaultLayoutTransitionDuration +

    + +
    +
    + +
    + + +
    +

    The amount of time it takes to complete the default transition animation. Default is 0.2.

    +
    + + + +
    @property (nonatomic, assign) NSTimeInterval defaultLayoutTransitionDuration
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      defaultLayoutTransitionDelay +

    + +
    +
    + +
    + + +
    +

    The amount of time (measured in seconds) to wait before beginning the default transition animation. +Default is 0.0.

    +
    + + + +
    @property (nonatomic, assign) NSTimeInterval defaultLayoutTransitionDelay
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      defaultLayoutTransitionOptions +

    + +
    +
    + +
    + + +
    +

    A mask of options indicating how you want to perform the default transition animations. +For a list of valid constants, see UIViewAnimationOptions.

    +
    + + + +
    @property (nonatomic, assign) UIViewAnimationOptions defaultLayoutTransitionOptions
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – animateLayoutTransition: +

    + +
    +
    + +
    + + +
    +

    A place to perform your animation. New nodes have been inserted here. You can also use this time to re-order the hierarchy.

    +
    + + + +
    - (void)animateLayoutTransition:(nonnull id<ASContextTransitioning>)context
    + + + + + + + + + +
    +

    Discussion

    +

    A place to perform your animation. New nodes have been inserted here. You can also use this time to re-order the hierarchy.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – didCompleteLayoutTransition: +

    + +
    +
    + +
    + + +
    +

    A place to clean up your nodes after the transition

    +
    + + + +
    - (void)didCompleteLayoutTransition:(nonnull id<ASContextTransitioning>)context
    + + + + + + + + + +
    +

    Discussion

    +

    A place to clean up your nodes after the transition

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – transitionLayoutWithSizeRange:animated:shouldMeasureAsync:measurementCompletion: +

    + +
    +
    + +
    + + +
    +

    Transitions the current layout with a new constrained size. Must be called on main thread.

    +
    + + + +
    - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync measurementCompletion:(nullable void ( ^ ) ( ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    animated

    Animation is optional, but will still proceed through your animateLayoutTransition implementation with isAnimated == NO.

    shouldMeasureAsync

    Measure the layout asynchronously.

    measurementCompletion

    Optional completion block called only if a new layout is calculated. +It is called on main, right after the measurement and before -animateLayoutTransition:.

    +
    + + + + + + + +
    +

    Discussion

    +

    If the passed constrainedSize is the the same as the node’s current constrained size, this method is noop. If passed YES to shouldMeasureAsync it’s guaranteed that measurement is happening on a background thread, otherwise measaurement will happen on the thread that the method was called on. The measurementCompletion callback is always called on the main thread right after the measurement and before -animateLayoutTransition:.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – transitionLayoutWithAnimation:shouldMeasureAsync:measurementCompletion: +

    + +
    +
    + +
    + + +
    +

    Invalidates the current layout and begins a relayout of the node with the current constrainedSize. Must be called on main thread.

    +
    + + + +
    - (void)transitionLayoutWithAnimation:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync measurementCompletion:(nullable void ( ^ ) ( ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    animated

    Animation is optional, but will still proceed through your animateLayoutTransition implementation with isAnimated == NO.

    shouldMeasureAsync

    Measure the layout asynchronously.

    measurementCompletion

    Optional completion block called only if a new layout is calculated.

    +
    + + + + + + + +
    +

    Discussion

    +

    It is called right after the measurement and before -animateLayoutTransition:.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – cancelLayoutTransition +

    + +
    +
    + +
    + + +
    +

    Cancels all performing layout transitions. Can be called on any thread.

    +
    + + + +
    - (void)cancelLayoutTransition
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Subclassing.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Subclassing.html new file mode 100755 index 0000000000..f8844132c0 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+Subclassing.html @@ -0,0 +1,2562 @@ + + + + + + ASDisplayNode(Subclassing) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(Subclassing) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode+Subclasses.h
    + + + + +
    + +

    Overview

    +

    The subclass header ASDisplayNode+Subclasses defines the following methods that either must or can be overriden by +subclasses of ASDisplayNode.

    + +

    These methods should never be called directly by other classes.

    + +

    Drawing

    + +

    Implement one of +displayWithParameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide +drawing for your node.

    + +

    Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for +use on the display queue. The display and drawRect implementations MUST be thread-safe, as they can be called on +the displayQueue (asynchronously) or the main thread (synchronously/displayImmediately).

    + +

    Class methods that require passing in copies of the values are used to minimize the need for locking around instance +variable access, and the possibility of the asynchronous display pass grabbing an inconsistent state across multiple +variables.

    +
    + + + + + +
    + + + + +

    Properties

    + +
    +
    + +

      calculatedLayout +

    + +
    +
    + +
    + + +
    +

    Return the calculated layout.

    +
    + + + +
    @property (nullable, nonatomic, readonly, assign) ASLayout *calculatedLayout
    + + + + + +
    +

    Return Value

    +

    Layout that wraps calculated size returned by -calculateSizeThatFits: (in manual layout mode), +or layout already calculated from layout spec returned by -layoutSpecThatFits: (in automatic layout mode).

    +
    + + + + + +
    +

    Discussion

    +

    For node subclasses that implement manual layout (e.g., they have a custom -layout method), +calculatedLayout may be accessed on subnodes to retrieved cached information about their size. +This allows -layout to be very fast, saving time on the main thread. +Note: .calculatedLayout will only be set for nodes that have had -measure: called on them. +For manual layout, make sure you call -measure: in your implementation of -calculateSizeThatFits:.

    + +

    For node subclasses that use automatic layout (e.g., they implement -layoutSpecThatFits:), +it is typically not necessary to use .calculatedLayout at any point. For these nodes, +the ASLayoutSpec implementation will automatically call -measureWithSizeRange: on all of the subnodes, +and the ASDisplayNode base class implementation of -layout will automatically make use of .calculatedLayout on the subnodes.

    Warning: Subclasses must not override this; it returns the last cached layout and is never expensive.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    View Lifecycle

    + +
    +
    + +

    – didLoad +

    + +
    +
    + +
    + + +
    +

    Called on the main thread immediately after self.view is created.

    +
    + + + +
    - (void)didLoad
    + + + + + + + + + +
    +

    Discussion

    +

    This is the best time to add gesture recognizers to the view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Layout

    + +
    +
    + +

    – layout +

    + +
    +
    + +
    + + +
    +

    Called on the main thread by the view’s -layoutSubviews.

    +
    + + + +
    - (void)layout
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses override this method to layout all subnodes or subviews.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – layoutDidFinish +

    + +
    +
    + +
    + + +
    +

    Called on the main thread by the view’s -layoutSubviews, after -layout.

    +
    + + + +
    - (void)layoutDidFinish
    + + + + + + + + + +
    +

    Discussion

    +

    Gives a chance for subclasses to perform actions after the subclass and superclass have finished laying +out.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – calculatedLayoutDidChange +

    + +
    +
    + +
    + + +
    +

    Called on a background thread if !isNodeLoaded - called on the main thread if isNodeLoaded.

    +
    + + + +
    - (void)calculatedLayoutDidChange
    + + + + + + + + + +
    +

    Discussion

    +

    When the .calculatedLayout property is set to a new ASLayout (directly from -calculateLayoutThatFits: or +calculated via use of -layoutSpecThatFits:), subclasses may inspect it here.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Layout calculation

    + +
    +
    + +

    – calculateLayoutThatFits: +

    + +
    +
    + +
    + + +
    +

    Calculate a layout based on given size range.

    +
    + + + +
    - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The minimum and maximum sizes the receiver should fit in.

    +
    + + + +
    +

    Return Value

    +

    An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used).

    +
    + + + + + +
    +

    Discussion

    +

    This method is called on a non-main thread. The default implementation calls either -layoutSpecThatFits: +or -calculateSizeThatFits:, whichever method is overriden. Subclasses rarely need to override this method, +override -layoutSpecThatFits: or -calculateSizeThatFits: instead.

    Note: This method should not be called directly outside of ASDisplayNode; use -measure: or -calculatedLayout instead.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – calculateLayoutThatFits:restrictedToSize:relativeToParentSize: +

    + +
    +
    + +
    + + +
    +

    ASDisplayNode’s implementation of -layoutThatFits:parentSize: calls this method to resolve the node’s size +against parentSize, intersect it with constrainedSize, and call -calculateLayoutThatFits: with the result.

    +
    + + + +
    - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize
    + + + + + + + + + +
    +

    Discussion

    +

    In certain advanced cases, you may want to customize this logic. Overriding this method allows you to receive all +three parameters and do the computation yourself.

    Warning: Overriding this method should be done VERY rarely.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – calculateSizeThatFits: +

    + +
    +
    + +
    + + +
    +

    Return the calculated size.

    +
    + + + +
    - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The maximum size the receiver should fit in.

    +
    + + + + + + + +
    +

    Discussion

    +

    Subclasses that override should expect this method to be called on a non-main thread. The returned size +is wrapped in an ASLayout and cached for quick access during -layout. Other expensive work that needs to +be done before display can be performed here, and using ivars to cache any valuable intermediate results is +encouraged.

    Note: Subclasses that override are committed to manual layout. Therefore, -layout: must be overriden to layout all subnodes or subviews.

    Note: This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or layoutThatFits:parentSize: instead.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – layoutSpecThatFits: +

    + +
    +
    + +
    + + +
    +

    Return a layout spec that describes the layout of the receiver and its children.

    +
    + + + +
    - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The minimum and maximum sizes the receiver should fit in.

    +
    + + + + + + + +
    +

    Discussion

    +

    Subclasses that override should expect this method to be called on a non-main thread. The returned layout spec +is used to calculate an ASLayout and cached by ASDisplayNode for quick access during -layout. Other expensive work that needs to +be done before display can be performed here, and using ivars to cache any valuable intermediate results is +encouraged.

    Note: This method should not be called directly outside of ASDisplayNode; use -measure: or -calculatedLayout instead.

    Warning: Subclasses that implement -layoutSpecThatFits: must not use .layoutSpecBlock. Doing so will trigger an +exception. A future version of the framework may support using both, calling them serially, with the .layoutSpecBlock +superseding any values set by the method override.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – invalidateCalculatedLayout +

    + +
    +
    + +
    + + +
    +

    Invalidate previously measured and cached layout.

    +
    + + + +
    - (void)invalidateCalculatedLayout
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses should call this method to invalidate the previously measured and cached layout for the display +node, when the contents of the node change in such a way as to require measuring it again.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Drawing

    + +
    +
    + +

    + drawRect:withParameters:isCancelled:isRasterizing: +

    + +
    +
    + +
    + + +
    +

    @summary Delegate method to draw layer contents into a CGBitmapContext. The current UIGraphics context will be set +to an appropriate context.

    +
    + + + +
    + (void)drawRect:(CGRect)bounds withParameters:(nullable id<NSObject>)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    bounds

    Region to draw in.

    parameters

    An object describing all of the properties you need to draw. Return this from +-drawParametersForAsyncLayer:

    isCancelledBlock

    Execute this block to check whether the current drawing operation has been cancelled to avoid +unnecessary work. A return value of YES means cancel drawing and return.

    isRasterizing

    YES if the layer is being rasterized into another layer, in which case drawRect: probably wants +to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store.

    +
    + + + + + + + +
    +

    Discussion

    +

    Note: Called on the display queue and/or main queue (MUST BE THREAD SAFE)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    + displayWithParameters:isCancelled: +

    + +
    +
    + +
    + + +
    +

    @summary Delegate override to provide new layer contents as a UIImage.

    +
    + + + +
    + (nullable UIImage *)displayWithParameters:(nullable id<NSObject>)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    parameters

    An object describing all of the properties you need to draw. Return this from +-drawParametersForAsyncLayer:

    isCancelledBlock

    Execute this block to check whether the current drawing operation has been cancelled to avoid +unnecessary work. A return value of YES means cancel drawing and return.

    +
    + + + +
    +

    Return Value

    +

    A UIImage with contents that are ready to display on the main thread. Make sure that the image is already +decoded before returning it here.

    +
    + + + + + +
    +

    Discussion

    +

    Note: Called on the display queue and/or main queue (MUST BE THREAD SAFE)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – drawParametersForAsyncLayer: +

    + +
    +
    + +
    + + +
    +

    Delegate override for drawParameters

    +
    + + + +
    - (nullable id<NSObject>)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
    + + + +
    +

    Parameters

    + + + + + + + +
    layer

    The layer that will be drawn into.

    +
    + + + + + + + +
    +

    Discussion

    +

    Note: Called on the main thread only

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – displayWillStart +

    + +
    +
    + +
    + + +
    +

    Indicates that the receiver is about to display.

    +
    + + + +
    - (void)displayWillStart
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may override this method to be notified when display (asynchronous or synchronous) is +about to begin.

    Note: Called on the main thread only

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – displayDidFinish +

    + +
    +
    + +
    + + +
    +

    Indicates that the receiver has finished displaying.

    +
    + + + +
    - (void)displayDidFinish
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may override this method to be notified when display (asynchronous or synchronous) has +completed.

    Note: Called on the main thread only

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Observing node-related changes

    + +
    +
    + +

    – interfaceStateDidChange:fromState: +

    + +
    +
    + +
    + + +
    +

    Called whenever any bit in the ASInterfaceState bitfield is changed.

    +
    + + + +
    - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when they become visible, should free cached data, and much more.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didEnterVisibleState +

    + +
    +
    + +
    + + +
    +

    Called whenever the node becomes visible.

    +
    + + + +
    - (void)didEnterVisibleState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when they become visible.

    Note: This method is guaranteed to be called on main.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didExitVisibleState +

    + +
    +
    + +
    + + +
    +

    Called whenever the node is no longer visible.

    +
    + + + +
    - (void)didExitVisibleState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when they are no longer visible.

    Note: This method is guaranteed to be called on main.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didEnterDisplayState +

    + +
    +
    + +
    + + +
    +

    Called whenever the the node has entered the display state.

    +
    + + + +
    - (void)didEnterDisplayState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when a node should be rendering its content.

    Note: This method is guaranteed to be called on main.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didExitDisplayState +

    + +
    +
    + +
    + + +
    +

    Called whenever the the node has exited the display state.

    +
    + + + +
    - (void)didExitDisplayState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor when a node should no longer be rendering its content.

    Note: This method is guaranteed to be called on main.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didEnterPreloadState +

    + +
    +
    + +
    + + +
    +

    Called whenever the the node has entered the preload state.

    +
    + + + +
    - (void)didEnterPreloadState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor data for a node should be preloaded, either from a local or remote source.

    Note: This method is guaranteed to be called on main.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didExitPreloadState +

    + +
    +
    + +
    + + +
    +

    Called whenever the the node has exited the preload state.

    +
    + + + +
    - (void)didExitPreloadState
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may use this to monitor whether preloading data for a node should be canceled.

    Note: This method is guaranteed to be called on main.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – willEnterHierarchy +

    + +
    +
    + +
    + + +
    +

    Called just before the view is added to a window.

    +
    + + + +
    - (void)willEnterHierarchy
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – didExitHierarchy +

    + +
    +
    + +
    + + +
    +

    Called after the view is removed from the window.

    +
    + + + +
    - (void)didExitHierarchy
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

      inHierarchy +

    + +
    +
    + +
    + + +
    +

    Whether the view or layer of this display node is currently in a window

    +
    + + + +
    @property (nonatomic, readonly, assign, getter=isInHierarchy) BOOL inHierarchy
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – fetchData +

    + +
    +
    + +
    + + +
    +

    Indicates that the node should fetch any external data, such as images.

    +
    + + + +
    - (void)fetchData
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses may override this method to be notified when they should begin to fetch data. Fetching +should be done asynchronously. The node is also responsible for managing the memory of any data. +The data may be remote and accessed via the network, but could also be a local database query.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – clearFetchedData +

    + +
    +
    + +
    + + +
    +

    Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node.

    +
    + + + +
    - (void)clearFetchedData
    + + + + + + + + + +
    +

    Discussion

    +

    This will not clear data recursively for all subnodes. Either call -recursivelyClearFetchedData or +selectively clear fetched data.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – clearContents +

    + +
    +
    + +
    + + +
    +

    Provides an opportunity to clear backing store and other memory-intensive intermediates, such as text layout managers +on the current node.

    +
    + + + +
    - (void)clearContents
    + + + + + + + + + +
    +

    Discussion

    +

    Called by -recursivelyClearContents. Base class implements self.contents = nil, clearing any backing +store, for asynchronous regeneration when needed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – subnodeDisplayWillStart: +

    + +
    +
    + +
    + + +
    +

    Indicates that the receiver is about to display its subnodes. This method is not called if there are no +subnodes present.

    +
    + + + +
    - (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode
    + + + +
    +

    Parameters

    + + + + + + + +
    subnode

    The subnode of which display is about to begin.

    +
    + + + + + + + +
    +

    Discussion

    +

    Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) is +about to begin.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – subnodeDisplayDidFinish: +

    + +
    +
    + +
    + + +
    +

    Indicates that the receiver is finished displaying its subnodes. This method is not called if there are +no subnodes present.

    +
    + + + +
    - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode
    + + + +
    +

    Parameters

    + + + + + + + +
    subnode

    The subnode of which display is about to completed.

    +
    + + + + + + + +
    +

    Discussion

    +

    Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) has +completed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – setNeedsDisplayAtScale: +

    + +
    +
    + +
    + + +
    +

    Marks the receiver’s bounds as needing to be redrawn, with a scale value.

    +
    + + + +
    - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale
    + + + +
    +

    Parameters

    + + + + + + + +
    contentsScale

    The scale at which the receiver should be drawn.

    +
    + + + + + + + +
    +

    Discussion

    +

    Subclasses should override this if they don’t want their contentsScale changed.

    Note: This changes an internal property. +-setNeedsDisplay is also available to trigger display without changing contentsScaleForDisplay.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – recursivelySetNeedsDisplayAtScale: +

    + +
    +
    + +
    + + +
    +

    Recursively calls setNeedsDisplayAtScale: on subnodes.

    +
    + + + +
    - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale
    + + + +
    +

    Parameters

    + + + + + + + +
    contentsScale

    The scale at which the receiver’s subnode hierarchy should be drawn.

    +
    + + + + + + + +
    +

    Discussion

    +

    Subclasses may override this if they require modifying the scale set on their child nodes.

    Note: Only the node tree is walked, not the view or layer trees.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

      contentsScaleForDisplay +

    + +
    +
    + +
    + + +
    +

    The scale factor to apply to the rendering.

    +
    + + + +
    @property (nonatomic, assign, readonly) CGFloat contentsScaleForDisplay
    + + + + + + + + + +
    +

    Discussion

    +

    Use setNeedsDisplayAtScale: to set a value and then after display, the display node will set the layer’s +contentsScale. This is to prevent jumps when re-rasterizing at a different contentsScale. +Read this property if you need to know the future contentsScale of your layer, eg in drawParameters.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Touch handling

    + +
    +
    + +

    – touchesBegan:withEvent: +

    + +
    +
    + +
    + + +
    +

    Tells the node when touches began in its view.

    +
    + + + +
    - (void)touchesBegan:(NSSet<UITouch*> *)touches withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touches

    A set of UITouch instances.

    event

    A UIEvent associated with the touch.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – touchesMoved:withEvent: +

    + +
    +
    + +
    + + +
    +

    Tells the node when touches moved in its view.

    +
    + + + +
    - (void)touchesMoved:(NSSet<UITouch*> *)touches withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touches

    A set of UITouch instances.

    event

    A UIEvent associated with the touch.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – touchesEnded:withEvent: +

    + +
    +
    + +
    + + +
    +

    Tells the node when touches ended in its view.

    +
    + + + +
    - (void)touchesEnded:(NSSet<UITouch*> *)touches withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touches

    A set of UITouch instances.

    event

    A UIEvent associated with the touch.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – touchesCancelled:withEvent: +

    + +
    +
    + +
    + + +
    +

    Tells the node when touches was cancelled in its view.

    +
    + + + +
    - (void)touchesCancelled:(nullable NSSet<UITouch*> *)touches withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    touches

    A set of UITouch instances.

    event

    A UIEvent associated with the touch.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Managing Gesture Recognizers

    + +
    +
    + +

    – gestureRecognizerShouldBegin: +

    + +
    +
    + +
    + + +
    +

    Asks the node if a gesture recognizer should continue tracking touches.

    +
    + + + +
    - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
    + + + +
    +

    Parameters

    + + + + + + + +
    gestureRecognizer

    A gesture recognizer trying to recognize a gesture.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Hit Testing

    + +
    +
    + +

    – hitTest:withEvent: +

    + +
    +
    + +
    + + +
    +

    Returns the view that contains the point.

    +
    + + + +
    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    point

    A point specified in the node’s local coordinate system (bounds).

    event

    The event that warranted a call to this method.

    +
    + + + +
    +

    Return Value

    +

    Returns a UIView, not ASDisplayNode, for two reasons: +1) allows sending events to plain UIViews that don’t have attached nodes, +2) hitTest: is never called before the views are created.

    +
    + + + + + +
    +

    Discussion

    +

    Override to make this node respond differently to touches: (e.g. hide touches from subviews, send all +touches to certain subviews (hit area maximizing), etc.)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Placeholders

    + +
    +
    + +

    – placeholderImage +

    + +
    +
    + +
    + + +
    +

    Optionally provide an image to serve as the placeholder for the backing store while the contents are being +displayed.

    +
    + + + +
    - (nullable UIImage *)placeholderImage
    + + + + + + + + + +
    +

    Discussion

    +

    Note: Called on the display queue and/or main queue (MUST BE THREAD SAFE)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + + + +

    Description

    + +
    +
    + +

    – descriptionForRecursiveDescription +

    + +
    +
    + +
    + + +
    +

    Return a description of the node

    +
    + + + +
    - (NSString *)descriptionForRecursiveDescription
    + + + + + + + + + +
    +

    Discussion

    +

    The function that gets called for each display node in -recursiveDescription

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    + +

    – asyncTraitCollectionDidChange +

    + +
    +
    + +
    + + +
    +

    Called when the node’s ASTraitCollection changes

    +
    + + + +
    - (void)asyncTraitCollectionDidChange
    + + + + + + + + + +
    +

    Discussion

    +

    Subclasses can override this method to react to a trait collection change.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Subclasses.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+UIViewBridge.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+UIViewBridge.html new file mode 100755 index 0000000000..97f565ab80 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASDisplayNode+UIViewBridge.html @@ -0,0 +1,348 @@ + + + + + + ASDisplayNode(UIViewBridge) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode(UIViewBridge) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + +
    + +

    Overview

    +

    UIView bridge

    + +

    ASDisplayNode provides thread-safe access to most of UIView and CALayer properties and methods, traditionally unsafe.

    + +

    Using them will not cause the actual view/layer to be created, and will be applied when it is created (when the view +or layer property is accessed).

    + +
      +
    • NOTE: After the view or layer is created, the properties pass through to the view or layer directly and must be called on the main thread.
    • +
    + + +

    See UIView and CALayer for documentation on these common properties.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – setNeedsDisplay +

    + +
    +
    + +
    + + +
    +

    Marks the view as needing display. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread.

    +
    + + + +
    - (void)setNeedsDisplay
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – setNeedsLayout +

    + +
    +
    + +
    + + +
    +

    Marks the node as needing layout. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread.

    +
    + + + +
    - (void)setNeedsLayout
    + + + + + + + + + +
    +

    Discussion

    +

    If this node was measured, calling this method triggers an internal relayout: the calculated layout is invalidated, +and the supernode is notified or (if this node is the root one) a full measurement pass is executed using the old constrained size.

    + +

    Note: ASCellNode has special behavior in that calling this method will automatically notify +the containing ASTableView / ASCollectionView that the cell should be resized, if necessary.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      backgroundColor +

    + +
    +
    + +
    + + +
    +

    The node view’s background color.

    +
    + + + +
    @property (nonatomic, strong, nullable) UIColor *backgroundColor
    + + + + + + + + + +
    +

    Discussion

    +

    In contrast to UIView, setting a transparent color will not set opaque = NO. +This only affects nodes that implement +drawRect like ASTextNode.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      contentMode +

    + +
    +
    + +
    + + +
    +

    A flag used to determine how a node lays out its content when its bounds change.

    +
    + + + +
    @property (nonatomic, assign) UIViewContentMode contentMode
    + + + + + + + + + +
    +

    Discussion

    +

    This is like UIView’s contentMode property, but better. We do our own mapping to layer.contentsGravity in +_ASDisplayView. You can set needsDisplayOnBoundsChange independently. +Thus, UIViewContentModeRedraw is not allowed; use needsDisplayOnBoundsChange = YES instead, and pick an appropriate +contentMode for your content while it’s being re-rendered.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASImageNode+AnimatedImage.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASImageNode+AnimatedImage.html new file mode 100755 index 0000000000..ff83bcb37f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASImageNode+AnimatedImage.html @@ -0,0 +1,275 @@ + + + + + + ASImageNode(AnimatedImage) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASImageNode(AnimatedImage) Category Reference

    + + +
    + + + + +
    Declared inASImageNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      animatedImage +

    + +
    +
    + +
    + + +
    +

    The animated image to playback

    +
    + + + +
    @property (nullable, nonatomic, strong) id<ASAnimatedImageProtocol> animatedImage
    + + + + + + + + + +
    +

    Discussion

    +

    Set this to an object which conforms to ASAnimatedImageProtocol +to have the ASImageNode playback an animated image.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      animatedImagePaused +

    + +
    +
    + +
    + + +
    +

    Pause the playback of an animated image.

    +
    + + + +
    @property (nonatomic, assign) BOOL animatedImagePaused
    + + + + + + + + + +
    +

    Discussion

    +

    Set to YES to pause playback of an animated image and NO to resume +playback.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      animatedImageRunLoopMode +

    + +
    +
    + +
    + + +
    +

    The runloop mode used to animate the image.

    +
    + + + +
    @property (nonatomic, strong) NSString *animatedImageRunLoopMode
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to NSRunLoopCommonModes. Another commonly used mode is NSDefaultRunLoopMode. +Setting NSDefaultRunLoopMode will cause animation to pause while scrolling (if the ASImageNode is +in a scroll view), which may improve scroll performance in some use cases.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASImageNode+Debugging.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASImageNode+Debugging.html new file mode 100755 index 0000000000..d60ea6fd94 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASImageNode+Debugging.html @@ -0,0 +1,184 @@ + + + + + + ASImageNode(Debugging) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASImageNode(Debugging) Category Reference

    + + +
    + + + + +
    Declared inAsyncDisplayKit+Debug.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + setShouldShowImageScalingOverlay: +

    + +
    +
    + +
    + + +
    +

    Enables an ASImageNode debug label that shows the ratio of pixels in the source image to those in +the displayed bounds (including cropRect). This helps detect excessive image fetching / downscaling, +as well as upscaling (such as providing a URL not suitable for a Retina device). For dev purposes only.

    +
    + + + +
    + (void)setShouldShowImageScalingOverlay:(BOOL)show
    + + + +
    +

    Parameters

    + + + + + + + +
    enabled

    Specify YES to show the label on all ASImageNodes with non-1.0x source-to-bounds pixel ratio.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    AsyncDisplayKit+Debug.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayout+.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayout+.html new file mode 100755 index 0000000000..f07aea41ee --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayout+.html @@ -0,0 +1,175 @@ + + + + + + ASLayout() Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayout() Category Reference

    + + +
    + + + + +
    Declared inASLayoutSpec+Subclasses.h
    + + + + + + +
    + + + + + + +
    +
    + +

      position +

    + +
    +
    + +
    + + +
    +

    Position in parent. Default to CGPointNull.

    +
    + + + +
    @property (nonatomic, assign, readwrite) CGPoint position
    + + + + + + + + + +
    +

    Discussion

    +

    When being used as a sublayout, this property must not equal CGPointNull.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec+Subclasses.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayout+Debugging.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayout+Debugging.html new file mode 100755 index 0000000000..0b9fc0a9e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayout+Debugging.html @@ -0,0 +1,170 @@ + + + + + + ASLayout(Debugging) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayout(Debugging) Category Reference

    + + +
    + + + + +
    Declared inASLayout.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – recursiveDescription +

    + +
    +
    + +
    + + +
    +

    Recrusively output the description of the layout tree.

    +
    + + + +
    - (NSString *)recursiveDescription
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutElementStyle+.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutElementStyle+.html new file mode 100755 index 0000000000..0bc980f565 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutElementStyle+.html @@ -0,0 +1,221 @@ + + + + + + ASLayoutElementStyle() Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutElementStyle() Category Reference

    + + +
    + + + + + + + +
    Conforms toASDescriptionProvider
    Declared inASLayoutElementStylePrivate.h
    + + + + + + +
    + + + + + + +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    The object that acts as the delegate of the style.

    +
    + + + +
    @property (nullable, nonatomic, weak) id<ASLayoutElementStyleDelegate> delegate
    + + + + + + + + + +
    +

    Discussion

    +

    The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElementStylePrivate.h

    +
    + + +
    +
    +
    + +

      size +

    + +
    +
    + +
    + + +
    +

    A size constraint that should apply to this ASLayoutElement.

    +
    + + + +
    @property (nonatomic, assign, readonly) ASLayoutElementSize size
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElementStylePrivate.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutSpec+Debugging.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutSpec+Debugging.html new file mode 100755 index 0000000000..c039aa2246 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutSpec+Debugging.html @@ -0,0 +1,173 @@ + + + + + + ASLayoutSpec(Debugging) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutSpec(Debugging) Category Reference

    + + +
    + + + + + + + +
    Conforms toASLayoutElementAsciiArtProtocol
    Declared inASLayoutSpec.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + asciiArtStringForChildren:parentName:direction: +

    + +
    +
    + +
    + + +
    +

    Used by other layout specs to create ascii art debug strings

    +
    + + + +
    + (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutSpec+Subclassing.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutSpec+Subclassing.html new file mode 100755 index 0000000000..177cf376e9 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASLayoutSpec+Subclassing.html @@ -0,0 +1,315 @@ + + + + + + ASLayoutSpec(Subclassing) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutSpec(Subclassing) Category Reference

    + + +
    + + + + +
    Declared inASLayoutSpec+Subclasses.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – layoutElementToAddFromLayoutElement: +

    + +
    +
    + +
    + + +
    +

    Helper method for finalLayoutElement support

    +
    + + + +
    - (id<ASLayoutElement>)layoutElementToAddFromLayoutElement:(id<ASLayoutElement>)child
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: If you are getting recursion crashes here after implementing finalLayoutElement, make sure +that you are setting isFinalLayoutElement flag to YES. This must be one BEFORE adding a child +to the new ASLayoutElement.

    + +

    For example: +- (idASLayoutElement)finalLayoutElement +{ +ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; +insetSpec.insets = UIEdgeInsetsMake(10,10,10,10); +insetSpec.isFinalLayoutElement = YES; +[insetSpec setChild:self]; +return insetSpec; +}

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec+Subclasses.h

    +
    + + +
    +
    +
    + +

    – setChild:atIndex: +

    + +
    +
    + +
    + + +
    +

    Adds a child with the given identifier to this layout spec.

    +
    + + + +
    - (void)setChild:(id<ASLayoutElement>)child atIndex:(NSUInteger)index
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    child

    A child to be added.

    index

    An index associated with the child.

    +
    + + + + + + + +
    +

    Discussion

    +

    Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the +responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, +only require a single child.

    + +

    For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) +a subclass can use the setChild method to set the “primary” child. It should then use this method +to set any other required children. Ideally a subclass would hide this from the user, and use the +setChild:forIndex: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild +property that behind the scenes is calling setChild:forIndex:.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec+Subclasses.h

    +
    + + +
    +
    +
    + +

    – childAtIndex: +

    + +
    +
    + +
    + + +
    +

    Returns the child added to this layout spec using the given index.

    +
    + + + +
    - (nullable id<ASLayoutElement>)childAtIndex:(NSUInteger)index
    + + + +
    +

    Parameters

    + + + + + + + +
    index

    An identifier associated with the the child.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec+Subclasses.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASRangeController+ASRangeControllerUpdateRangeProtocol.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASRangeController+ASRangeControllerUpdateRangeProtocol.html new file mode 100755 index 0000000000..cbdbcb323a --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASRangeController+ASRangeControllerUpdateRangeProtocol.html @@ -0,0 +1,198 @@ + + + + + + ASRangeController(ASRangeControllerUpdateRangeProtocol) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRangeController(ASRangeControllerUpdateRangeProtocol) Category Reference

    + + +
    + + + + + + + +
    Conforms toASRangeControllerUpdateRangeProtocol
    Declared inASRangeController.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – updateCurrentRangeWithMode: +

    + +
    +
    + +
    + + +
    +
      +
    • Update the range mode for a range controller to a explicitly set mode until the node that contains the range
    • +
    • controller becomes visible again +*
    • +
    • Logic for the automatic range mode:
    • +
      1. +
      2. If there are no visible node paths available nothing is to be done and no range update will happen
      3. +
      +
    • +
      1. +
      2. The initial range update if the range controller is visible always will be ASLayoutRangeModeCount
      3. +
      +
    • +
    • (ASLayoutRangeModeMinimum) as it’s the initial fetch
    • +
      1. +
      2. The range mode set explicitly via updateCurrentRangeWithMode: will last at least one range update. After that it +the range controller will use the explicit set range mode until it becomes visible and a new range update was +triggered or a new range mode via updateCurrentRangeWithMode: is set
      3. +
      +
    • +
      1. +
      2. If range mode is not explicitly set the range mode is variying based if the range controller is visible or not
      3. +
      +
    • +
    + +
    + + + +
    - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASRangeController+Debugging.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASRangeController+Debugging.html new file mode 100755 index 0000000000..e85b0d47a3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASRangeController+Debugging.html @@ -0,0 +1,183 @@ + + + + + + ASRangeController(Debugging) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRangeController(Debugging) Category Reference

    + + +
    + + + + +
    Declared inAsyncDisplayKit+Debug.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + setShouldShowRangeDebugOverlay: +

    + +
    +
    + +
    + + +
    +

    Class method to enable a visualization overlay of the all ASRangeController’s tuning parameters. For dev purposes only. +To use, message ASRangeController in the AppDelegate –> [ASRangeController setShouldShowRangeDebugOverlay:YES];

    +
    + + + +
    + (void)setShouldShowRangeDebugOverlay:(BOOL)show
    + + + +
    +

    Parameters

    + + + + + + + +
    enable

    Specify YES to make this debug feature enabled when messaging the ASRangeController class.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    AsyncDisplayKit+Debug.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTableView+Deprecated.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTableView+Deprecated.html new file mode 100755 index 0000000000..c13c9fc87c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTableView+Deprecated.html @@ -0,0 +1,948 @@ + + + + + + ASTableView(Deprecated) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTableView(Deprecated) Category Reference

    + + +
    + + + + +
    Declared inASTableView.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – initWithFrame:style: +

    + +
    +
    + +
    + + +
    +

    Initializer.

    +
    + + + +
    - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    frame

    A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. +The frame of the table view changes as table cells are added and deleted.

    style

    A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in full mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in full mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in full mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    rangeMode

    The range mode to get the running parameters for.

    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in the given mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeMode

    The range mode to set the running parameters for.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – visibleNodes +

    + +
    +
    + +
    + + +
    +

    Similar to -visibleCells.

    +
    + + + +
    - (NSArray<ASCellNode*> *)visibleNodes
    + + + + + +
    +

    Return Value

    +

    an array containing the cell nodes being displayed on screen.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – indexPathForNode: +

    + +
    +
    + +
    + + +
    +

    Similar to -indexPathForCell:.

    +
    + + + +
    - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
    + + + +
    +

    Parameters

    + + + + + + + +
    cellNode

    a cellNode part of the table view

    +
    + + + +
    +

    Return Value

    +

    an indexPath for this cellNode

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – reloadDataWithCompletion: +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadDataWithCompletion:(void ( ^ _Nullable ) ( ))completion
    + + + +
    +

    Parameters

    + + + + + + + +
    completion

    block to run on completion of asynchronous loading or nil. If supplied, the block is run on +the main thread.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UITableView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – reloadData +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadData
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UITableView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – reloadDataImmediately +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadDataImmediately
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UITableView’s version and will block the main thread while +all the cells load.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – relayoutItems +

    + +
    +
    + +
    + + +
    +

    Triggers a relayout of all nodes.

    +
    + + + +
    - (void)relayoutItems
    + + + + + + + + + +
    +

    Discussion

    +

    This method invalidates and lays out every cell node in the table view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – endUpdatesAnimated:completion: +

    + +
    +
    + +
    + + +
    +

    Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view. +You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations +to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating +the operations simultaneously. This method is must be called from the main thread. It’s important to remember that the ASTableView will +be processing the updates asynchronously after this call and are not guaranteed to be reflected in the ASTableView until +the completion block is executed.

    +
    + + + +
    - (void)endUpdatesAnimated:(BOOL)animated completion:(void ( ^ _Nullable ) ( BOOL completed ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    animated

    NO to disable all animations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – waitUntilAllUpdatesAreCommitted +

    + +
    +
    + +
    + + +
    +

    Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread.

    +
    + + + +
    - (void)waitUntilAllUpdatesAreCommitted
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – clearContents +

    + +
    +
    + +
    + + +
    +

    Deprecated in 2.0. You should not call this method.

    +
    + + + +
    - (void)clearContents
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – clearFetchedData +

    + +
    +
    + +
    + + +
    +

    Deprecated in 2.0. You should not call this method.

    +
    + + + +
    - (void)clearFetchedData
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTableView+Internal.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTableView+Internal.html new file mode 100755 index 0000000000..e21fda7dad --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTableView+Internal.html @@ -0,0 +1,453 @@ + + + + + + ASTableView(Internal) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTableView(Internal) Category Reference

    + + +
    + + + + +
    Declared inASTableViewInternal.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – _initWithFrame:style:dataControllerClass: +

    + +
    +
    + +
    + + +
    +

    Initializer.

    +
    + + + +
    - (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    frame

    A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. +The frame of the table view changes as table cells are added and deleted.

    style

    A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants.

    dataControllerClass

    A controller class injected to and used to create a data controller for the table view.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableViewInternal.h

    +
    + + +
    +
    +
    + +

      test_enableSuperUpdateCallLogging +

    + +
    +
    + +
    + + +
    +

    Set YES and we’ll log every time we call [super insertRows…] etc

    +
    + + + +
    @property (nonatomic) BOOL test_enableSuperUpdateCallLogging
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableViewInternal.h

    +
    + + +
    +
    +
    + +

    – convertIndexPathFromTableNode:waitingIfNeeded: +

    + +
    +
    + +
    + + +
    +

    Attempt to get the view-layer index path for the row with the given index path.

    +
    + + + +
    - (NSIndexPath *)convertIndexPathFromTableNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path of the row.

    wait

    If the item hasn’t reached the view yet, this attempts to wait for updates to commit.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableViewInternal.h

    +
    + + +
    +
    +
    + +

    – convertIndexPathToTableNode: +

    + +
    +
    + +
    + + +
    +

    Attempt to get the node index path given the view-layer index path.

    +
    + + + +
    - (NSIndexPath *)convertIndexPathToTableNode:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    The index path of the row.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableViewInternal.h

    +
    + + +
    +
    +
    + +

    – convertIndexPathsToTableNode: +

    + +
    +
    + +
    + + +
    +

    Attempt to get the node index paths given the view-layer index paths.

    +
    + + + +
    - (NSArray<NSIndexPath*> *)convertIndexPathsToTableNode:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of index paths in the view space

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableViewInternal.h

    +
    + + +
    +
    +
    + +

    – sectionIndexWidth +

    + +
    +
    + +
    + + +
    +

    Returns the width of the section index view on the right-hand side of the table, if one is present.

    +
    + + + +
    - (CGFloat)sectionIndexWidth
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableViewInternal.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTextNode+.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTextNode+.html new file mode 100755 index 0000000000..ec3bdd4fbb --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTextNode+.html @@ -0,0 +1,226 @@ + + + + + + ASTextNode() Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTextNode() Category Reference

    + + +
    + + + + +
    Declared inASTextNode+Beta.h
    + + + + + + +
    + + + + + + +
    +
    + +

      pointSizeScaleFactors +

    + +
    +
    + +
    + + +
    +

    An array of descending scale factors that will be applied to this text node to try to make it fit within its constrained size

    +
    + + + +
    @property (nullable, nonatomic, copy) NSArray<NSNumber*> *pointSizeScaleFactors
    + + + + + + + + + +
    +

    Discussion

    +

    This array should be in descending order and NOT contain the scale factor 1.0. For example, it could return @[@(.9), @(.85), @(.8)]; +@default nil (no scaling)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode+Beta.h

    +
    + + +
    +
    +
    + +

      textContainerInset +

    + +
    +
    + +
    + + +
    +

    Text margins for text laid out in the text node.

    +
    + + + +
    @property (nonatomic, assign) UIEdgeInsets textContainerInset
    + + + + + + + + + +
    +

    Discussion

    +

    defaults to UIEdgeInsetsZero. +This property can be useful for handling text which does not fit within the view by default. An example: like UILabel, +ASTextNode will clip the left and right of the string “judar” if it’s rendered in an italicised font.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode+Beta.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTextNode+Deprecated.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTextNode+Deprecated.html new file mode 100755 index 0000000000..a310bc3f27 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASTextNode+Deprecated.html @@ -0,0 +1,175 @@ + + + + + + ASTextNode(Deprecated) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTextNode(Deprecated) Category Reference

    + + +
    + + + + +
    Declared inASTextNode.h
    + + + + + + + + +
    + + + + + + +
    +
    + +

      ) +

    + +
    +
    + +
    + + +
    +

    The attributedString and attributedText properties are equivalent, but attributedText is now the standard API +name in order to match UILabel and ASEditableTextNode.

    +
    + + + +
    @property (nullable, nonatomic, copy) NSAttributedString *ASDISPLAYNODE_DEPRECATED_MSG ( "Use .attributedText instead." )
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASViewController+ASRangeControllerUpdateRangeProtocol.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASViewController+ASRangeControllerUpdateRangeProtocol.html new file mode 100755 index 0000000000..0c0d6ab95f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/ASViewController+ASRangeControllerUpdateRangeProtocol.html @@ -0,0 +1,176 @@ + + + + + + ASViewController(ASRangeControllerUpdateRangeProtocol) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASViewController(ASRangeControllerUpdateRangeProtocol) Category Reference

    + + +
    + + + + +
    Declared inASViewController.h
    + + + + + + +
    + + + + + + +
    +
    + +

      automaticallyAdjustRangeModeBasedOnViewEvents +

    + +
    +
    + +
    + + +
    +

    Automatically adjust range mode based on view events. If you set this to YES, the view controller or its node +must conform to the ASRangeControllerUpdateRangeProtocol.

    +
    + + + +
    @property (nonatomic, assign) BOOL automaticallyAdjustRangeModeBasedOnViewEvents
    + + + + + + + + + +
    +

    Discussion

    +

    Default value is YES if node or view controller conform to ASRangeControllerUpdateRangeProtocol otherwise it is NO.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/CALayer+AsyncDisplayKit.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/CALayer+AsyncDisplayKit.html new file mode 100755 index 0000000000..33a9fc83d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/CALayer+AsyncDisplayKit.html @@ -0,0 +1,182 @@ + + + + + + CALayer(AsyncDisplayKit) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    CALayer(AsyncDisplayKit) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – addSubnode: +

    + +
    +
    + +
    + + +
    +

    Convenience method, equivalent to [layer addSublayer:node.layer].

    +
    + + + +
    - (void)addSubnode:(nonnull ASDisplayNode *)node
    + + + +
    +

    Parameters

    + + + + + + + +
    node

    The node to be added.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/NSNumber+ASDimension.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/NSNumber+ASDimension.html new file mode 100755 index 0000000000..7d11d7936e --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/NSNumber+ASDimension.html @@ -0,0 +1,118 @@ + + + + + + NSNumber(ASDimension) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    NSNumber(ASDimension) Category Reference

    + + +
    + + + + +
    Declared inASDimension.h
    + + + + +
    + +

    Overview

    +

    Resolve this dimension to a parent size.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/NSURL+ASPhotosFrameworkURLs.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/NSURL+ASPhotosFrameworkURLs.html new file mode 100755 index 0000000000..1ca4bce449 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/NSURL+ASPhotosFrameworkURLs.html @@ -0,0 +1,176 @@ + + + + + + NSURL(ASPhotosFrameworkURLs) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    NSURL(ASPhotosFrameworkURLs) Category Reference

    + + +
    + + + + +
    Declared inASMultiplexImageNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + URLWithAssetLocalIdentifier:targetSize:contentMode:options: +

    + +
    +
    + +
    + + +
    +

    Create an NSURL that specifies an image from the Photos framework.

    +
    + + + +
    + (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options
    + + + + + + + + + +
    +

    Discussion

    +

    When implementing -multiplexImageNode:URLForImageIdentifier:, you can return a URL +created by this method and the image node will attempt to load the image from the Photos framework.

    Note: The synchronous flag in options is ignored.

    Note: The Opportunistic delivery mode is not supported and will be treated as HighQualityFormat.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/UIImage+ASDKAdditions.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/UIImage+ASDKAdditions.html new file mode 100755 index 0000000000..6bf7903113 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/UIImage+ASDKAdditions.html @@ -0,0 +1,352 @@ + + + + + + UIImage(ASDKAdditions) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + Texture +

    + +
    +
    + + + +
    +
    +
    +
    +

    UIImage(ASDKAdditions) Category Reference

    + + +
    + + + + +
    Declared inUIImage+ASConvenience.h
    + + + + + + +
    + + + + + + +
    +
    + +

    + as_resizableRoundedImageWithCornerRadius:cornerColor:fillColor: +

    + +
    +
    + +
    + + +
    +

    This generates a flat-color, rounded-corner resizeable image

    +
    + + + +
    + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(UIColor *)cornerColor fillColor:(UIColor *)fillColor
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    cornerRadius

    The radius of the rounded-corner

    cornerColor

    The fill color of the corners (For Alpha corners use clearColor)

    fillColor

    The fill color of the rounded-corner image

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    UIImage+ASConvenience.h

    +
    + + +
    +
    +
    + +

    + as_resizableRoundedImageWithCornerRadius:cornerColor:fillColor:borderColor:borderWidth: +

    + +
    +
    + +
    + + +
    +

    This generates a flat-color, rounded-corner resizeable image with a border

    +
    + + + +
    + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(UIColor *)cornerColor fillColor:(UIColor *)fillColor borderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    cornerRadius

    The radius of the rounded-corner

    cornerColor

    The fill color of the corners (For Alpha corners use clearColor)

    fillColor

    The fill color of the rounded-corner image

    borderColor

    The border color. Set to nil for no border.

    borderWidth

    The border width. Dummy value if borderColor = nil.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    UIImage+ASConvenience.h

    +
    + + +
    +
    +
    + +

    + as_resizableRoundedImageWithCornerRadius:cornerColor:fillColor:borderColor:borderWidth:roundedCorners:scale: +

    + +
    +
    + +
    + + +
    +

    This generates a flat-color, rounded-corner resizeable image with a border

    +
    + + + +
    + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(UIColor *)cornerColor fillColor:(UIColor *)fillColor borderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth roundedCorners:(UIRectCorner)roundedCorners scale:(CGFloat)scale
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    cornerRadius

    The radius of the rounded-corner

    cornerColor

    The fill color of the corners (For Alpha corners use clearColor)

    fillColor

    The fill color of the rounded-corner image

    borderColor

    The border color. Set to nil for no border.

    borderWidth

    The border width. Dummy value if borderColor = nil.

    roundedCorners

    Select individual or multiple corners to round. Set to UIRectCornerAllCorners to round all 4 corners.

    scale

    The number of pixels per point. Provide 0.0 to use the screen scale.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    UIImage+ASConvenience.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Categories/UIView+AsyncDisplayKit.html b/submodules/AsyncDisplayKit/docs/appledoc/Categories/UIView+AsyncDisplayKit.html new file mode 100755 index 0000000000..a1bcd6c40b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Categories/UIView+AsyncDisplayKit.html @@ -0,0 +1,192 @@ + + + + + + UIView(AsyncDisplayKit) Category Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    UIView(AsyncDisplayKit) Category Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + +
    + +

    Overview

    +

    UIVIew(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to an UIView.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – addSubnode: +

    + +
    +
    + +
    + + +
    +

    Convenience method, equivalent to [view addSubview:node.view] or [view.layer addSublayer:node.layer] if layer-backed.

    +
    + + + +
    - (void)addSubnode:(nonnull ASDisplayNode *)node
    + + + +
    +

    Parameters

    + + + + + + + +
    node

    The node to be added.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASAbsoluteLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASAbsoluteLayoutSpec.html new file mode 100755 index 0000000000..4a526d9b96 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASAbsoluteLayoutSpec.html @@ -0,0 +1,302 @@ + + + + + + ASAbsoluteLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASAbsoluteLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASAbsoluteLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    A layout spec that positions children at fixed positions.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      sizing +

    + +
    +
    + +
    + + +
    +

    How much space will the spec taken up

    +
    + + + +
    @property (nonatomic, assign) ASAbsoluteLayoutSpecSizing sizing
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAbsoluteLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + absoluteLayoutSpecWithSizing:children: +

    + +
    +
    + +
    + + +
    +

    How much space the spec will take up

    +
    + + + +
    + (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray<id<ASLayoutElement> > *)children
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    sizing

    How much space the spec will take up

    children

    Children to be positioned at fixed positions

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAbsoluteLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + absoluteLayoutSpecWithChildren: +

    + +
    +
    + +
    + + +
    +

    Children to be positioned at fixed positions

    +
    + + + +
    + (instancetype)absoluteLayoutSpecWithChildren:(NSArray<id<ASLayoutElement> > *)children
    + + + +
    +

    Parameters

    + + + + + + + +
    children

    Children to be positioned at fixed positions

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAbsoluteLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASAsciiArtBoxCreator.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASAsciiArtBoxCreator.html new file mode 100755 index 0000000000..de14a95656 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASAsciiArtBoxCreator.html @@ -0,0 +1,236 @@ + + + + + + ASAsciiArtBoxCreator Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASAsciiArtBoxCreator Class Reference

    + + +
    + + + + + + + +
    Inherits fromNSObject
    Declared inASAsciiArtBoxCreator.h
    + + + + +
    + +

    Overview

    +

    A that takes a parent and its children and renders as ascii art box.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    + horizontalBoxStringForChildren:parent: +

    + +
    +
    + +
    + + +
    +

    Renders an ascii art box with the children aligned horizontally +Example: +————ASStackLayoutSpec———–

    + +

    | ASTextNode ASTextNode ASTextNode |

    +
    + + + +
    + (NSString *)horizontalBoxStringForChildren:(NSArray<NSString*> *)children parent:(NSString *)parent
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAsciiArtBoxCreator.h

    +
    + + +
    +
    +
    + +

    + verticalBoxStringForChildren:parent: +

    + +
    +
    + +
    + + +
    +

    Renders an ascii art box with the children aligned vertically. +Example: +–ASStackLayoutSpec– +| ASTextNode | +| ASTextNode |

    + +

    | ASTextNode |

    +
    + + + +
    + (NSString *)verticalBoxStringForChildren:(NSArray<NSString*> *)children parent:(NSString *)parent
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAsciiArtBoxCreator.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASBackgroundLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASBackgroundLayoutSpec.html new file mode 100755 index 0000000000..e860a8b322 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASBackgroundLayoutSpec.html @@ -0,0 +1,247 @@ + + + + + + ASBackgroundLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASBackgroundLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASBackgroundLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    Lays out a single layoutElement child, then lays out a background layoutElement instance behind it stretched to its size.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      background +

    + +
    +
    + +
    + + +
    +

    Background layoutElement for this layout spec

    +
    + + + +
    @property (nullable, nonatomic, strong) id<ASLayoutElement> background
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASBackgroundLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + backgroundLayoutSpecWithChild:background: +

    + +
    +
    + +
    + + +
    +

    Creates and returns an ASBackgroundLayoutSpec object

    +
    + + + +
    + (instancetype)backgroundLayoutSpecWithChild:(id<ASLayoutElement>)child background:(nullable id<ASLayoutElement>)background
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    child

    A child that is laid out to determine the size of this spec.

    background

    A layoutElement object that is laid out behind the child. If this is nil, the background is omitted.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASBackgroundLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASButtonNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASButtonNode.html new file mode 100755 index 0000000000..3aca43f70f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASButtonNode.html @@ -0,0 +1,831 @@ + + + + + + ASButtonNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASButtonNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASButtonNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      contentSpacing +

    + +
    +
    + +
    + + +
    +

    Spacing between image and title. Defaults to 8.0.

    +
    + + + +
    @property (nonatomic, assign) CGFloat contentSpacing
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

      laysOutHorizontally +

    + +
    +
    + +
    + + +
    +

    Whether button should be laid out vertically (image on top of text) or horizontally (image to the left of text). +ASButton node does not yet support RTL but it should be fairly easy to implement. +Defaults to YES.

    +
    + + + +
    @property (nonatomic, assign) BOOL laysOutHorizontally
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

      contentHorizontalAlignment +

    + +
    +
    + +
    + + +
    +

    Horizontally align content (text or image). +Defaults to ASHorizontalAlignmentMiddle.

    +
    + + + +
    @property (nonatomic, assign) ASHorizontalAlignment contentHorizontalAlignment
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

      contentVerticalAlignment +

    + +
    +
    + +
    + + +
    +

    Vertically align content (text or image). +Defaults to ASVerticalAlignmentCenter.

    +
    + + + +
    @property (nonatomic, assign) ASVerticalAlignment contentVerticalAlignment
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

      contentEdgeInsets +

    + +
    +
    + +
    + + +
    +

    The insets used around the title and image node

    +
    + + + +
    @property (nonatomic, assign) UIEdgeInsets contentEdgeInsets
    + + + + + + + + + +
    +

    Discussion

    +

    The insets used around the title and image node

    +
    + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

      imageAlignment +

    + +
    +
    + +
    + + +
    +

    @discusstion Whether the image should be aligned at the beginning or at the end of node. Default is ASButtonNodeImageAlignmentBeginning.

    +
    + + + +
    @property (nonatomic, assign) ASButtonNodeImageAlignment imageAlignment
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – attributedTitleForState: +

    + +
    +
    + +
    + + +
    +

    Returns the styled title associated with the specified state.

    +
    + + + +
    - (NSAttributedString *_Nullable)attributedTitleForState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + +
    state

    The state that uses the styled title. The possible values are described in ASControlState.

    +
    + + + +
    +

    Return Value

    +

    The title for the specified state.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – setAttributedTitle:forState: +

    + +
    +
    + +
    + + +
    +

    Sets the styled title to use for the specified state. This will reset styled title previously set with -setTitle:withFont:withColor:forState.

    +
    + + + +
    - (void)setAttributedTitle:(nullable NSAttributedString *)title forState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    title

    The styled text string to use for the title.

    state

    The state that uses the specified title. The possible values are described in ASControlState.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – setTitle:withFont:withColor:forState: +

    + +
    +
    + +
    + + +
    +

    Sets the title to use for the specified state. This will reset styled title previously set with -setAttributedTitle:forState.

    +
    + + + +
    - (void)setTitle:(NSString *)title withFont:(nullable UIFont *)font withColor:(nullable UIColor *)color forState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    title

    The styled text string to use for the title.

    font

    The font to use for the title.

    color

    The color to use for the title.

    state

    The state that uses the specified title. The possible values are described in ASControlState.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – imageForState: +

    + +
    +
    + +
    + + +
    +

    Returns the image used for a button state.

    +
    + + + +
    - (nullable UIImage *)imageForState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + +
    state

    The state that uses the image. Possible values are described in ASControlState.

    +
    + + + +
    +

    Return Value

    +

    The image used for the specified state.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – setImage:forState: +

    + +
    +
    + +
    + + +
    +

    Sets the image to use for the specified state.

    +
    + + + +
    - (void)setImage:(nullable UIImage *)image forState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    image

    The image to use for the specified state.

    state

    The state that uses the specified title. The values are described in ASControlState.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – setBackgroundImage:forState: +

    + +
    +
    + +
    + + +
    +

    Sets the background image to use for the specified state.

    +
    + + + +
    - (void)setBackgroundImage:(nullable UIImage *)image forState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    image

    The image to use for the specified state.

    state

    The state that uses the specified title. The values are described in ASControlState.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    + +

    – backgroundImageForState: +

    + +
    +
    + +
    + + +
    +

    Returns the background image used for a button state.

    +
    + + + +
    - (nullable UIImage *)backgroundImageForState:(ASControlState)state
    + + + +
    +

    Parameters

    + + + + + + + +
    state

    The state that uses the image. Possible values are described in ASControlState.

    +
    + + + +
    +

    Return Value

    +

    The background image used for the specified state.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCellNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCellNode.html new file mode 100755 index 0000000000..2f92770841 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCellNode.html @@ -0,0 +1,558 @@ + + + + + + ASCellNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCellNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Declared inASCellNode.h
    + + + + +
    + +

    Overview

    +
      +
    • Generic cell node. Subclass this instead of ASDisplayNode to use with ASTableView and ASCollectionView.

    • +
    • @note When a cell node is contained inside a collection view (or table view),

    • +
    • calling -setNeedsLayout will also notify the collection on the main thread
    • +
    • so that the collection can update its item layout if the cell’s size changed.
    • +
    + +
    + + + + + +
    + + + + + + +
    +
    + +

      neverShowPlaceholders +

    + +
    +
    + +
    + + +
    +

    When enabled, ensures that the cell is completely displayed before allowed onscreen.

    + +

    @default NO

    +
    + + + +
    @property (nonatomic, assign) BOOL neverShowPlaceholders
    + + + + + + + + + +
    +

    Discussion

    +

    Normally, ASCellNodes are preloaded and have finished display before they are onscreen. +However, if the Table or Collection’s rangeTuningParameters are set to small values (or 0), +or if the user is scrolling rapidly on a slow device, it is possible for a cell’s display to +be incomplete when it becomes visible.

    + +

    In this case, normally placeholder states are shown and scrolling continues uninterrupted. +The finished, drawn content is then shown as soon as it is ready.

    + +

    With this property set to YES, the main thread will be blocked until display is complete for +the cell. This is more similar to UIKit, and in fact makes AsyncDisplayKit scrolling visually +indistinguishable from UIKit’s, except being faster.

    + +

    Using this option does not eliminate all of the performance advantages of AsyncDisplayKit. +Normally, a cell has been preloading and is almost done when it reaches the screen, so the +blocking time is very short. If the rangeTuningParameters are set to 0, still this option +outperforms UIKit: while the main thread is waiting, subnode display executes concurrently.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      selected +

    + +
    +
    + +
    + + +
    +

    A Boolean value that is synchronized with the underlying collection or tableView cell property. +Setting this value is equivalent to calling selectItem / deselectItem on the collection or table.

    +
    + + + +
    @property (nonatomic, assign, getter=isSelected) BOOL selected
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      highlighted +

    + +
    +
    + +
    + + +
    +

    A Boolean value that is synchronized with the underlying collection or tableView cell property. +Setting this value is equivalent to calling highlightItem / unHighlightItem on the collection or table.

    +
    + + + +
    @property (nonatomic, assign, getter=isHighlighted) BOOL highlighted
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      indexPath +

    + +
    +
    + +
    + + +
    +

    The current index path of this cell node, or @c nil if this node is +not a valid item inside a table node or collection node.

    +
    + + + +
    @property (nonatomic, readonly, nullable) NSIndexPath *indexPath
    + + + + + + + + + +
    +

    Discussion

    +

    Note: This property must be accessed on the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      owningNode +

    + +
    +
    + +
    + + +
    +

    The owning node (ASCollectionNode/ASTableNode) of this cell node, or @c nil if this node is +not a valid item inside a table node or collection node or if those nodes are nil.

    +
    + + + +
    @property (weak, nonatomic, readonly, nullable) ASDisplayNode *owningNode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

    – applyLayoutAttributes: +

    + +
    +
    + +
    + + +
    +

    Called by the system when ASCellNode is used with an ASCollectionNode. It will not be called by ASTableNode. +When the UICollectionViewLayout object returns a new UICollectionViewLayoutAttributes object, the corresponding ASCellNode will be updated. +See UICollectionViewCell’s applyLayoutAttributes: for a full description.

    +
    + + + +
    - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

    – initWithViewControllerBlock:didLoadBlock: +

    + +
    +
    + +
    + + +
    +

    Initializes a cell with a given view controller block.

    +
    + + + +
    - (instancetype)initWithViewControllerBlock:(ASDisplayNodeViewControllerBlock)viewControllerBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    viewControllerBlock

    The block that will be used to create the backing view controller.

    didLoadBlock

    The block that will be called after the view controller’s view is loaded.

    +
    + + + +
    +

    Return Value

    +

    An ASCellNode created using the root view of the view controller provided by the viewControllerBlock. +The view controller’s root view is resized to match the calculated size produced during layout.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

    – cellNodeVisibilityEvent:inScrollView:withCellFrame: +

    + +
    +
    + +
    + + +
    +

    Notifies the cell node of certain visibility events, such as changing visible rect.

    +
    + + + +
    - (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(nullable UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: In cases where an ASCellNode is used as a plain node – i.e. not returned from the +nodeBlockForItemAtIndexPath/nodeForItemAtIndexPath data source methods – this method will +deliver only the Visible and Invisible events, scrollView will be nil, and +cellFrame will be the zero rect.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCenterLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCenterLayoutSpec.html new file mode 100755 index 0000000000..fe4f01cffd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCenterLayoutSpec.html @@ -0,0 +1,207 @@ + + + + + + ASCenterLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCenterLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASRelativeLayoutSpec : ASLayoutSpec : NSObject
    Declared inASCenterLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    Lays out a single layoutElement child and position it so that it is centered into the layout bounds. +NOTE: ASRelativeLayoutSpec offers all of the capabilities of Center, and more. +Check it out if you would like to be able to position the child at any corner or the middle of an edge.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    + centerLayoutSpecWithCenteringOptions:sizingOptions:child: +

    + +
    +
    + +
    + + +
    +

    Initializer.

    +
    + + + +
    + (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions child:(id<ASLayoutElement>)child
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    centeringOptions

    How the child is centered.

    sizingOptions

    How much space will be taken up.

    child

    The child to center.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCenterLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCollectionNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCollectionNode.html new file mode 100755 index 0000000000..2471991465 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCollectionNode.html @@ -0,0 +1,2405 @@ + + + + + + ASCollectionNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionNode Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Conforms toASRangeControllerUpdateRangeProtocol
    Declared inASCollectionNode.h
    + + + + +
    + +

    Overview

    +

    ASCollectionNode is a node based class that wraps an ASCollectionView. It can be used +as a subnode of another node, and provide room for many (great) features and improvements later on.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – initWithCollectionViewLayout: +

    + +
    +
    + +
    + + +
    +

    Initializes an ASCollectionNode

    +
    + + + +
    - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout
    + + + +
    +

    Parameters

    + + + + + + + +
    layout

    The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil.

    +
    + + + + + + + +
    +

    Discussion

    +

    Initializes and returns a newly allocated collection node object with the specified layout.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – initWithFrame:collectionViewLayout: +

    + +
    +
    + +
    + + +
    +

    Initializes an ASCollectionNode

    +
    + + + +
    - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    frame

    The frame rectangle for the collection view, measured in points. The origin of the frame is relative to the superview in which you plan to add it. This frame is passed to the superclass during initialization.

    layout

    The layout object to use for organizing items. The collection view stores a strong reference to the specified object. Must not be nil.

    +
    + + + + + + + +
    +

    Discussion

    +

    Initializes and returns a newly allocated collection node object with the specified frame and layout.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      view +

    + +
    +
    + +
    + + +
    +

    Returns the corresponding ASCollectionView

    +
    + + + +
    @property (strong, nonatomic, readonly) ASCollectionView *view
    + + + + + +
    +

    Return Value

    +

    view The corresponding ASCollectionView.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    The object that acts as the asynchronous delegate of the collection view

    +
    + + + +
    @property (weak, nonatomic) id<ASCollectionDelegate> delegate
    + + + + + + + + + +
    +

    Discussion

    +

    The delegate must adopt the ASCollectionDelegate protocol. The collection view maintains a weak reference to the delegate object.

    + +

    The delegate object is responsible for providing size constraints for nodes and indicating whether batch fetching should begin.

    Note: This is a convenience method which sets the asyncDelegate on the collection node’s collection view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      dataSource +

    + +
    +
    + +
    + + +
    +

    The object that acts as the asynchronous data source of the collection view

    +
    + + + +
    @property (weak, nonatomic) id<ASCollectionDataSource> dataSource
    + + + + + + + + + +
    +

    Discussion

    +

    The datasource must adopt the ASCollectionDataSource protocol. The collection view maintains a weak reference to the datasource object.

    + +

    The datasource object is responsible for providing nodes or node creation blocks to the collection view.

    Note: This is a convenience method which sets the asyncDatasource on the collection node’s collection view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      allowsSelection +

    + +
    +
    + +
    + + +
    +

    A Boolean value that indicates whether users can select items in the collection node. +If the value of this property is YES (the default), users can select items. If you want more fine-grained control over the selection of items, you must provide a delegate object and implement the appropriate methods of the UICollectionNodeDelegate protocol.

    +
    + + + +
    @property (nonatomic, assign) BOOL allowsSelection
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      allowsMultipleSelection +

    + +
    +
    + +
    + + +
    +

    A Boolean value that determines whether users can select more than one item in the collection node. +This property controls whether multiple items can be selected simultaneously. The default value of this property is NO. +When the value of this property is YES, tapping a cell adds it to the current selection (assuming the delegate permits the cell to be selected). Tapping the cell again removes it from the selection.

    +
    + + + +
    @property (nonatomic, assign) BOOL allowsMultipleSelection
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in full mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in full mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in full mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    rangeMode

    The range mode to get the running parameters for.

    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in the given mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeMode

    The range mode to set the running parameters for.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – scrollToItemAtIndexPath:atScrollPosition:animated: +

    + +
    +
    + +
    + + +
    +

    Scrolls the collection to the given item.

    +
    + + + +
    - (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    indexPath

    The index path of the item.

    scrollPosition

    Where the item should end up after the scroll.

    animated

    Whether the scroll should be animated or not.

    + +

    This method must be called on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – registerSupplementaryNodeOfKind: +

    + +
    +
    + +
    + + +
    +

    Registers the given kind of supplementary node for use in creating node-backed supplementary elements.

    +
    + + + +
    - (void)registerSupplementaryNodeOfKind:(NSString *)elementKind
    + + + +
    +

    Parameters

    + + + + + + + +
    elementKind

    The kind of supplementary node that will be requested through the data source.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to register support for the use of supplementary nodes in place of the default +registerClass:forSupplementaryViewOfKind:withReuseIdentifier: and registerNib:forSupplementaryViewOfKind:withReuseIdentifier: +methods. This method will register an internal backing view that will host the contents of the supplementary nodes +returned from the data source.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – performBatchAnimated:updates:completion: +

    + +
    +
    + +
    + + +
    +

    Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. +The data source must be updated to reflect the changes before the update block completes.

    +
    + + + +
    - (void)performBatchAnimated:(BOOL)animated updates:(nullable __attribute ( ( noescape ) ) void ( ^ ) ( ))updates completion:(nullable void ( ^ ) ( BOOL finished ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    animated

    NO to disable animations for this batch

    updates

    The block that performs the relevant insert, delete, reload, or move operations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – performBatchUpdates:completion: +

    + +
    +
    + +
    + + +
    +

    Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. +The data source must be updated to reflect the changes before the update block completes.

    +
    + + + +
    - (void)performBatchUpdates:(nullable __attribute ( ( noescape ) ) void ( ^ ) ( ))updates completion:(nullable void ( ^ ) ( BOOL finished ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    updates

    The block that performs the relevant insert, delete, reload, or move operations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – waitUntilAllUpdatesAreCommitted +

    + +
    +
    + +
    + + +
    +

    Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread.

    +
    + + + +
    - (void)waitUntilAllUpdatesAreCommitted
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – insertSections: +

    + +
    +
    + +
    + + +
    +

    Inserts one or more sections.

    +
    + + + +
    - (void)insertSections:(NSIndexSet *)sections
    + + + +
    +

    Parameters

    + + + + + + + +
    sections

    An index set that specifies the sections to insert.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – deleteSections: +

    + +
    +
    + +
    + + +
    +

    Deletes one or more sections.

    +
    + + + +
    - (void)deleteSections:(NSIndexSet *)sections
    + + + +
    +

    Parameters

    + + + + + + + +
    sections

    An index set that specifies the sections to delete.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – reloadSections: +

    + +
    +
    + +
    + + +
    +

    Reloads the specified sections.

    +
    + + + +
    - (void)reloadSections:(NSIndexSet *)sections
    + + + +
    +

    Parameters

    + + + + + + + +
    sections

    An index set that specifies the sections to reload.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – moveSection:toSection: +

    + +
    +
    + +
    + + +
    +

    Moves a section to a new location.

    +
    + + + +
    - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    section

    The index of the section to move.

    newSection

    The index that is the destination of the move for the section.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – insertItemsAtIndexPaths: +

    + +
    +
    + +
    + + +
    +

    Inserts items at the locations identified by an array of index paths.

    +
    + + + +
    - (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of NSIndexPath objects, each representing an item index and section index that together identify an item.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – deleteItemsAtIndexPaths: +

    + +
    +
    + +
    + + +
    +

    Deletes the items specified by an array of index paths.

    +
    + + + +
    - (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of NSIndexPath objects identifying the items to delete.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – reloadItemsAtIndexPaths: +

    + +
    +
    + +
    + + +
    +

    Reloads the specified items.

    +
    + + + +
    - (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPaths

    An array of NSIndexPath objects identifying the items to reload.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – moveItemAtIndexPath:toIndexPath: +

    + +
    +
    + +
    + + +
    +

    Moves the item at a specified location to a destination location.

    +
    + + + +
    - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path identifying the item to move.

    newIndexPath

    The index path that is the destination of the move for the item.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The data source must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – reloadDataWithCompletion: +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadDataWithCompletion:(nullable void ( ^ ) ( ))completion
    + + + +
    +

    Parameters

    + + + + + + + +
    completion

    block to run on completion of asynchronous loading or nil. If supplied, the block is run on +the main thread.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UICollectionView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – reloadData +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadData
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UICollectionView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – relayoutItems +

    + +
    +
    + +
    + + +
    +

    Triggers a relayout of all nodes.

    +
    + + + +
    - (void)relayoutItems
    + + + + + + + + + +
    +

    Discussion

    +

    This method invalidates and lays out every cell node in the collection view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      indexPathsForSelectedItems +

    + +
    +
    + +
    + + +
    +

    The index paths of the selected items, or @c nil if no items are selected.

    +
    + + + +
    @property (nonatomic, readonly, nullable) NSArray<NSIndexPath*> *indexPathsForSelectedItems
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – selectItemAtIndexPath:animated:scrollPosition: +

    + +
    +
    + +
    + + +
    +

    Selects the item at the specified index path and optionally scrolls it into view. +If the allowsSelection property is NO, calling this method has no effect. If there is an existing selection with a different index path and the allowsMultipleSelection property is NO, calling this method replaces the previous selection. +This method does not cause any selection-related delegate methods to be called.

    +
    + + + +
    - (void)selectItemAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    indexPath

    The index path of the item to select. Specifying nil for this parameter clears the current selection.

    animated

    Specify YES to animate the change in the selection or NO to make the change without animating it.

    scrollPosition

    An option that specifies where the item should be positioned when scrolling finishes. For a list of possible values, see UICollectionViewScrollPosition.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – deselectItemAtIndexPath:animated: +

    + +
    +
    + +
    + + +
    +

    Deselects the item at the specified index. +If the allowsSelection property is NO, calling this method has no effect. +This method does not cause any selection-related delegate methods to be called.

    +
    + + + +
    - (void)deselectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path of the item to select. Specifying nil for this parameter clears the current selection.

    animated

    Specify YES to animate the change in the selection or NO to make the change without animating it.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – numberOfItemsInSection: +

    + +
    +
    + +
    + + +
    +

    Retrieves the number of items in the given section.

    +
    + + + +
    - (NSInteger)numberOfItemsInSection:(NSInteger)section
    + + + +
    +

    Parameters

    + + + + + + + +
    section

    The section.

    +
    + + + +
    +

    Return Value

    +

    The number of items.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      numberOfSections +

    + +
    +
    + +
    + + +
    +

    The number of sections.

    +
    + + + +
    @property (nonatomic, readonly) NSInteger numberOfSections
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      visibleNodes +

    + +
    +
    + +
    + + +
    +

    Similar to -visibleCells.

    +
    + + + +
    @property (nonatomic, readonly) NSArray<__kindofASCellNode*> *visibleNodes
    + + + + + +
    +

    Return Value

    +

    an array containing the nodes being displayed on screen. This must be called on the main thread.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – nodeForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Retrieves the node for the item at the given index path.

    +
    + + + +
    - (nullable __kindof ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    The index path of the requested item.

    +
    + + + +
    +

    Return Value

    +

    The node for the given item, or @c nil if no item exists at the specified path.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – indexPathForNode: +

    + +
    +
    + +
    + + +
    +

    Retrieve the index path for the item with the given node.

    +
    + + + +
    - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
    + + + +
    +

    Parameters

    + + + + + + + +
    cellNode

    A node for an item in the collection node.

    +
    + + + +
    +

    Return Value

    +

    The indexPath for this item.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      indexPathsForVisibleItems +

    + +
    +
    + +
    + + +
    +

    Retrieve the index paths of all visible items.

    +
    + + + +
    @property (nonatomic, readonly) NSArray<NSIndexPath*> *indexPathsForVisibleItems
    + + + + + +
    +

    Return Value

    +

    an array containing the index paths of all visible items. This must be called on the main thread.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – indexPathForItemAtPoint: +

    + +
    +
    + +
    + + +
    +

    Retrieve the index path of the item at the given point.

    +
    + + + +
    - (nullable NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point
    + + + +
    +

    Parameters

    + + + + + + + +
    point

    The point of the requested item.

    +
    + + + +
    +

    Return Value

    +

    The indexPath for the item at the given point. This must be called on the main thread.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – cellForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Retrieve the cell at the given index path.

    +
    + + + +
    - (nullable UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    The index path of the requested item.

    +
    + + + +
    +

    Return Value

    +

    The cell for the given index path. This must be called on the main thread.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – contextForSection: +

    + +
    +
    + +
    + + +
    +

    Retrieves the context object for the given section, as provided by the data source in +the @c collectionNode:contextForSection: method.

    +
    + + + +
    - (nullable id<ASSectionContext>)contextForSection:(NSInteger)section
    + + + +
    +

    Parameters

    + + + + + + + +
    section

    The section to get the context for.

    +
    + + + +
    +

    Return Value

    +

    The context object, or @c nil if no context was provided.

    + +

    TODO: This method currently accepts @c section in the view index space, but it should +be in the node index space. To get the context in the view index space (e.g. for subclasses +of @c UICollectionViewLayout, the user will call the same method on @c ASCollectionView.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCollectionView.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCollectionView.html new file mode 100755 index 0000000000..c726f1a145 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASCollectionView.html @@ -0,0 +1,731 @@ + + + + + + ASCollectionView Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionView Class Reference

    + + +
    + + + + + + + +
    Inherits fromUICollectionView
    Declared inASCollectionView.h
    + + + + +
    + +

    Overview

    +

    Asynchronous UICollectionView with Intelligent Preloading capabilities.

    ASCollectionView is a true subclass of UICollectionView, meaning it is pointer-compatible +with code that currently uses UICollectionView.

    + +

    The main difference is that asyncDataSource expects -nodeForItemAtIndexPath, an ASCellNode, and +the sizeForItemAtIndexPath: method is eliminated (as are the performance problems caused by it). +This is made possible because ASCellNodes can calculate their own size, and preload ahead of time.

    Note: ASCollectionNode is strongly recommended over ASCollectionView. This class exists for adoption convenience.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      asyncDelegate +

    + +
    +
    + +
    + + +
    +

    The object that acts as the asynchronous delegate of the collection view

    +
    + + + +
    @property (nonatomic, weak) id<ASCollectionDelegate> asyncDelegate
    + + + + + + + + + +
    +

    Discussion

    +

    The delegate must adopt the ASCollectionDelegate protocol. The collection view maintains a weak reference to the delegate object.

    + +

    The delegate object is responsible for providing size constraints for nodes and indicating whether batch fetching should begin.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      asyncDataSource +

    + +
    +
    + +
    + + +
    +

    The object that acts as the asynchronous data source of the collection view

    +
    + + + +
    @property (nonatomic, weak) id<ASCollectionDataSource> asyncDataSource
    + + + + + + + + + +
    +

    Discussion

    +

    The datasource must adopt the ASCollectionDataSource protocol. The collection view maintains a weak reference to the datasource object.

    + +

    The datasource object is responsible for providing nodes or node creation blocks to the collection view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      collectionNode +

    + +
    +
    + +
    + + +
    +

    Returns the corresponding ASCollectionNode

    +
    + + + +
    @property (nonatomic, weak, readonly) ASCollectionNode *collectionNode
    + + + + + +
    +

    Return Value

    +

    collectionNode The corresponding ASCollectionNode, if one exists.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      leadingScreensForBatching +

    + +
    +
    + +
    + + +
    +

    The number of screens left to scroll before the delegate -collectionView:beginBatchFetchingWithContext: is called.

    +
    + + + +
    @property (nonatomic, assign) CGFloat leadingScreensForBatching
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to two screenfuls.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      layoutInspector +

    + +
    +
    + +
    + + +
    +

    Optional introspection object for the collection view’s layout.

    +
    + + + +
    @property (nonatomic, weak) id<ASCollectionViewLayoutInspecting> layoutInspector
    + + + + + + + + + +
    +

    Discussion

    +

    Since supplementary and decoration views are controlled by the collection view’s layout, this object +is used as a bridge to provide information to the internal data controller about the existence of these views and +their associated index paths. For collection views using UICollectionViewFlowLayout, a default inspector +implementation ASCollectionViewFlowLayoutInspector is created and set on this property by default. Custom +collection view layout subclasses will need to provide their own implementation of an inspector object for their +supplementary views to be compatible with ASCollectionView’s supplementary node support.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – nodeForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Retrieves the node for the item at the given index path.

    +
    + + + +
    - (nullable ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    The index path of the requested node.

    +
    + + + +
    +

    Return Value

    +

    The node at the given index path, or @c nil if no item exists at the specified path.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – supplementaryNodeForElementKind:atIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -supplementaryViewForElementKind:atIndexPath:

    +
    + + + +
    - (nullable ASCellNode *)supplementaryNodeForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    elementKind

    The kind of supplementary node to locate.

    indexPath

    The index path of the requested supplementary node.

    +
    + + + +
    +

    Return Value

    +

    The specified supplementary node or @c nil.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – contextForSection: +

    + +
    +
    + +
    + + +
    +

    Retrieves the context object for the given section, as provided by the data source in +the @c collectionNode:contextForSection: method. This method must be called on the main thread.

    +
    + + + +
    - (nullable id<ASSectionContext>)contextForSection:(NSInteger)section
    + + + +
    +

    Parameters

    + + + + + + + +
    section

    The section to get the context for.

    +
    + + + +
    +

    Return Value

    +

    The context object, or @c nil if no context was provided.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      scrollDirection +

    + +
    +
    + +
    + + +
    +

    Determines collection view’s current scroll direction. Supports 2-axis collection views.

    +
    + + + +
    @property (nonatomic, readonly) ASScrollDirection scrollDirection
    + + + + + +
    +

    Return Value

    +

    a bitmask of ASScrollDirection values.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      scrollableDirections +

    + +
    +
    + +
    + + +
    +

    Determines collection view’s scrollable directions.

    +
    + + + +
    @property (nonatomic, readonly) ASScrollDirection scrollableDirections
    + + + + + +
    +

    Return Value

    +

    a bitmask of ASScrollDirection values.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

      zeroContentInsets +

    + +
    +
    + +
    + + +
    +

    Forces the .contentInset to be UIEdgeInsetsZero.

    +
    + + + +
    @property (nonatomic) BOOL zeroContentInsets
    + + + + + + + + + +
    +

    Discussion

    +

    By default, UIKit sets the top inset to the navigation bar height, even for horizontally +scrolling views. This can only be disabled by setting a property on the containing UIViewController, +automaticallyAdjustsScrollViewInsets, which may not be accessible. ASPagerNode uses this to ensure +its flow layout behaves predictably and does not log undefined layout warnings.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASControlNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASControlNode.html new file mode 100755 index 0000000000..62781e4332 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASControlNode.html @@ -0,0 +1,735 @@ + + + + + + ASControlNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASControlNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Declared inASControlNode.h
    + + + + +
    + +

    Overview

    +

    ASControlNode cannot be used directly. It instead defines the common interface and behavior structure for all its subclasses. Subclasses should import “ASControlNode+Subclasses.h” for information on methods intended to be overriden.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      enabled +

    + +
    +
    + +
    + + +
    +

    Indicates whether or not the receiver is enabled.

    +
    + + + +
    @property (nonatomic, assign, getter=isEnabled) BOOL enabled
    + + + + + + + + + +
    +

    Discussion

    +

    Specify YES to make the control enabled; otherwise, specify NO to make it disabled. The default value is YES. If the enabled state is NO, the control ignores touch events and subclasses may draw differently.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

      highlighted +

    + +
    +
    + +
    + + +
    +

    Indicates whether or not the receiver is highlighted.

    +
    + + + +
    @property (nonatomic, assign, getter=isHighlighted) BOOL highlighted
    + + + + + + + + + +
    +

    Discussion

    +

    This is set automatically when the there is a touch inside the control and removed on exit or touch up. This is different from touchInside in that it includes an area around the control, rather than just for touches inside the control.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

      selected +

    + +
    +
    + +
    + + +
    +

    Indicates whether or not the receiver is highlighted.

    +
    + + + +
    @property (nonatomic, assign, getter=isSelected) BOOL selected
    + + + + + + + + + +
    +

    Discussion

    +

    This is set automatically when the receiver is tapped.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

      tracking +

    + +
    +
    + +
    + + +
    +

    Indicates whether or not the receiver is currently tracking touches related to an event.

    +
    + + + +
    @property (nonatomic, readonly, assign, getter=isTracking) BOOL tracking
    + + + + + + + + + +
    +

    Discussion

    +

    YES if the receiver is tracking touches; NO otherwise.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

      touchInside +

    + +
    +
    + +
    + + +
    +

    Indicates whether or not a touch is inside the bounds of the receiver.

    +
    + + + +
    @property (nonatomic, readonly, assign, getter=isTouchInside) BOOL touchInside
    + + + + + + + + + +
    +

    Discussion

    +

    YES if a touch is inside the receiver’s bounds; NO otherwise.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

    – addTarget:action:forControlEvents: +

    + +
    +
    + +
    + + +
    +

    Adds a target-action pair for a particular event (or events).

    +
    + + + +
    - (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEvents
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    target

    The object to which the action message is sent. If this is nil, the responder chain is searched for an object willing to respond to the action message. target is not retained.

    action

    A selector identifying an action message. May optionally include the sender and the event as parameters, in that order. May not be NULL.

    controlEvents

    A bitmask specifying the control events for which the action message is sent. May not be 0. See “Control Events” for bitmask constants.

    +
    + + + + + + + +
    +

    Discussion

    +

    You may call this method multiple times, and you may specify multiple target-action pairs for a particular event. Targets are held weakly.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

    – actionsForTarget:forControlEvent: +

    + +
    +
    + +
    + + +
    +

    Returns the actions that are associated with a target and a particular control event.

    +
    + + + +
    - (nullable NSArray<NSString*> *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    target

    The target object. May not be nil.

    controlEvent

    A single constant of type ASControlNodeEvent that specifies a particular user action on the control; for a list of these constants, see “Control Events”. May not be 0 or ASControlNodeEventAllEvents.

    +
    + + + +
    +

    Return Value

    +

    An array of selector names as NSString objects, or nil if there are no action selectors associated with controlEvent.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

    – allTargets +

    + +
    +
    + +
    + + +
    +

    Returns all target objects associated with the receiver.

    +
    + + + +
    - (NSSet *)allTargets
    + + + + + +
    +

    Return Value

    +

    A set of all targets for the receiver. The set may include NSNull to indicate at least one nil target (meaning, the responder chain is searched for a target.)

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

    – removeTarget:action:forControlEvents: +

    + +
    +
    + +
    + + +
    +

    Removes a target-action pair for a particular event.

    +
    + + + +
    - (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(ASControlNodeEvent)controlEvents
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    target

    The target object. Pass nil to remove all targets paired with action and the specified control events.

    action

    A selector identifying an action message. Pass NULL to remove all action messages paired with target.

    controlEvents

    A bitmask specifying the control events associated with target and action. See “Control Events” for bitmask constants. May not be 0.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

    – sendActionsForControlEvents:withEvent: +

    + +
    +
    + +
    + + +
    +

    Sends the actions for the control events for a particular event.

    +
    + + + +
    - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    controlEvents

    A bitmask specifying the control events for which to send actions. See “Control Events” for bitmask constants. May not be 0.

    event

    The event which triggered these control actions. May be nil.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    + +

    – setDefaultFocusAppearance +

    + +
    +
    + +
    + + +
    +

    How the node looks when it isn’t focused. Exposed here so that subclasses can override.

    +
    + + + +
    - (void)setDefaultFocusAppearance
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASDisplayNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASDisplayNode.html new file mode 100755 index 0000000000..caf1107240 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASDisplayNode.html @@ -0,0 +1,2727 @@ + + + + + + ASDisplayNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNode Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromASDealloc2MainObject
    Conforms toASLayoutElement
    Declared inASDisplayNode.h
    + + + + +
    + +

    Overview

    +

    An ASDisplayNode is an abstraction over UIView and CALayer that allows you to perform calculations about a view +hierarchy off the main thread, and could do rendering off the main thread as well.

    + +

    The node API is designed to be as similar as possible to UIView. See the README for examples.

    + +

    Subclassing

    + +

    ASDisplayNode can be subclassed to create a new UI element. The subclass header ASDisplayNode+Subclasses provides +necessary declarations and conveniences.

    + +

    Commons reasons to subclass includes making a UIView property available and receiving a callback after async +display.

    +
    + + + + + +
    + + + + +

    Initializing a node object

    + +
    +
    + +

    – init +

    + +
    +
    + +
    + + +
    +

    Designated initializer.

    +
    + + + +
    - (instancetype)init
    + + + + + +
    +

    Return Value

    +

    An ASDisplayNode instance whose view will be a subclass that enables asynchronous rendering, and passes +through -layout and touch handling methods.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – initWithViewBlock: +

    + +
    +
    + +
    + + +
    +

    Alternative initializer with a block to create the backing view.

    +
    + + + +
    - (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock
    + + + +
    +

    Parameters

    + + + + + + + +
    viewBlock

    The block that will be used to create the backing view.

    +
    + + + +
    +

    Return Value

    +

    An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main +queue. The view will render synchronously and -layout and touch handling methods on the node will not be called.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – initWithViewBlock:didLoadBlock: +

    + +
    +
    + +
    + + +
    +

    Alternative initializer with a block to create the backing view.

    +
    + + + +
    - (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    viewBlock

    The block that will be used to create the backing view.

    didLoadBlock

    The block that will be called after the view created by the viewBlock is loaded

    +
    + + + +
    +

    Return Value

    +

    An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main +queue. The view will render synchronously and -layout and touch handling methods on the node will not be called.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – initWithLayerBlock: +

    + +
    +
    + +
    + + +
    +

    Alternative initializer with a block to create the backing layer.

    +
    + + + +
    - (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock
    + + + +
    +

    Parameters

    + + + + + + + +
    layerBlock

    The block that will be used to create the backing layer.

    +
    + + + +
    +

    Return Value

    +

    An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main +queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – initWithLayerBlock:didLoadBlock: +

    + +
    +
    + +
    + + +
    +

    Alternative initializer with a block to create the backing layer.

    +
    + + + +
    - (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    layerBlock

    The block that will be used to create the backing layer.

    didLoadBlock

    The block that will be called after the layer created by the layerBlock is loaded

    +
    + + + +
    +

    Return Value

    +

    An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main +queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – onDidLoad: +

    + +
    +
    + +
    + + +
    +

    Add a block of work to be performed on the main thread when the node’s view or layer is loaded. Thread safe.

    +
    + + + +
    - (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body
    + + + +
    +

    Parameters

    + + + + + + + +
    body

    The work to be performed when the node is loaded.

    + +

    @precondition The node is not already loaded.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: Be careful not to retain self in body. Change the block parameter list to ^(MYCustomNode *self) {} if you +want to shadow self (e.g. if calling this during init).

    Note: This will only be called the next time the node is loaded. If the node is later added to a subtree of a node +that has shouldRasterizeDescendants=YES, and is unloaded, this block will not be called if it is loaded again.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      synchronous +

    + +
    +
    + +
    + + +
    +

    Returns whether the node is synchronous.

    +
    + + + +
    @property (nonatomic, readonly, assign, getter=isSynchronous) BOOL synchronous
    + + + + + +
    +

    Return Value

    +

    NO if the node wraps a _ASDisplayView, YES otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + + + +

    Getting view and layer

    + +
    +
    + +

      view +

    + +
    +
    + +
    + + +
    +

    Returns a view.

    +
    + + + +
    @property (nonatomic, readonly, strong) UIView *view
    + + + + + + + + + +
    +

    Discussion

    +

    The view property is lazily initialized, similar to UIViewController. +To go the other direction, use ASViewToDisplayNode() in ASDisplayNodeExtras.h.

    Warning: The first access to it must be on the main thread, and should only be used on the main thread thereafter as +well.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      nodeLoaded +

    + +
    +
    + +
    + + +
    +

    Returns whether a node’s backing view or layer is loaded.

    +
    + + + +
    @property (nonatomic, readonly, assign, getter=isNodeLoaded) BOOL nodeLoaded
    + + + + + +
    +

    Return Value

    +

    YES if a view is loaded, or if layerBacked is YES and layer is not nil; NO otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      layerBacked +

    + +
    +
    + +
    + + +
    +

    Returns whether the node rely on a layer instead of a view.

    +
    + + + +
    @property (nonatomic, assign, getter=isLayerBacked) BOOL layerBacked
    + + + + + +
    +

    Return Value

    +

    YES if the node rely on a layer, NO otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      layer +

    + +
    +
    + +
    + + +
    +

    Returns a layer.

    +
    + + + +
    @property (nonatomic, readonly, strong) CALayer *layer
    + + + + + + + + + +
    +

    Discussion

    +

    The layer property is lazily initialized, similar to the view property. +To go the other direction, use ASLayerToDisplayNode() in ASDisplayNodeExtras.h.

    Warning: The first access to it must be on the main thread, and should only be used on the main thread thereafter as +well.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      visible +

    + +
    +
    + +
    + + +
    +

    Returns YES if the node is – at least partially – visible in a window.

    +
    + + + +
    @property (readonly, getter=isVisible) BOOL visible
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      inPreloadState +

    + +
    +
    + +
    + + +
    +

    Returns YES if the node is in the preloading interface state.

    +
    + + + +
    @property (readonly, getter=isInPreloadState) BOOL inPreloadState
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      inDisplayState +

    + +
    +
    + +
    + + +
    +

    Returns YES if the node is in the displaying interface state.

    +
    + + + +
    @property (readonly, getter=isInDisplayState) BOOL inDisplayState
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      interfaceState +

    + +
    +
    + +
    + + +
    +

    Returns the Interface State of the node.

    +
    + + + +
    @property (readonly) ASInterfaceState interfaceState
    + + + + + +
    +

    Return Value

    +

    The current ASInterfaceState of the node, indicating whether it is visible and other situational properties.

    +
    + + + + + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + + + +

    Managing dimensions

    + +
    +
    + +

    – layoutThatFits: +

    + +
    +
    + +
    + + +
    +

    Asks the node to return a layout based on given size range.

    +
    + + + +
    - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The minimum and maximum sizes the receiver should fit in.

    +
    + + + +
    +

    Return Value

    +

    An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used).

    +
    + + + + + +
    +

    Discussion

    +

    Though this method does not set the bounds of the view, it does have side effects–caching both the +constraint and the result.

    Warning: Subclasses must not override this; it caches results from -calculateLayoutThatFits:. Calling this method may +be expensive if result is not cached.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      layoutSpecBlock +

    + +
    +
    + +
    + + +
    +

    Provides a way to declare a block to provide an ASLayoutSpec without having to subclass ASDisplayNode and +implement layoutSpecThatFits:

    +
    + + + +
    @property (nonatomic, readwrite, copy, nullable) ASLayoutSpecBlock layoutSpecBlock
    + + + + + +
    +

    Return Value

    +

    A block that takes a constrainedSize ASSizeRange argument, and must return an ASLayoutSpec that includes all +of the subnodes to position in the layout. This input-output relationship is identical to the subclass override +method -layoutSpecThatFits:

    +
    + + + + + +
    +

    Discussion

    +

    Warning: Subclasses that implement -layoutSpecThatFits: must not also use .layoutSpecBlock. Doing so will trigger +an exception. A future version of the framework may support using both, calling them serially, with the +.layoutSpecBlock superseding any values set by the method override.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      calculatedSize +

    + +
    +
    + +
    + + +
    +

    Return the calculated size.

    +
    + + + +
    @property (nonatomic, readonly, assign) CGSize calculatedSize
    + + + + + +
    +

    Return Value

    +

    Size already calculated by -calculateLayoutThatFits:.

    +
    + + + + + +
    +

    Discussion

    +

    Ideal for use by subclasses in -layout, having already prompted their subnodes to calculate their size by +calling -measure: on them in -calculateLayoutThatFits.

    Warning: Subclasses must not override this; it returns the last cached measurement and is never expensive.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      constrainedSizeForCalculatedLayout +

    + +
    +
    + +
    + + +
    +

    Return the constrained size range used for calculating layout.

    +
    + + + +
    @property (nonatomic, readonly, assign) ASSizeRange constrainedSizeForCalculatedLayout
    + + + + + +
    +

    Return Value

    +

    The minimum and maximum constrained sizes used by calculateLayoutThatFits:.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + + + +

    Managing the nodes hierarchy

    + +
    +
    + +

    – addSubnode: +

    + +
    +
    + +
    + + +
    +

    Add a node as a subnode to this node.

    +
    + + + +
    - (void)addSubnode:(ASDisplayNode *)subnode
    + + + +
    +

    Parameters

    + + + + + + + +
    subnode

    The node to be added.

    +
    + + + + + + + +
    +

    Discussion

    +

    The subnode’s view will automatically be added to this node’s view, lazily if the views are not created +yet.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – insertSubnode:belowSubnode: +

    + +
    +
    + +
    + + +
    +

    Insert a subnode before a given subnode in the list.

    +
    + + + +
    - (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    subnode

    The node to insert below another node.

    below

    The sibling node that will be above the inserted node.

    +
    + + + + + + + +
    +

    Discussion

    +

    If the views are loaded, the subnode’s view will be inserted below the given node’s view in the hierarchy +even if there are other non-displaynode views.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – insertSubnode:aboveSubnode: +

    + +
    +
    + +
    + + +
    +

    Insert a subnode after a given subnode in the list.

    +
    + + + +
    - (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    subnode

    The node to insert below another node.

    above

    The sibling node that will be behind the inserted node.

    +
    + + + + + + + +
    +

    Discussion

    +

    If the views are loaded, the subnode’s view will be inserted above the given node’s view in the hierarchy +even if there are other non-displaynode views.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – insertSubnode:atIndex: +

    + +
    +
    + +
    + + +
    +

    Insert a subnode at a given index in subnodes.

    +
    + + + +
    - (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    subnode

    The node to insert.

    idx

    The index in the array of the subnodes property at which to insert the node. Subnodes indices start at 0 +and cannot be greater than the number of subnodes.

    +
    + + + + + + + +
    +

    Discussion

    +

    If this node’s view is loaded, ASDisplayNode insert the subnode’s view after the subnode at index - 1’s +view even if there are other non-displaynode views.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – replaceSubnode:withSubnode: +

    + +
    +
    + +
    + + +
    +

    Replace subnode with replacementSubnode.

    +
    + + + +
    - (void)replaceSubnode:(ASDisplayNode *)subnode withSubnode:(ASDisplayNode *)replacementSubnode
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    subnode

    A subnode of self.

    replacementSubnode

    A node with which to replace subnode.

    +
    + + + + + + + +
    +

    Discussion

    +

    Should both subnode and replacementSubnode already be subnodes of self, subnode is removed and +replacementSubnode inserted in its place. +If subnode is not a subnode of self, this method will throw an exception. +If replacementSubnode is nil, this method will throw an exception

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – removeFromSupernode +

    + +
    +
    + +
    + + +
    +

    Remove this node from its supernode.

    +
    + + + +
    - (void)removeFromSupernode
    + + + + + + + + + +
    +

    Discussion

    +

    The node’s view will be automatically removed from the supernode’s view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      subnodes +

    + +
    +
    + +
    + + +
    +

    The receiver’s immediate subnodes.

    +
    + + + +
    @property (nonatomic, readonly, copy) NSArray<ASDisplayNode*> *subnodes
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      supernode +

    + +
    +
    + +
    + + +
    +

    The receiver’s supernode.

    +
    + + + +
    @property (nonatomic, readonly, weak) ASDisplayNode *supernode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + + + +

    Drawing and Updating the View

    + +
    +
    + +

      displaysAsynchronously +

    + +
    +
    + +
    + + +
    +

    Whether this node’s view performs asynchronous rendering.

    +
    + + + +
    @property (nonatomic, assign) BOOL displaysAsynchronously
    + + + + + +
    +

    Return Value

    +

    Defaults to YES, except for synchronous views (ie, those created with -initWithViewBlock: / +-initWithLayerBlock:), which are always NO.

    +
    + + + + + +
    +

    Discussion

    +

    If this flag is set, then the node will participate in the current asyncdisplaykit_async_transaction and +do its rendering on the displayQueue instead of the main thread.

    + +

    Asynchronous rendering proceeds as follows:

    + +

    When the view is initially added to the hierarchy, it has -needsDisplay true. +After layout, Core Animation will call -display on the _ASDisplayLayer +-display enqueues a rendering operation on the displayQueue +When the render block executes, it calls the delegate display method (-drawRect:… or -display) +The delegate provides contents via this method and an operation is added to the asyncdisplaykit_async_transaction +Once all rendering is complete for the current asyncdisplaykit_async_transaction, +the completion for the block sets the contents on all of the layers in the same frame

    + +

    If asynchronous rendering is disabled:

    + +

    When the view is initially added to the hierarchy, it has -needsDisplay true. +After layout, Core Animation will call -display on the _ASDisplayLayer +-display calls delegate display method (-drawRect:… or -display) immediately +-display sets the layer contents immediately with the result

    + +

    Note: this has nothing to do with -[CALayer drawsAsynchronously].

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      shouldRasterizeDescendants +

    + +
    +
    + +
    + + +
    +

    Whether to draw all descendant nodes' layers/views into this node’s layer/view’s backing store.

    +
    + + + +
    @property (nonatomic, assign) BOOL shouldRasterizeDescendants
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      displaySuspended +

    + +
    +
    + +
    + + +
    +

    Prevent the node’s layer from displaying.

    +
    + + + +
    @property (nonatomic, assign) BOOL displaySuspended
    + + + + + + + + + +
    +

    Discussion

    +

    A subclass may check this flag during -display or -drawInContext: to cancel a display that is already in +progress.

    + +

    Defaults to NO. Does not control display for any child or descendant nodes; for that, use +-recursivelySetDisplaySuspended:.

    + +

    If a setNeedsDisplay occurs while displaySuspended is YES, and displaySuspended is set to NO, then the +layer will be automatically displayed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      shouldAnimateSizeChanges +

    + +
    +
    + +
    + + +
    +

    Whether size changes should be animated. Default to YES.

    +
    + + + +
    @property (nonatomic, assign) BOOL shouldAnimateSizeChanges
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – recursivelySetDisplaySuspended: +

    + +
    +
    + +
    + + +
    +

    Prevent the node and its descendants' layer from displaying.

    +
    + + + +
    - (void)recursivelySetDisplaySuspended:(BOOL)flag
    + + + +
    +

    Parameters

    + + + + + + + +
    flag

    YES if display should be prevented or cancelled; NO otherwise.

    +
    + + + + + + + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – recursivelyClearContents +

    + +
    +
    + +
    + + +
    +

    Calls -clearContents on the receiver and its subnode hierarchy.

    +
    + + + +
    - (void)recursivelyClearContents
    + + + + + + + + + +
    +

    Discussion

    +

    Clears backing stores and other memory-intensive intermediates. +If the node is removed from a visible hierarchy and then re-added, it will automatically trigger a new asynchronous display, +as long as displaySuspended is not set. +If the node remains in the hierarchy throughout, -setNeedsDisplay is required to trigger a new asynchronous display.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – recursivelyClearFetchedData +

    + +
    +
    + +
    + + +
    +

    Calls -clearFetchedData on the receiver and its subnode hierarchy.

    +
    + + + +
    - (void)recursivelyClearFetchedData
    + + + + + + + + + +
    +

    Discussion

    +

    Clears any memory-intensive fetched content. +This method is used to notify the node that it should purge any content that is both expensive to fetch and to +retain in memory.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – recursivelyFetchData +

    + +
    +
    + +
    + + +
    +

    Calls -fetchData on the receiver and its subnode hierarchy.

    +
    + + + +
    - (void)recursivelyFetchData
    + + + + + + + + + +
    +

    Discussion

    +

    Fetches content from remote sources for the current node and all subnodes.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – setNeedsDataFetch +

    + +
    +
    + +
    + + +
    +

    Triggers a recursive call to fetchData when the node has an interfaceState of ASInterfaceStatePreload

    +
    + + + +
    - (void)setNeedsDataFetch
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      placeholderEnabled +

    + +
    +
    + +
    + + +
    +

    Toggle displaying a placeholder over the node that covers content until the node and all subnodes are +displayed.

    +
    + + + +
    @property (nonatomic, assign) BOOL placeholderEnabled
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to NO.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      placeholderFadeDuration +

    + +
    +
    + +
    + + +
    +

    Set the time it takes to fade out the placeholder when a node’s contents are finished displaying.

    +
    + + + +
    @property (nonatomic, assign) NSTimeInterval placeholderFadeDuration
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to 0 seconds.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      drawingPriority +

    + +
    +
    + +
    + + +
    +

    Determines drawing priority of the node. Nodes with higher priority will be drawn earlier.

    +
    + + + +
    @property (nonatomic, assign) NSInteger drawingPriority
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to ASDefaultDrawingPriority. There may be multiple drawing threads, and some of them may +decide to perform operations in queued order (regardless of drawingPriority)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + + + +

    Hit Testing

    + +
    +
    + +

      hitTestSlop +

    + +
    +
    + +
    + + +
    +

    Bounds insets for hit testing.

    +
    + + + +
    @property (nonatomic, assign) UIEdgeInsets hitTestSlop
    + + + + + + + + + +
    +

    Discussion

    +

    When set to a non-zero inset, increases the bounds for hit testing to make it easier to tap or perform +gestures on this node. Default is UIEdgeInsetsZero.

    + +

    This affects the default implementation of -hitTest and -pointInside, so subclasses should call super if you override +it and want hitTestSlop applied.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – pointInside:withEvent: +

    + +
    +
    + +
    + + +
    +

    Returns a Boolean value indicating whether the receiver contains the specified point.

    +
    + + + +
    - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    point

    A point that is in the receiver’s local coordinate system (bounds).

    event

    The event that warranted a call to this method.

    +
    + + + +
    +

    Return Value

    +

    YES if point is inside the receiver’s bounds; otherwise, NO.

    +
    + + + + + +
    +

    Discussion

    +

    Includes the “slop” factor specified with hitTestSlop.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + + + +

    Converting Between View Coordinate Systems

    + +
    +
    + +

    – convertPoint:toNode: +

    + +
    +
    + +
    + + +
    +

    Converts a point from the receiver’s coordinate system to that of the specified node.

    +
    + + + +
    - (CGPoint)convertPoint:(CGPoint)point toNode:(nullable ASDisplayNode *)node
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    point

    A point specified in the local coordinate system (bounds) of the receiver.

    node

    The node into whose coordinate system point is to be converted.

    +
    + + + +
    +

    Return Value

    +

    The point converted to the coordinate system of node.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – convertPoint:fromNode: +

    + +
    +
    + +
    + + +
    +

    Converts a point from the coordinate system of a given node to that of the receiver.

    +
    + + + +
    - (CGPoint)convertPoint:(CGPoint)point fromNode:(nullable ASDisplayNode *)node
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    point

    A point specified in the local coordinate system (bounds) of node.

    node

    The node with point in its coordinate system.

    +
    + + + +
    +

    Return Value

    +

    The point converted to the local coordinate system (bounds) of the receiver.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – convertRect:toNode: +

    + +
    +
    + +
    + + +
    +

    Converts a rectangle from the receiver’s coordinate system to that of another view.

    +
    + + + +
    - (CGRect)convertRect:(CGRect)rect toNode:(nullable ASDisplayNode *)node
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    rect

    A rectangle specified in the local coordinate system (bounds) of the receiver.

    node

    The node that is the target of the conversion operation.

    +
    + + + +
    +

    Return Value

    +

    The converted rectangle.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – convertRect:fromNode: +

    + +
    +
    + +
    + + +
    +

    Converts a rectangle from the coordinate system of another node to that of the receiver.

    +
    + + + +
    - (CGRect)convertRect:(CGRect)rect fromNode:(nullable ASDisplayNode *)node
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    rect

    A rectangle specified in the local coordinate system (bounds) of node.

    node

    The node with rect in its coordinate system.

    +
    + + + +
    +

    Return Value

    +

    The converted rectangle.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASEditableTextNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASEditableTextNode.html new file mode 100755 index 0000000000..409bc45979 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASEditableTextNode.html @@ -0,0 +1,700 @@ + + + + + + ASEditableTextNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASEditableTextNode Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Conforms toUITextInputTraits
    Declared inASEditableTextNode.h
    + + + + +
    + +

    Overview

    +

    Does not support layer backing.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – init +

    + +
    +
    + +
    + + +
    +

    Initializes an editable text node using default TextKit components.

    +
    + + + +
    - (instancetype)init
    + + + + + +
    +

    Return Value

    +

    An initialized ASEditableTextNode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – initWithTextKitComponents:placeholderTextKitComponents: +

    + +
    +
    + +
    + + +
    +

    Initializes an editable text node using the provided TextKit components.

    +
    + + + +
    - (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    textKitComponents

    The TextKit stack used to render text.

    placeholderTextKitComponents

    The TextKit stack used to render placeholder text.

    +
    + + + +
    +

    Return Value

    +

    An initialized ASEditableTextNode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

      scrollEnabled +

    + +
    +
    + +
    + + +
    +

    Enable scrolling on the textView +@default true

    +
    + + + +
    @property (nonatomic) BOOL scrollEnabled
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

      textView +

    + +
    +
    + +
    + + +
    +

    Access to underlying UITextView for more configuration options.

    +
    + + + +
    @property (nonatomic, readonly, strong) UITextView *textView
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This property should only be used on the main thread and should not be accessed before the editable text node’s view is created.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – isDisplayingPlaceholder +

    + +
    +
    + +
    + + +
    +

    Indicates if the receiver is displaying the placeholder text.

    +
    + + + +
    - (BOOL)isDisplayingPlaceholder
    + + + + + +
    +

    Return Value

    +

    YES if the placeholder is currently displayed; NO otherwise.

    +
    + + + + + +
    +

    Discussion

    +

    To update the placeholder, see the attributedPlaceholderText property.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

      attributedPlaceholderText +

    + +
    +
    + +
    + + +
    +

    The styled placeholder text displayed by the text node while no text is entered

    +
    + + + +
    @property (nonatomic, readwrite, strong, nullable) NSAttributedString *attributedPlaceholderText
    + + + + + + + + + +
    +

    Discussion

    +

    The placeholder is displayed when the user has not entered any text and the keyboard is not visible.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

      attributedText +

    + +
    +
    + +
    + + +
    +

    The styled text displayed by the receiver.

    +
    + + + +
    @property (nonatomic, readwrite, copy, nullable) NSAttributedString *attributedText
    + + + + + + + + + +
    +

    Discussion

    +

    When the placeholder is displayed (as indicated by -isDisplayingPlaceholder), this value is nil. Otherwise, this value is the attributed text the user has entered. This value can be modified regardless of whether the receiver is the first responder (and thus, editing) or not. Changing this value from nil to non-nil will result in the placeholder being hidden, and the new value being displayed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

      textContainerInset +

    + +
    +
    + +
    + + +
    +

    The textContainerInset of both the placeholder and typed textView. This value defaults to UIEdgeInsetsZero.

    +
    + + + +
    @property (nonatomic, readwrite) UIEdgeInsets textContainerInset
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

      autocapitalizationType +

    + +
    +
    + +
    + + +
    +

    properties.

    +
    + + + +
    @property (nonatomic, readwrite, assign) UITextAutocapitalizationType autocapitalizationType
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – isFirstResponder +

    + +
    +
    + +
    + + +
    +

    Indicates whether the receiver’s text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user.

    +
    + + + +
    - (BOOL)isFirstResponder
    + + + + + +
    +

    Return Value

    +

    YES if the receiver’s text view is the first-responder; NO otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – frameForTextRange: +

    + +
    +
    + +
    + + +
    +

    Returns the frame of the given range of characters.

    +
    + + + +
    - (CGRect)frameForTextRange:(NSRange)textRange
    + + + +
    +

    Parameters

    + + + + + + + +
    textRange

    A range of characters.

    +
    + + + +
    +

    Return Value

    +

    A CGRect that is the bounding box of the glyphs covered by the given range of characters, in the coordinate system of the receiver.

    +
    + + + + + +
    +

    Discussion

    +

    This method raises an exception if textRange is not a valid range of characters within the receiver’s attributed text.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASImageNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASImageNode.html new file mode 100755 index 0000000000..387adaa081 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASImageNode.html @@ -0,0 +1,674 @@ + + + + + + ASImageNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASImageNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASImageNode.h
    + + + + +
    + +

    Overview

    +

    Supports cropping, tinting, and arbitrary image modification blocks.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      image +

    + +
    +
    + +
    + + +
    +

    The image to display.

    +
    + + + +
    @property (nullable, nonatomic, strong) UIImage *image
    + + + + + + + + + +
    +

    Discussion

    +

    The node will efficiently display stretchable images by using +the layer’s contentsCenter property. Non-stretchable images work too, of +course.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      placeholderColor +

    + +
    +
    + +
    + + +
    +

    The placeholder color.

    +
    + + + +
    @property (nullable, nonatomic, strong) UIColor *placeholderColor
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      cropEnabled +

    + +
    +
    + +
    + + +
    +

    Indicates whether efficient cropping of the receiver is enabled.

    +
    + + + +
    @property (nonatomic, assign, getter=isCropEnabled) BOOL cropEnabled
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to YES. See -setCropEnabled:recropImmediately:inBounds: for more +information.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      forceUpscaling +

    + +
    +
    + +
    + + +
    +

    Indicates that efficient downsizing of backing store should not be enabled.

    +
    + + + +
    @property (nonatomic, assign) BOOL forceUpscaling
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to NO. @see ASCroppedImageBackingSizeAndDrawRectInBounds for more +information.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      forcedSize +

    + +
    +
    + +
    + + +
    +

    Forces image to be rendered at forcedSize.

    +
    + + + +
    @property (nonatomic, assign) CGSize forcedSize
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to CGSizeZero to indicate that the forcedSize should not be used. +Setting forcedSize to non-CGSizeZero will force the backing of the layer contents to +be forcedSize (automatically adjusted for contentsSize).

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

    – setCropEnabled:recropImmediately:inBounds: +

    + +
    +
    + +
    + + +
    +

    Enables or disables efficient cropping.

    +
    + + + +
    - (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    cropEnabled

    YES to efficiently crop the receiver’s contents such that +contents outside of its bounds are not included; NO otherwise.

    recropImmediately

    If the receiver has an image, YES to redisplay the +receiver immediately; NO otherwise.

    cropBounds

    The bounds into which the receiver will be cropped. Useful +if bounds are to change in response to cropping (but have not yet done so).

    +
    + + + + + + + +
    +

    Discussion

    +

    Efficient cropping is only performed when the receiver’s view’s +contentMode is UIViewContentModeScaleAspectFill. By default, cropping is +enabled. The crop alignment may be controlled via cropAlignmentFactor.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      cropRect +

    + +
    +
    + +
    + + +
    +

    A value that controls how the receiver’s efficient cropping is aligned.

    +
    + + + +
    @property (nonatomic, readwrite, assign) CGRect cropRect
    + + + + + + + + + +
    +

    Discussion

    +

    This value defines a rectangle that is to be featured by the +receiver. The rectangle is specified as a “unit rectangle,” using +fractions of the source image’s width and height, e.g. CGRectMake(0.5, 0, +0.5, 1.0) will feature the full right half a photo. If the cropRect is +empty, the content mode of the receiver will be used to determine its +dimensions, and only the cropRect’s origin will be used for positioning. The +default value of this property is CGRectMake(0.5, 0.5, 0.0, 0.0).

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      imageModificationBlock +

    + +
    +
    + +
    + + +
    +

    An optional block which can perform drawing operations on image +during the display phase.

    +
    + + + +
    @property (nullable, nonatomic, readwrite, copy) asimagenode_modification_block_t imageModificationBlock
    + + + + + + + + + +
    +

    Discussion

    +

    Can be used to add image effects (such as rounding, adding +borders, or other pattern overlays) without extraneous display calls.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

    – setNeedsDisplayWithCompletion: +

    + +
    +
    + +
    + + +
    +

    Marks the receiver as needing display and performs a block after +display has finished.

    +
    + + + +
    - (void)setNeedsDisplayWithCompletion:(nullable void ( ^ ) ( BOOL canceled ))displayCompletionBlock
    + + + +
    +

    Parameters

    + + + + + + + +
    displayCompletionBlock

    The block to be performed after display has +finished. Its canceled property will be YES if display was prevented or +canceled (via displaySuspended); NO otherwise.

    +
    + + + + + + + +
    +

    Discussion

    +

    displayCompletionBlock will be performed on the main-thread. If +displaySuspended is YES, displayCompletionBlock is will be +performed immediately and YES will be passed for canceled.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    + +

      isDefaultFocusAppearance +

    + +
    +
    + +
    + + +
    +

    A bool to track if the current appearance of the node +is the default focus appearance. +Exposed here so the category methods can set it.

    +
    + + + +
    @property (nonatomic, assign) BOOL isDefaultFocusAppearance
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASInsetLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASInsetLayoutSpec.html new file mode 100755 index 0000000000..5349d434f1 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASInsetLayoutSpec.html @@ -0,0 +1,214 @@ + + + + + + ASInsetLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASInsetLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASInsetLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    A layout spec that wraps another layoutElement child, applying insets around it.

    + +

    If the child has a size specified as a fraction, the fraction is resolved against this spec’s parent +size after applying insets.

    + +

    @example ASOuterLayoutSpec contains an ASInsetLayoutSpec with an ASInnerLayoutSpec. Suppose that: +- ASOuterLayoutSpec is 200pt wide. +- ASInnerLayoutSpec specifies its width as 100%. +- The ASInsetLayoutSpec has insets of 10pt on every side. +ASInnerLayoutSpec will have size 180pt, not 200pt, because it receives a parent size that has been adjusted for insets.

    + +

    If you’re familiar with CSS: ASInsetLayoutSpec’s child behaves similarly to “box-sizing: border-box”.

    + +

    An infinite inset is resolved as an inset equal to all remaining space after applying the other insets and child size. +@example An ASInsetLayoutSpec with an infinite left inset and 10px for all other edges will position it’s child 10px from the right edge.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    + insetLayoutSpecWithInsets:child: +

    + +
    +
    + +
    + + +
    +

    The amount of space to inset on each side.

    +
    + + + +
    + (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id<ASLayoutElement>)child
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    insets

    The amount of space to inset on each side.

    child

    The wrapped child to inset.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASInsetLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayout.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayout.html new file mode 100755 index 0000000000..98474e1041 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayout.html @@ -0,0 +1,762 @@ + + + + + + ASLayout Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayout Class Reference

    + + +
    + + + + + + + +
    Inherits fromNSObject
    Declared inASLayout.h
    + + + + +
    + +

    Overview

    +

    A node in the layout tree that represents the size and position of the object that created it (ASLayoutElement).

    +
    + + + + + +
    + + + + + + +
    +
    + +

      layoutElement +

    + +
    +
    + +
    + + +
    +

    The underlying object described by this layout

    +
    + + + +
    @property (nonatomic, weak, readonly) id<ASLayoutElement> layoutElement
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

      type +

    + +
    +
    + +
    + + +
    +

    The type of ASLayoutElement that created this layout

    +
    + + + +
    @property (nonatomic, assign, readonly) ASLayoutElementType type
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

      size +

    + +
    +
    + +
    + + +
    +

    Size of the current layout

    +
    + + + +
    @property (nonatomic, assign, readonly) CGSize size
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

      position +

    + +
    +
    + +
    + + +
    +

    Position in parent. Default to CGPointNull.

    +
    + + + +
    @property (nonatomic, assign, readonly) CGPoint position
    + + + + + + + + + +
    +

    Discussion

    +

    When being used as a sublayout, this property must not equal CGPointNull.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

      sublayouts +

    + +
    +
    + +
    + + +
    +

    Array of ASLayouts. Each must have a valid non-null position.

    +
    + + + +
    @property (nonatomic, copy, readonly) NSArray<ASLayout*> *sublayouts
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

      frame +

    + +
    +
    + +
    + + +
    +

    Returns a valid frame for the current layout computed with the size and position.

    +
    + + + +
    @property (nonatomic, assign, readonly) CGRect frame
    + + + + + + + + + +
    +

    Discussion

    +

    Clamps the layout’s origin or position to 0 if any of the calculated values are infinite.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

    – initWithLayoutElement:size:position:sublayouts: +

    + +
    +
    + +
    + + +
    +

    Designated initializer

    +
    + + + +
    - (instancetype)initWithLayoutElement:(id<ASLayoutElement>)layoutElement size:(CGSize)size position:(CGPoint)position sublayouts:(nullable NSArray<ASLayout*> *)sublayouts
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

    + layoutWithLayoutElement:size:position:sublayouts: +

    + +
    +
    + +
    + + +
    +

    Convenience class initializer for layout construction.

    +
    + + + +
    + (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement size:(CGSize)size position:(CGPoint)position sublayouts:(nullable NSArray<ASLayout*> *)sublayouts
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    layoutElement

    The backing ASLayoutElement object.

    size

    The size of this layout.

    position

    The position of this layout within its parent (if available).

    sublayouts

    Sublayouts belong to the new layout.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

    + layoutWithLayoutElement:size:sublayouts: +

    + +
    +
    + +
    + + +
    +

    Convenience initializer that has CGPointNull position. +Best used by ASDisplayNode subclasses that are manually creating a layout for -calculateLayoutThatFits:, +or for ASLayoutSpec subclasses that are referencing the “self” level in the layout tree, +or for creating a sublayout of which the position is yet to be determined.

    +
    + + + +
    + (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement size:(CGSize)size sublayouts:(nullable NSArray<ASLayout*> *)sublayouts
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    layoutElement

    The backing ASLayoutElement object.

    size

    The size of this layout.

    sublayouts

    Sublayouts belong to the new layout.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

    + layoutWithLayoutElement:size: +

    + +
    +
    + +
    + + +
    +

    Convenience that has CGPointNull position and no sublayouts. +Best used for creating a layout that has no sublayouts, and is either a root one +or a sublayout of which the position is yet to be determined.

    +
    + + + +
    + (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement size:(CGSize)size
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    layoutElement

    The backing ASLayoutElement object.

    size

    The size of this layout.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

    + layoutWithLayout:position: +

    + +
    +
    + +
    + + +
    +

    Convenience initializer that creates a layout based on the values of the given layout, with a new position

    +
    + + + +
    + (instancetype)layoutWithLayout:(ASLayout *)layout position:(CGPoint)position
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    layout

    The layout to use to create the new layout

    position

    The position of the new layout

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    + +

    – filteredNodeLayoutTree +

    + +
    +
    + +
    + + +
    +

    Traverses the existing layout tree and generates a new tree that represents only ASDisplayNode layouts

    +
    + + + +
    - (ASLayout *)filteredNodeLayoutTree
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayout.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayoutElementStyle.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayoutElementStyle.html new file mode 100755 index 0000000000..6a71f721ed --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayoutElementStyle.html @@ -0,0 +1,796 @@ + + + + + + ASLayoutElementStyle Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutElementStyle Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromNSObject
    Conforms toASAbsoluteLayoutElement
    ASStackLayoutElement
    Declared inASLayoutElement.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – initWithDelegate: +

    + +
    +
    + +
    + + +
    +

    Initializes the layoutElement style with a specified delegate

    +
    + + + +
    - (instancetype)initWithDelegate:(id<ASLayoutElementStyleDelegate>)delegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    The object that acts as the delegate of the style.

    +
    + + + +
    @property (nullable, nonatomic, weak, readonly) id<ASLayoutElementStyleDelegate> delegate
    + + + + + + + + + +
    +

    Discussion

    +

    The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      width +

    + +
    +
    + +
    + + +
    +

    The width property specifies the height of the content area of an ASLayoutElement. +The minWidth and maxWidth properties override width. +Defaults to ASDimensionAuto

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASDimension width
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      height +

    + +
    +
    + +
    + + +
    +

    The height property specifies the height of the content area of an ASLayoutElement +The minHeight and maxHeight properties override height. +Defaults to ASDimensionAuto

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASDimension height
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      minHeight +

    + +
    +
    + +
    + + +
    +

    The minHeight property is used to set the minimum height of a given element. It prevents the used value +of the height property from becoming smaller than the value specified for minHeight. +The value of minHeight overrides both maxHeight and height. +Defaults to ASDimensionAuto

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASDimension minHeight
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      maxHeight +

    + +
    +
    + +
    + + +
    +

    The maxHeight property is used to set the maximum height of an element. It prevents the used value of the +height property from becoming larger than the value specified for maxHeight. +The value of maxHeight overrides height, but minHeight overrides maxHeight. +Defaults to ASDimensionAuto

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASDimension maxHeight
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      minWidth +

    + +
    +
    + +
    + + +
    +

    The minWidth property is used to set the minimum width of a given element. It prevents the used value of +the width property from becoming smaller than the value specified for minWidth. +The value of minWidth overrides both maxWidth and width. +Defaults to ASDimensionAuto

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASDimension minWidth
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      maxWidth +

    + +
    +
    + +
    + + +
    +

    The maxWidth property is used to set the maximum width of a given element. It prevents the used value of +the width property from becoming larger than the value specified for maxWidth. +The value of maxWidth overrides width, but minWidth overrides maxWidth. +Defaults to ASDimensionAuto

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASDimension maxWidth
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      preferredSize +

    + +
    +
    + +
    + + +
    +

    Provides a suggested size for a layout element. If the optional minSize or maxSize are provided, +and the preferredSize exceeds these, the minSize or maxSize will be enforced. If this optional value is not +provided, the layout element’s size will default to it’s intrinsic content size provided calculateSizeThatFits:

    +
    + + + +
    @property (nonatomic, assign) CGSize preferredSize
    + + + + + + + + + +
    +

    Discussion

    +

    This method is optional, but one of either preferredSize or preferredLayoutSize is required +for nodes that either have no intrinsic content size or +should be laid out at a different size than its intrinsic content size. For example, this property could be +set on an ASImageNode to display at a size different from the underlying image size.

    Warning: Calling the getter when the size’s width or height are relative will cause an assert.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      minSize +

    + +
    +
    + +
    + + +
    +

    An optional property that provides a minimum size bound for a layout element. If provided, this restriction will +always be enforced. If a parent layout element’s minimum size is smaller than its child’s minimum size, the child’s +minimum size will be enforced and its size will extend out of the layout spec’s.

    +
    + + + +
    @property (nonatomic, assign) CGSize minSize
    + + + + + + + + + +
    +

    Discussion

    +

    For example, if you set a preferred relative width of 50% and a minimum width of 200 points on an +element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, +since 160 pts is lower than the minimum width of 200 pts, the minimum width would be used.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      maxSize +

    + +
    +
    + +
    + + +
    +

    An optional property that provides a maximum size bound for a layout element. If provided, this restriction will +always be enforced. If a child layout element’s maximum size is smaller than its parent, the child’s maximum size will +be enforced and its size will extend out of the layout spec’s.

    +
    + + + +
    @property (nonatomic, assign) CGSize maxSize
    + + + + + + + + + +
    +

    Discussion

    +

    For example, if you set a preferred relative width of 50% and a maximum width of 120 points on an +element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, +since 160 pts is higher than the maximum width of 120 pts, the maximum width would be used.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      preferredLayoutSize +

    + +
    +
    + +
    + + +
    +

    Provides a suggested RELATIVE size for a layout element. An ASLayoutSize uses percentages rather +than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minLayoutSize or +maxLayoutSize are provided, and the preferredLayoutSize exceeds these, the minLayoutSize or maxLayoutSize +will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size +provided calculateSizeThatFits:

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASLayoutSize preferredLayoutSize
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      minLayoutSize +

    + +
    +
    + +
    + + +
    +

    An optional property that provides a minimum RELATIVE size bound for a layout element. If provided, this +restriction will always be enforced. If a parent layout element’s minimum relative size is smaller than its child’s minimum +relative size, the child’s minimum relative size will be enforced and its size will extend out of the layout spec’s.

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASLayoutSize minLayoutSize
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      maxLayoutSize +

    + +
    +
    + +
    + + +
    +

    An optional property that provides a maximum RELATIVE size bound for a layout element. If provided, this +restriction will always be enforced. If a parent layout element’s maximum relative size is smaller than its child’s maximum +relative size, the child’s maximum relative size will be enforced and its size will extend out of the layout spec’s.

    +
    + + + +
    @property (nonatomic, assign, readwrite) ASLayoutSize maxLayoutSize
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayoutSpec.html new file mode 100755 index 0000000000..3bb92fb29e --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASLayoutSpec.html @@ -0,0 +1,363 @@ + + + + + + ASLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + Texture +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutSpec Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromNSObject
    Conforms toASLayoutElement
    Declared inASLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    A layout spec is an immutable object that describes a layout, loosely inspired by React.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      isMutable +

    + +
    +
    + +
    + + +
    +

    Creation of a layout spec should only happen by a user in layoutSpecThatFits:. During that method, a +layout spec can be created and mutated. Once it is passed back to ASDK, the isMutable flag will be +set to NO and any further mutations will cause an assert.

    +
    + + + +
    @property (nonatomic, assign) BOOL isMutable
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec.h

    +
    + + +
    +
    +
    + +

      parent +

    + +
    +
    + +
    + + +
    +

    Parent of the layout spec

    +
    + + + +
    @property (nullable, nonatomic, weak) id<ASLayoutElement> parent
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec.h

    +
    + + +
    +
    +
    + +

      child +

    + +
    +
    + +
    + + +
    +

    Adds a child to this layout spec using a default identifier.

    +
    + + + +
    @property (nullable, strong, nonatomic) id<ASLayoutElement> child
    + + + +
    +

    Parameters

    + + + + + + + +
    child

    A child to be added.

    +
    + + + + + + + +
    +

    Discussion

    +

    Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the +responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, +only require a single child.

    + +

    For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) +a subclass should use this method to set the “primary” child. It can then use setChild:forIdentifier: +to set any other required children. Ideally a subclass would hide this from the user, and use the +setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild +property that behind the scenes is calling setChild:forIdentifier:.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec.h

    +
    + + +
    +
    +
    + +

      children +

    + +
    +
    + +
    + + +
    +

    Adds childen to this layout spec.

    +
    + + + +
    @property (nullable, strong, nonatomic) NSArray<id<ASLayoutElement> > *children
    + + + +
    +

    Parameters

    + + + + + + + +
    children

    An array of ASLayoutElement children to be added.

    +
    + + + + + + + +
    +

    Discussion

    +

    Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the +reponsibility of holding on to the spec children. Some layout specs, like ASStackLayoutSpec, +can take an unknown number of children. In this case, the this method should be used. +For good measure, in these layout specs it probably makes sense to define +setChild: and setChild:forIdentifier: methods to do something appropriate or to assert.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASMapNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASMapNode.html new file mode 100755 index 0000000000..75a586bc13 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASMapNode.html @@ -0,0 +1,531 @@ + + + + + + ASMapNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASMapNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASImageNode : ASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASMapNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      options +

    + +
    +
    + +
    + + +
    +

    The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change.

    This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise).

    Changes to the region and camera options will only be animated when when the liveMap mode is enabled, otherwise these options will be applied statically to the new snapshot.

    The options object is used to specify properties even when the liveMap mode is enabled, allowing seamless transitions between the snapshot and liveMap (as well as back to the snapshot).

    +
    + + + +
    @property (nonatomic, strong) MKMapSnapshotOptions *options
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      region +

    + +
    +
    + +
    + + +
    +

    The region is simply the sub-field on the options object. If the objects object is reset, + this will in effect be overwritten and become the value of the .region property on that object. + Defaults to MKCoordinateRegionForMapRect(MKMapRectWorld).

    +
    + + + +
    @property (nonatomic, assign) MKCoordinateRegion region
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      mapView +

    + +
    +
    + +
    + + +
    +

    This is the MKMapView that is the live map part of ASMapNode. This will be nil if .liveMap = NO. Note, MKMapView is not thread-safe.

    +
    + + + +
    @property (nullable, nonatomic, readonly) MKMapView *mapView
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      liveMap +

    + +
    +
    + +
    + + +
    +

    Set this to YES to turn the snapshot into an interactive MKMapView and vice versa. Defaults to NO. This property may be set on a background thread before the node is loaded, and will automatically be actioned, once the node is loaded.

    +
    + + + +
    @property (nonatomic, assign, getter=isLiveMap) BOOL liveMap
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      needsMapReloadOnBoundsChange +

    + +
    +
    + +
    + + +
    +

    Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. +@default Default value is YES.

    +
    + + + +
    @property (nonatomic, assign) BOOL needsMapReloadOnBoundsChange
    + + + + + + + + + +
    +

    Discussion

    +

    If mapSize is set then this will be set to NO, since the size will be the same in all orientations.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      mapDelegate +

    + +
    +
    + +
    + + +
    +

    Set the delegate of the MKMapView. This can be set even before mapView is created and will be set on the map in the case that the liveMap mode is engaged.

    +
    + + + +
    @property (nonatomic, weak) id<MKMapViewDelegate> mapDelegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      annotations +

    + +
    +
    + +
    + + +
    +

    The annotations to display on the map.

    +
    + + + +
    @property (nonatomic, copy) NSArray<id<MKAnnotation> > *annotations
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      showAnnotationsOptions +

    + +
    +
    + +
    + + +
    +

    This property specifies how to show the annotations. +@default Default value is ASMapNodeShowAnnotationsIgnored

    +
    + + + +
    @property (nonatomic, assign) ASMapNodeShowAnnotationsOptions showAnnotationsOptions
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    + +

      imageForStaticMapAnnotationBlock +

    + +
    +
    + +
    + + +
    +

    The block which should return annotation image for static map based on provided annotation.

    +
    + + + +
    @property (nonatomic, copy, nullable) UIImage *( ^ ) ( id<MKAnnotation> annotation , CGPoint *centerOffset ) imageForStaticMapAnnotationBlock
    + + + + + + + + + +
    +

    Discussion

    +

    This block is executed on an arbitrary serial queue. If this block is nil, standard pin is used.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMapNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASMultiplexImageNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASMultiplexImageNode.html new file mode 100755 index 0000000000..bb6387c5e7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASMultiplexImageNode.html @@ -0,0 +1,651 @@ + + + + + + ASMultiplexImageNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASMultiplexImageNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASImageNode : ASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASMultiplexImageNode.h
    + + + + +
    + +

    Overview

    +

    ASMultiplexImageNode begins loading images when its resource can either return a UIImage directly, or a URL the image node should load.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – initWithCache:downloader: +

    + +
    +
    + +
    + + +
    +

    The designated initializer.

    +
    + + + +
    - (instancetype)initWithCache:(nullable id<ASImageCacheProtocol>)cache downloader:(nullable id<ASImageDownloaderProtocol>)downloader
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    cache

    The object that implements a cache of images for the image node.

    downloader

    The object that implements image downloading for the image node.

    +
    + + + +
    +

    Return Value

    +

    An initialized ASMultiplexImageNode.

    +
    + + + + + +
    +

    Discussion

    +

    If cache is nil, the receiver will not attempt to retrieve images from a cache before downloading them.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    The delegate, which must conform to the ASMultiplexImageNodeDelegate protocol.

    +
    + + + +
    @property (nonatomic, readwrite, weak) id<ASMultiplexImageNodeDelegate> delegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      dataSource +

    + +
    +
    + +
    + + +
    +

    The data source, which must conform to the ASMultiplexImageNodeDataSource protocol.

    +
    + + + +
    @property (nonatomic, readwrite, weak) id<ASMultiplexImageNodeDataSource> dataSource
    + + + + + + + + + +
    +

    Discussion

    +

    This value is required for ASMultiplexImageNode to load images.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      downloadsIntermediateImages +

    + +
    +
    + +
    + + +
    +

    Whether the receiver should download more than just its highest-quality image. Defaults to NO.

    +
    + + + +
    @property (nonatomic, readwrite, assign) BOOL downloadsIntermediateImages
    + + + + + + + + + +
    +

    Discussion

    +

    ASMultiplexImageNode immediately loads and displays the first image specified in imageIdentifiers (its +highest-quality image). If that image is not immediately available or cached, the node can download and display +lesser-quality images. Set downloadsIntermediateImages to YES to enable this behaviour.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      imageIdentifiers +

    + +
    +
    + +
    + + +
    +

    An array of identifiers representing various versions of an image for ASMultiplexImageNode to display.

    +
    + + + +
    @property (nonatomic, readwrite, copy) NSArray<ASImageIdentifier> *imageIdentifiers
    + + + + + + + + + +
    +

    Discussion

    +

    An identifier can be any object that conforms to NSObject and NSCopying. The array should be in +decreasing order of image quality – that is, the first identifier in the array represents the best version.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – reloadImageIdentifierSources +

    + +
    +
    + +
    + + +
    +

    Notify the receiver SSAA that its data source has new UIImages or NSURLs available for imageIdentifiers.

    +
    + + + +
    - (void)reloadImageIdentifierSources
    + + + + + + + + + +
    +

    Discussion

    +

    If a higher-quality image than is currently displayed is now available, it will be loaded.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      loadedImageIdentifier +

    + +
    +
    + +
    + + +
    +

    The identifier for the last image that the receiver loaded, or nil.

    +
    + + + +
    @property (nullable, nonatomic, readonly) ASImageIdentifier loadedImageIdentifier
    + + + + + + + + + +
    +

    Discussion

    +

    This value may differ from displayedImageIdentifier if the image hasn’t yet been displayed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      displayedImageIdentifier +

    + +
    +
    + +
    + + +
    +

    The identifier for the image that the receiver is currently displaying, or nil.

    +
    + + + +
    @property (nullable, nonatomic, readonly) ASImageIdentifier displayedImageIdentifier
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      shouldRenderProgressImages +

    + +
    +
    + +
    + + +
    +

    If the downloader implements progressive image rendering and this value is YES progressive renders of the +image will be displayed as the image downloads. Regardless of this properties value, progress renders will +only occur when the node is visible. Defaults to YES.

    +
    + + + +
    @property (nonatomic, assign, readwrite) BOOL shouldRenderProgressImages
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

      imageManager +

    + +
    +
    + +
    + + +
    +
      +
    • @abstract The image manager that this image node should use when requesting images from the Photos framework. If this is nil (the default), then PHImageManager.defaultManager is used.
    • +
    + +
    + + + +
    @property (nullable, nonatomic, strong) PHImageManager *imageManager
    + + + + + + + + + +
    +

    Discussion

    +
      +
    • @see +[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:] below.
    • +
    + +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASNavigationController.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASNavigationController.html new file mode 100755 index 0000000000..4dd2da497c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASNavigationController.html @@ -0,0 +1,127 @@ + + + + + + ASNavigationController Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASNavigationController Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromUINavigationController
    Conforms toASManagesChildVisibilityDepth
    Declared inASNavigationController.h
    + + + + +
    + +

    Overview

    +

    ASNavigationController

    ASNavigationController is a drop in replacement for UINavigationController +which improves memory efficiency by implementing the @c ASManagesChildVisibilityDepth protocol. +You can use ASNavigationController with regular UIViewControllers, as well as ASViewControllers. +It is safe to subclass or use even where AsyncDisplayKit is not adopted.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASNetworkImageNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASNetworkImageNode.html new file mode 100755 index 0000000000..36b5f30476 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASNetworkImageNode.html @@ -0,0 +1,633 @@ + + + + + + ASNetworkImageNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASNetworkImageNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASImageNode : ASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASNetworkImageNode.h
    + + + + +
    + +

    Overview

    +

    ASNetworkImageNode is a simple image node that can download and display an image from the network, with support for a +placeholder image (defaultImage). The currently-displayed image is always available in the inherited ASImageNode + property.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – initWithCache:downloader: +

    + +
    +
    + +
    + + +
    +

    The designated initializer. Cache and Downloader are WEAK references.

    +
    + + + +
    - (instancetype)initWithCache:(nullable id<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)downloader
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    cache

    The object that implements a cache of images for the image node. Weak reference.

    downloader

    The object that implements image downloading for the image node. Must not be nil. Weak reference.

    +
    + + + +
    +

    Return Value

    +

    An initialized ASNetworkImageNode.

    +
    + + + + + +
    +

    Discussion

    +

    If cache is nil, the receiver will not attempt to retrieve images from a cache before downloading them.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

    – init +

    + +
    +
    + +
    + + +
    +

    Convenience initialiser.

    +
    + + + +
    - (instancetype)init
    + + + + + +
    +

    Return Value

    +

    An ASNetworkImageNode configured to use the NSURLSession-powered ASBasicImageDownloader, and no extra cache.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    The delegate, which must conform to the ASNetworkImageNodeDelegate protocol.

    +
    + + + +
    @property (nullable, nonatomic, weak, readwrite) id<ASNetworkImageNodeDelegate> delegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      defaultImage +

    + +
    +
    + +
    + + +
    +

    A placeholder image to display while the URL is loading.

    +
    + + + +
    @property (nullable, nonatomic, strong, readwrite) UIImage *defaultImage
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      URL +

    + +
    +
    + +
    + + +
    +

    The URL of a new image to download and display.

    +
    + + + +
    @property (nullable, nonatomic, strong, readwrite) NSURL *URL
    + + + + + + + + + +
    +

    Discussion

    +

    Changing this property will reset the displayed image to a placeholder (defaultImage) while loading.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

    – setURL:resetToDefault: +

    + +
    +
    + +
    + + +
    +

    Download and display a new image.

    +
    + + + +
    - (void)setURL:(nullable NSURL *)URL resetToDefault:(BOOL)reset
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    URL

    The URL of a new image to download and display.

    reset

    Whether to display a placeholder (defaultImage) while loading the new image.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      shouldCacheImage +

    + +
    +
    + +
    + + +
    +

    If URL is a local file, set this property to YES to take advantage of UIKit’s image caching. Defaults to YES.

    +
    + + + +
    @property (nonatomic, assign, readwrite) BOOL shouldCacheImage
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      shouldRenderProgressImages +

    + +
    +
    + +
    + + +
    +

    If the downloader implements progressive image rendering and this value is YES progressive renders of the +image will be displayed as the image downloads. Regardless of this properties value, progress renders will +only occur when the node is visible. Defaults to YES.

    +
    + + + +
    @property (nonatomic, assign, readwrite) BOOL shouldRenderProgressImages
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      currentImageQuality +

    + +
    +
    + +
    + + +
    +

    The image quality of the current image. This is a number between 0 and 1 and can be used to track +progressive progress. Calculated by dividing number of bytes / expected number of total bytes.

    +
    + + + +
    @property (nonatomic, assign, readonly) CGFloat currentImageQuality
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

      renderedImageQuality +

    + +
    +
    + +
    + + +
    +

    The image quality (value between 0 and 1) of the last image that completed displaying.

    +
    + + + +
    @property (nonatomic, assign, readonly) CGFloat renderedImageQuality
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASOverlayLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASOverlayLayoutSpec.html new file mode 100755 index 0000000000..8ebefaec69 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASOverlayLayoutSpec.html @@ -0,0 +1,247 @@ + + + + + + ASOverlayLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASOverlayLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASOverlayLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    This layout spec lays out a single layoutElement child and then overlays a layoutElement object on top of it streched to its size

    +
    + + + + + +
    + + + + + + +
    +
    + +

      overlay +

    + +
    +
    + +
    + + +
    +

    Overlay layoutElement of this layout spec

    +
    + + + +
    @property (nullable, nonatomic, strong) id<ASLayoutElement> overlay
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASOverlayLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + overlayLayoutSpecWithChild:overlay: +

    + +
    +
    + +
    + + +
    +

    Creates and returns an ASOverlayLayoutSpec object with a given child and an layoutElement that act as overlay.

    +
    + + + +
    + (instancetype)overlayLayoutSpecWithChild:(id<ASLayoutElement>)child overlay:(nullable id<ASLayoutElement>)overlay
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    child

    A child that is laid out to determine the size of this spec.

    overlay

    A layoutElement object that is laid out over the child. If this is nil, the overlay is omitted.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASOverlayLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASPagerNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASPagerNode.html new file mode 100755 index 0000000000..b6f529ae77 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASPagerNode.html @@ -0,0 +1,579 @@ + + + + + + ASPagerNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASPagerNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASCollectionNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASPagerNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – init +

    + +
    +
    + +
    + + +
    +

    Configures a default horizontal, paging flow layout with 0 inter-item spacing.

    +
    + + + +
    - (instancetype)init
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – initWithCollectionViewLayout: +

    + +
    +
    + +
    + + +
    +

    Initializer with custom-configured flow layout properties.

    +
    + + + +
    - (instancetype)initWithCollectionViewLayout:(ASPagerFlowLayout *)flowLayout
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – setDataSource: +

    + +
    +
    + +
    + + +
    +

    Data Source is required, and uses a different protocol from ASCollectionNode.

    +
    + + + +
    - (void)setDataSource:(nullable id<ASPagerDataSource>)dataSource
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – dataSource +

    + +
    +
    + +
    + + +
    +

    The object that acts as the asynchronous data source of the collection view

    +
    + + + +
    - (nullable id<ASPagerDataSource>)dataSource
    + + + + + + + + + +
    +

    Discussion

    +

    The datasource must adopt the ASCollectionDataSource protocol. The collection view maintains a weak reference to the datasource object.

    + +

    The datasource object is responsible for providing nodes or node creation blocks to the collection view.

    Note: This is a convenience method which sets the asyncDatasource on the collection node’s collection view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – setDelegate: +

    + +
    +
    + +
    + + +
    +

    Delegate is optional. +This includes UIScrollViewDelegate as well as most methods from UICollectionViewDelegate, like willDisplay…

    +
    + + + +
    - (void)setDelegate:(nullable id<ASPagerDelegate>)delegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – delegate +

    + +
    +
    + +
    + + +
    +

    The object that acts as the asynchronous delegate of the collection view

    +
    + + + +
    - (nullable id<ASPagerDelegate>)delegate
    + + + + + + + + + +
    +

    Discussion

    +

    The delegate must adopt the ASCollectionDelegate protocol. The collection view maintains a weak reference to the delegate object.

    + +

    The delegate object is responsible for providing size constraints for nodes and indicating whether batch fetching should begin.

    Note: This is a convenience method which sets the asyncDelegate on the collection node’s collection view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

      view +

    + +
    +
    + +
    + + +
    +

    The underlying ASCollectionView object.

    +
    + + + +
    @property (nonatomic, readonly) ASCollectionView *view
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

      currentPageIndex +

    + +
    +
    + +
    + + +
    +

    Returns the current page index

    +
    + + + +
    @property (nonatomic, assign, readonly) NSInteger currentPageIndex
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – scrollToPageAtIndex:animated: +

    + +
    +
    + +
    + + +
    +

    Scroll the contents of the receiver to ensure that the page is visible

    +
    + + + +
    - (void)scrollToPageAtIndex:(NSInteger)index animated:(BOOL)animated
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – nodeForPageAtIndex: +

    + +
    +
    + +
    + + +
    +

    Returns the node for the passed page index

    +
    + + + +
    - (ASCellNode *)nodeForPageAtIndex:(NSInteger)index
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRangeController.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRangeController.html new file mode 100755 index 0000000000..ad02f940ee --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRangeController.html @@ -0,0 +1,437 @@ + + + + + + ASRangeController Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRangeController Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromASDealloc2MainObject
    Conforms toASDataControllerDelegate
    Declared inASRangeController.h
    + + + + +
    + +

    Overview

    +

    Working range controller.

    + +

    Used internally by ASTableView and ASCollectionView. It is paired with ASDataController. +It is designed to support custom scrolling containers as well. Observes the visible range, maintains +“working ranges” to trigger network calls and rendering, and is responsible for driving asynchronous layout of cells. +This includes cancelling those asynchronous operations as cells fall outside of the working ranges.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – setNeedsUpdate +

    + +
    +
    + +
    + + +
    +

    Notify the range controller that the visible range has been updated. +This is the primary input call that drives updating the working ranges, and triggering their actions. +The ranges will be updated in the next turn of the main loop, or when -updateIfNeeded is called.

    +
    + + + +
    - (void)setNeedsUpdate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – updateIfNeeded +

    + +
    +
    + +
    + + +
    +

    Update the ranges immediately, if -setNeedsUpdate has been called since the last update. +This is useful because the ranges must be updated immediately after a cell is added +into a table/collection to satisfy interface state API guarantees.

    +
    + + + +
    - (void)updateIfNeeded
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – configureContentView:forCellNode: +

    + +
    +
    + +
    + + +
    +

    Add the sized node for indexPath as a subview of contentView.

    +
    + + + +
    - (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    contentView

    UIView to add a (sized) node’s view to.

    node

    The cell node to be added.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

      layoutController +

    + +
    +
    + +
    + + +
    +

    An object that describes the layout behavior of the ranged component (table view, collection view, etc.)

    +
    + + + +
    @property (nonatomic, strong) id<ASLayoutController> layoutController
    + + + + + + + + + +
    +

    Discussion

    +

    Used primarily for providing the current range of index paths and identifying when the +range controller should invalidate its range.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

      dataSource +

    + +
    +
    + +
    + + +
    +

    The underlying data source for the range controller

    +
    + + + +
    @property (nonatomic, weak) id<ASRangeControllerDataSource> dataSource
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    Delegate for handling range controller events. Must not be nil.

    +
    + + + +
    @property (nonatomic, weak) id<ASRangeControllerDelegate> delegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRatioLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRatioLayoutSpec.html new file mode 100755 index 0000000000..cc159b5851 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRatioLayoutSpec.html @@ -0,0 +1,139 @@ + + + + + + ASRatioLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRatioLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASRatioLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    Ratio layout spec +For when the content should respect a certain inherent ratio but can be scaled (think photos or videos) +The ratio passed is the ratio of height / width you expect

    + +

    For a ratio 0.5, the spec will have a flat rectangle shape

    + +
    + +

    | | +| _ _ |

    + +

    For a ratio 2.0, the spec will be twice as tall as it is wide + _ _ +| | +| | +| | +| |

    + +

    *

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRelativeLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRelativeLayoutSpec.html new file mode 100755 index 0000000000..ebcd558584 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASRelativeLayoutSpec.html @@ -0,0 +1,305 @@ + + + + + + ASRelativeLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRelativeLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASRelativeLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    Lays out a single layoutElement child and positions it within the layout bounds according to vertical and horizontal positional specifiers. +Can position the child at any of the 4 corners, or the middle of any of the 4 edges, as well as the center - similar to “9-part” image areas.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    + relativePositionLayoutSpecWithHorizontalPosition:verticalPosition:sizingOption:child: +

    + +
    +
    + +
    + + +
    +

    convenience constructor for a ASRelativeLayoutSpec

    +
    + + + +
    + (instancetype)relativePositionLayoutSpecWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption child:(id<ASLayoutElement>)child
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    horizontalPosition

    how to position the item on the horizontal (x) axis

    verticalPosition

    how to position the item on the vertical (y) axis

    sizingOption

    how much size to take up

    child

    the child to layout

    +
    + + + +
    +

    Return Value

    +

    a configured ASRelativeLayoutSpec

    +
    + + + + + +
    +

    Discussion

    +

    convenience constructor for a ASRelativeLayoutSpec

    +
    + + + + + + + +
    +

    Declared In

    +

    ASRelativeLayoutSpec.h

    +
    + + +
    +
    +
    + +

    – initWithHorizontalPosition:verticalPosition:sizingOption:child: +

    + +
    +
    + +
    + + +
    +

    convenience initializer for a ASRelativeLayoutSpec

    +
    + + + +
    - (instancetype)initWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption child:(id<ASLayoutElement>)child
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    horizontalPosition

    how to position the item on the horizontal (x) axis

    verticalPosition

    how to position the item on the vertical (y) axis

    sizingOption

    how much size to take up

    child

    the child to layout

    +
    + + + +
    +

    Return Value

    +

    a configured ASRelativeLayoutSpec

    +
    + + + + + +
    +

    Discussion

    +

    convenience initializer for a ASRelativeLayoutSpec

    +
    + + + + + + + +
    +

    Declared In

    +

    ASRelativeLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASScrollNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASScrollNode.html new file mode 100755 index 0000000000..83843991b4 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASScrollNode.html @@ -0,0 +1,183 @@ + + + + + + ASScrollNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASScrollNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Declared inASScrollNode.h
    + + + + +
    + +

    Overview

    +

    Simple node that wraps UIScrollView.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      view +

    + +
    +
    + +
    + + +
    +

    The node’s UIScrollView.

    +
    + + + +
    @property (nonatomic, readonly, strong) UIScrollView *view
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASScrollNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASStackLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASStackLayoutSpec.html new file mode 100755 index 0000000000..80de7f100c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASStackLayoutSpec.html @@ -0,0 +1,649 @@ + + + + + + ASStackLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASStackLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASStackLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    A simple layout spec that stacks a list of children vertically or horizontally.

    + +
      +
    • All children are initially laid out with the an infinite available size in the stacking direction.
    • +
    • In the other direction, this spec’s constraint is passed.
    • +
    • The children’s sizes are summed in the stacking direction. + +
        +
      • If this sum is less than this spec’s minimum size in stacking direction, children with flexGrow are flexed.
      • +
      • If it is greater than this spec’s maximum size in the stacking direction, children with flexShrink are flexed.
      • +
      • If, even after flexing, the sum is still greater than this spec’s maximum size in the stacking direction, +justifyContent determines how children are laid out.
      • +
      +
    • +
    + + +

    For example:

    + +
      +
    • Suppose stacking direction is Vertical, min-width=100, max-width=300, min-height=200, max-height=500.
    • +
    • All children are laid out with min-width=100, max-width=300, min-height=0, max-height=INFINITY.
    • +
    • If the sum of the childrens' heights is less than 200, children with flexGrow are flexed larger.
    • +
    • If the sum of the childrens' heights is greater than 500, children with flexShrink are flexed smaller. + Each child is shrunk by ((sum of heights) - 500)/(number of flexShrink-able children).
    • +
    • If the sum of the childrens' heights is greater than 500 even after flexShrink-able children are flexed, + justifyContent determines how children are laid out.
    • +
    + +
    + + + + + +
    + + + + + + +
    +
    + +

      direction +

    + +
    +
    + +
    + + +
    +

    Specifies the direction children are stacked in. If horizontalAlignment and verticalAlignment were set, +they will be resolved again, causing justifyContent and alignItems to be updated accordingly

    +
    + + + +
    @property (nonatomic, assign) ASStackLayoutDirection direction
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

      spacing +

    + +
    +
    + +
    + + +
    +

    The amount of space between each child.

    +
    + + + +
    @property (nonatomic, assign) CGFloat spacing
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

      horizontalAlignment +

    + +
    +
    + +
    + + +
    +

    Specifies how children are aligned horizontally. Depends on the stack direction, setting the alignment causes either +justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. +Thus, it is preferred to those properties

    +
    + + + +
    @property (nonatomic, assign) ASHorizontalAlignment horizontalAlignment
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

      verticalAlignment +

    + +
    +
    + +
    + + +
    +

    Specifies how children are aligned vertically. Depends on the stack direction, setting the alignment causes either +justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. +Thus, it is preferred to those properties

    +
    + + + +
    @property (nonatomic, assign) ASVerticalAlignment verticalAlignment
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

      justifyContent +

    + +
    +
    + +
    + + +
    +

    The amount of space between each child.

    +
    + + + +
    @property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

      alignItems +

    + +
    +
    + +
    + + +
    +

    Orientation of children along cross axis

    +
    + + + +
    @property (nonatomic, assign) ASStackLayoutAlignItems alignItems
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

      baselineRelativeArrangement +

    + +
    +
    + +
    + + +
    +

    If YES the vertical spacing between two views is measured from the last baseline of the top view to the top of the bottom view

    +
    + + + +
    @property (nonatomic, assign) BOOL baselineRelativeArrangement
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + stackLayoutSpecWithDirection:spacing:justifyContent:alignItems:children: +

    + +
    +
    + +
    + + +
    +

    The direction of the stack view (horizontal or vertical)

    +
    + + + +
    + (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray<id<ASLayoutElement> > *)children
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    direction

    The direction of the stack view (horizontal or vertical)

    spacing

    The spacing between the children

    justifyContent

    If no children are flexible, this describes how to fill any extra space

    alignItems

    Orientation of the children along the cross axis

    children

    ASLayoutElement children to be positioned.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + verticalStackLayoutSpec +

    + +
    +
    + +
    + + +
    +

    A stack layout spec with direction of ASStackLayoutDirectionVertical

    +
    + + + +
    + (instancetype)verticalStackLayoutSpec
    + + + + + +
    +

    Return Value

    +

    A stack layout spec with direction of ASStackLayoutDirectionVertical

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    + +

    + horizontalStackLayoutSpec +

    + +
    +
    + +
    + + +
    +

    A stack layout spec with direction of ASStackLayoutDirectionHorizontal

    +
    + + + +
    + (instancetype)horizontalStackLayoutSpec
    + + + + + +
    +

    Return Value

    +

    A stack layout spec with direction of ASStackLayoutDirectionHorizontal

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutSpec.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTabBarController.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTabBarController.html new file mode 100755 index 0000000000..851476ab61 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTabBarController.html @@ -0,0 +1,125 @@ + + + + + + ASTabBarController Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTabBarController Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromUITabBarController
    Conforms toASManagesChildVisibilityDepth
    Declared inASTabBarController.h
    + + + + +
    + +

    Overview

    +

    ASTabBarController

    ASTabBarController is a drop in replacement for UITabBarController +which implements the memory efficiency improving @c ASManagesChildVisibilityDepth protocol.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTableNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTableNode.html new file mode 100755 index 0000000000..aa37fc25a1 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTableNode.html @@ -0,0 +1,2128 @@ + + + + + + ASTableNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTableNode Class Reference

    + + +
    + + + + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Conforms toASRangeControllerUpdateRangeProtocol
    Declared inASTableNode.h
    + + + + +
    + +

    Overview

    +

    ASTableNode is a node based class that wraps an ASTableView. It can be used +as a subnode of another node, and provide room for many (great) features and improvements later on.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – init +

    + +
    +
    + +
    + + +
    +

    Designated initializer.

    +
    + + + +
    - (instancetype)init
    + + + + + +
    +

    Return Value

    +

    An ASDisplayNode instance whose view will be a subclass that enables asynchronous rendering, and passes +through -layout and touch handling methods.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

      view +

    + +
    +
    + +
    + + +
    +

    Returns a view.

    +
    + + + +
    @property (strong, nonatomic, readonly) ASTableView *view
    + + + + + + + + + +
    +

    Discussion

    +

    The view property is lazily initialized, similar to UIViewController. +To go the other direction, use ASViewToDisplayNode() in ASDisplayNodeExtras.h.

    Warning: The first access to it must be on the main thread, and should only be used on the main thread thereafter as +well.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in full mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in full mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in full mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tuningParametersForRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    rangeMode

    The range mode to get the running parameters for.

    rangeType

    The range type to get the tuning parameters for.

    +
    + + + +
    +

    Return Value

    +

    A tuning parameter value for the given range type in the given mode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – setTuningParameters:forRangeMode:rangeType: +

    + +
    +
    + +
    + + +
    +

    Set the tuning parameters for a range type in the specified mode.

    +
    + + + +
    - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    tuningParameters

    The tuning parameters to store for a range type.

    rangeMode

    The range mode to set the running parameters for.

    rangeType

    The range type to set the tuning parameters for.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – scrollToRowAtIndexPath:atScrollPosition:animated: +

    + +
    +
    + +
    + + +
    +

    Scrolls the table to the given row.

    +
    + + + +
    - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    indexPath

    The index path of the row.

    scrollPosition

    Where the row should end up after the scroll.

    animated

    Whether the scroll should be animated or not.

    + +

    This method must be called on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – reloadDataWithCompletion: +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadDataWithCompletion:(nullable void ( ^ ) ( ))completion
    + + + +
    +

    Parameters

    + + + + + + + +
    completion

    block to run on completion of asynchronous loading or nil. If supplied, the block is run on +the main thread.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UITableView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – reloadData +

    + +
    +
    + +
    + + +
    +

    Reload everything from scratch, destroying the working range and all cached nodes.

    +
    + + + +
    - (void)reloadData
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: This method is substantially more expensive than UITableView’s version.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – relayoutItems +

    + +
    +
    + +
    + + +
    +

    Triggers a relayout of all nodes.

    +
    + + + +
    - (void)relayoutItems
    + + + + + + + + + +
    +

    Discussion

    +

    This method invalidates and lays out every cell node in the table view.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – performBatchAnimated:updates:completion: +

    + +
    +
    + +
    + + +
    +

    Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. +The data source must be updated to reflect the changes before the update block completes.

    +
    + + + +
    - (void)performBatchAnimated:(BOOL)animated updates:(nullable __attribute ( ( noescape ) ) void ( ^ ) ( ))updates completion:(nullable void ( ^ ) ( BOOL finished ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    animated

    NO to disable animations for this batch

    updates

    The block that performs the relevant insert, delete, reload, or move operations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – performBatchUpdates:completion: +

    + +
    +
    + +
    + + +
    +

    Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. +The data source must be updated to reflect the changes before the update block completes.

    +
    + + + +
    - (void)performBatchUpdates:(nullable __attribute ( ( noescape ) ) void ( ^ ) ( ))updates completion:(nullable void ( ^ ) ( BOOL finished ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    updates

    The block that performs the relevant insert, delete, reload, or move operations.

    completion

    A completion handler block to execute when all of the operations are finished. This block takes a single +Boolean parameter that contains the value YES if all of the related animations completed successfully or +NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – waitUntilAllUpdatesAreCommitted +

    + +
    +
    + +
    + + +
    +

    Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread.

    +
    + + + +
    - (void)waitUntilAllUpdatesAreCommitted
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – insertSections:withRowAnimation: +

    + +
    +
    + +
    + + +
    +

    Inserts one or more sections, with an option to animate the insertion.

    +
    + + + +
    - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    sections

    An index set that specifies the sections to insert.

    animation

    A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – deleteSections:withRowAnimation: +

    + +
    +
    + +
    + + +
    +

    Deletes one or more sections, with an option to animate the deletion.

    +
    + + + +
    - (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    sections

    An index set that specifies the sections to delete.

    animation

    A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – reloadSections:withRowAnimation: +

    + +
    +
    + +
    + + +
    +

    Reloads the specified sections using a given animation effect.

    +
    + + + +
    - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    sections

    An index set that specifies the sections to reload.

    animation

    A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – moveSection:toSection: +

    + +
    +
    + +
    + + +
    +

    Moves a section to a new location.

    +
    + + + +
    - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    section

    The index of the section to move.

    newSection

    The index that is the destination of the move for the section.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – insertRowsAtIndexPaths:withRowAnimation: +

    + +
    +
    + +
    + + +
    +

    Inserts rows at the locations identified by an array of index paths, with an option to animate the insertion.

    +
    + + + +
    - (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPaths

    An array of NSIndexPath objects, each representing a row index and section index that together identify a row.

    animation

    A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – deleteRowsAtIndexPaths:withRowAnimation: +

    + +
    +
    + +
    + + +
    +

    Deletes the rows specified by an array of index paths, with an option to animate the deletion.

    +
    + + + +
    - (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPaths

    An array of NSIndexPath objects identifying the rows to delete.

    animation

    A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – reloadRowsAtIndexPaths:withRowAnimation: +

    + +
    +
    + +
    + + +
    +

    Reloads the specified rows using a given animation effect.

    +
    + + + +
    - (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath*> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPaths

    An array of NSIndexPath objects identifying the rows to reload.

    animation

    A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – moveRowAtIndexPath:toIndexPath: +

    + +
    +
    + +
    + + +
    +

    Moves the row at a specified location to a destination location.

    +
    + + + +
    - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path identifying the row to move.

    newIndexPath

    The index path that is the destination of the move for the row.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes +before this method is called.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – selectRowAtIndexPath:animated:scrollPosition: +

    + +
    +
    + +
    + + +
    +

    Selects a row in the table view identified by index path, optionally scrolling the row to a location in the table view. +This method does not cause any selection-related delegate methods to be called.

    +
    + + + +
    - (void)selectRowAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    indexPath

    An index path identifying a row in the table view.

    animated

    Specify YES to animate the change in the selection or NO to make the change without animating it.

    scrollPosition

    A constant that identifies a relative position in the table view (top, middle, bottom) for the row when scrolling concludes. See UITableViewScrollPosition for descriptions of valid constants.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – numberOfRowsInSection: +

    + +
    +
    + +
    + + +
    +

    Retrieves the number of rows in the given section.

    +
    + + + +
    - (NSInteger)numberOfRowsInSection:(NSInteger)section
    + + + +
    +

    Parameters

    + + + + + + + +
    section

    The section.

    +
    + + + +
    +

    Return Value

    +

    The number of rows.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

      numberOfSections +

    + +
    +
    + +
    + + +
    +

    The number of sections in the table node.

    +
    + + + +
    @property (nonatomic, readonly) NSInteger numberOfSections
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

      visibleNodes +

    + +
    +
    + +
    + + +
    +

    Similar to -visibleCells.

    +
    + + + +
    @property (nonatomic, readonly) NSArray<__kindofASCellNode*> *visibleNodes
    + + + + + +
    +

    Return Value

    +

    an array containing the nodes being displayed on screen. This must be called on the main thread.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – nodeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Retrieves the node for the row at the given index path.

    +
    + + + +
    - (nullable __kindof ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – indexPathForNode: +

    + +
    +
    + +
    + + +
    +

    Similar to -indexPathForCell:.

    +
    + + + +
    - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
    + + + +
    +

    Parameters

    + + + + + + + +
    cellNode

    a node for a row.

    +
    + + + +
    +

    Return Value

    +

    The index path to this row, if it exists.

    +
    + + + + + +
    +

    Discussion

    +

    This method will return @c nil for a node that is still being +displayed in the table view, if the data source has deleted the row. +That is, the node is visible but it no longer corresponds +to any item in the data source and will be removed soon.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – rectForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -[UITableView rectForRowAtIndexPath:]

    +
    + + + +
    - (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    An index path identifying a row in the table view.

    +
    + + + +
    +

    Return Value

    +

    A rectangle defining the area in which the table view draws the row or CGRectZero if indexPath is invalid.

    +
    + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – cellForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -[UITableView cellForRowAtIndexPath:]

    +
    + + + +
    - (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + +
    indexPath

    An index path identifying a row in the table view.

    +
    + + + +
    +

    Return Value

    +

    An object representing a cell of the table, or nil if the cell is not visible or indexPath is out of range.

    +
    + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

      indexPathForSelectedRow +

    + +
    +
    + +
    + + +
    +

    Similar to UITableView.indexPathForSelectedRow

    +
    + + + +
    @property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow
    + + + + + +
    +

    Return Value

    +

    The value of this property is an index path identifying the row and section +indexes of the selected row, or nil if the index path is invalid. If there are multiple selections, +this property contains the first index-path object in the array of row selections; +this object has the lowest index values for section and row.

    +
    + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – indexPathForRowAtPoint: +

    + +
    +
    + +
    + + +
    +

    Similar to -[UITableView indexPathForRowAtPoint:]

    +
    + + + +
    - (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point
    + + + +
    +

    Parameters

    + + + + + + + +
    point

    A point in the local coordinate system of the table view (the table view’€™s bounds).

    +
    + + + +
    +

    Return Value

    +

    An index path representing the row and section associated with point, +or nil if the point is out of the bounds of any row.

    +
    + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – indexPathsForRowsInRect: +

    + +
    +
    + +
    + + +
    +

    Similar to -[UITableView indexPathsForRowsInRect:]

    +
    + + + +
    - (nullable NSArray<NSIndexPath*> *)indexPathsForRowsInRect:(CGRect)rect
    + + + +
    +

    Parameters

    + + + + + + + +
    rect

    A rectangle defining an area of the table view in local coordinates.

    +
    + + + +
    +

    Return Value

    +

    An array of NSIndexPath objects each representing a row and section index identifying a row within rect. +Returns an empty array if there aren’t any rows to return.

    +
    + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – indexPathsForVisibleRows +

    + +
    +
    + +
    + + +
    +

    Similar to -[UITableView indexPathsForVisibleRows]

    +
    + + + +
    - (NSArray<NSIndexPath*> *)indexPathsForVisibleRows
    + + + + + +
    +

    Return Value

    +

    The value of this property is an array of NSIndexPath objects each representing a row index and section index +that together identify a visible row in the table view. If no rows are visible, the value is nil.

    +
    + + + + + +
    +

    Discussion

    +

    This method must be called from the main thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTableView.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTableView.html new file mode 100755 index 0000000000..d1482c155a --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTableView.html @@ -0,0 +1,333 @@ + + + + + + ASTableView Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTableView Class Reference

    + + +
    + + + + + + + +
    Inherits fromUITableView
    Declared inASTableView.h
    + + + + +
    + +

    Overview

    +

    Asynchronous UITableView with Intelligent Preloading capabilities.

    ASTableView is a true subclass of UITableView, meaning it is pointer-compatible with code that +currently uses UITableView

    + +

    The main difference is that asyncDataSource expects -nodeForRowAtIndexPath, an ASCellNode, and +the heightForRowAtIndexPath: method is eliminated (as are the performance problems caused by it). +This is made possible because ASCellNodes can calculate their own size, and preload ahead of time.

    Note: ASTableNode is strongly recommended over ASTableView. This class is provided for adoption convenience.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      tableNode +

    + +
    +
    + +
    + + +
    +

    The corresponding table node, or nil if one does not exist.

    +
    + + + +
    @property (nonatomic, weak, readonly) ASTableNode *tableNode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

    – nodeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Retrieves the node for the row at the given index path.

    +
    + + + +
    - (nullable ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

      automaticallyAdjustsContentOffset +

    + +
    +
    + +
    + + +
    +

    YES to automatically adjust the contentOffset when cells are inserted or deleted “before” +visible cells, maintaining the users' visible scroll position. Currently this feature tracks insertions, moves and deletions of +cells, but section edits are ignored.

    +
    + + + +
    @property (nonatomic) BOOL automaticallyAdjustsContentOffset
    + + + + + + + + + +
    +

    Discussion

    +

    default is NO.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    + +

      leadingScreensForBatching +

    + +
    +
    + +
    + + +
    +

    The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called.

    +
    + + + +
    @property (nonatomic, assign) CGFloat leadingScreensForBatching
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to two screenfuls.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableView.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTextCellNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTextCellNode.html new file mode 100755 index 0000000000..da3f0aa37d --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTextCellNode.html @@ -0,0 +1,316 @@ + + + + + + ASTextCellNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTextCellNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASCellNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASCellNode.h
    + + + + +
    + +

    Overview

    +

    Simple label-style cell node. Read its source for an example of custom ASCellNodes.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – initWithAttributes:insets: +

    + +
    +
    + +
    + + +
    +

    Initializes a text cell with given text attributes and text insets

    +
    + + + +
    - (instancetype)initWithAttributes:(NSDictionary *)textAttributes insets:(UIEdgeInsets)textInsets
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      text +

    + +
    +
    + +
    + + +
    +

    Text to display.

    +
    + + + +
    @property (nonatomic, copy) NSString *text
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      textAttributes +

    + +
    +
    + +
    + + +
    +

    A dictionary containing key-value pairs for text attributes. You can specify the font, text color, text shadow color, and text shadow offset using the keys listed in NSString UIKit Additions Reference.

    +
    + + + +
    @property (nonatomic, copy) NSDictionary *textAttributes
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    + +

      textInsets +

    + +
    +
    + +
    + + +
    +

    The text inset or outset for each edge. The default value is 15.0 horizontal and 11.0 vertical padding.

    +
    + + + +
    @property (nonatomic, assign) UIEdgeInsets textInsets
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCellNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTextNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTextNode.html new file mode 100755 index 0000000000..fab6645fc8 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASTextNode.html @@ -0,0 +1,1338 @@ + + + + + + ASTextNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTextNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASTextNode.h
    + + + + +
    + +

    Overview

    +

    Backed by TextKit.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      attributedText +

    + +
    +
    + +
    + + +
    +

    The styled text displayed by the node.

    +
    + + + +
    @property (nullable, nonatomic, copy) NSAttributedString *attributedText
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to nil, no text is shown. +For inline image attachments, add an attribute of key NSAttachmentAttributeName, with a value of an NSTextAttachment.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      truncationAttributedText +

    + +
    +
    + +
    + + +
    +

    The attributedText to use when the text must be truncated.

    +
    + + + +
    @property (nullable, nonatomic, copy) NSAttributedString *truncationAttributedText
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to a localized ellipsis character.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      additionalTruncationMessage +

    + +
    +
    + +
    + + +
    +

    @summary The second attributed string appended for truncation.

    +
    + + + +
    @property (nullable, nonatomic, copy) NSAttributedString *additionalTruncationMessage
    + + + + + + + + + +
    +

    Discussion

    +

    This string will be highlighted on touches. +@default nil

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      truncationMode +

    + +
    +
    + +
    + + +
    +

    Determines how the text is truncated to fit within the receiver’s maximum size.

    +
    + + + +
    @property (nonatomic, assign) NSLineBreakMode truncationMode
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to NSLineBreakByWordWrapping.

    Note: Setting a truncationMode in attributedString will override the truncation mode set here.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      truncated +

    + +
    +
    + +
    + + +
    +

    If the text node is truncated. Text must have been sized first.

    +
    + + + +
    @property (nonatomic, readonly, assign, getter=isTruncated) BOOL truncated
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      maximumNumberOfLines +

    + +
    +
    + +
    + + +
    +

    The maximum number of lines to render of the text before truncation. +@default 0 (No limit)

    +
    + + + +
    @property (nonatomic, assign) NSUInteger maximumNumberOfLines
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      lineCount +

    + +
    +
    + +
    + + +
    +

    The number of lines in the text. Text must have been sized first.

    +
    + + + +
    @property (nonatomic, readonly, assign) NSUInteger lineCount
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      exclusionPaths +

    + +
    +
    + +
    + + +
    +

    An array of path objects representing the regions where text should not be displayed.

    +
    + + + +
    @property (nullable, nonatomic, strong) NSArray<UIBezierPath*> *exclusionPaths
    + + + + + + + + + +
    +

    Discussion

    +

    The default value of this property is an empty array. You can +assign an array of UIBezierPath objects to exclude text from one or more regions in +the text node’s bounds. You can use this property to have text wrap around images, +shapes or other text like a fancy magazine.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      placeholderEnabled +

    + +
    +
    + +
    + + +
    +

    ASTextNode has a special placeholder behavior when placeholderEnabled is YES.

    +
    + + + +
    @property (nonatomic, assign) BOOL placeholderEnabled
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to NO. When YES, it draws rectangles for each line of text, +following the true shape of the text’s wrapping. This visually mirrors the overall +shape and weight of paragraphs, making the appearance of the finished text less jarring.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      placeholderColor +

    + +
    +
    + +
    + + +
    +

    The placeholder color.

    +
    + + + +
    @property (nullable, nonatomic, strong) UIColor *placeholderColor
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      placeholderInsets +

    + +
    +
    + +
    + + +
    +

    Inset each line of the placeholder.

    +
    + + + +
    @property (nonatomic, assign) UIEdgeInsets placeholderInsets
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      shadowPadding +

    + +
    +
    + +
    + + +
    +

    The number of pixels used for shadow padding on each side of the receiver.

    +
    + + + +
    @property (nonatomic, readonly, assign) UIEdgeInsets shadowPadding
    + + + + + + + + + +
    +

    Discussion

    +

    Each inset will be less than or equal to zero, so that applying +UIEdgeInsetsRect(boundingRectForText, shadowPadding) +will return a CGRect large enough to fit both the text and the appropriate shadow padding.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – rectsForTextRange: +

    + +
    +
    + +
    + + +
    +

    Returns an array of rects bounding the characters in a given text range.

    +
    + + + +
    - (NSArray<NSValue*> *)rectsForTextRange:(NSRange)textRange
    + + + +
    +

    Parameters

    + + + + + + + +
    textRange

    A range of text. Must be valid for the receiver’s string.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to detect all the different rectangles a given range of text occupies. +The rects returned are not guaranteed to be contiguous (for example, if the given text range spans +a line break, the rects returned will be on opposite sides and different lines). The rects returned +are in the coordinate system of the receiver.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – highlightRectsForTextRange: +

    + +
    +
    + +
    + + +
    +

    Returns an array of rects used for highlighting the characters in a given text range.

    +
    + + + +
    - (NSArray<NSValue*> *)highlightRectsForTextRange:(NSRange)textRange
    + + + +
    +

    Parameters

    + + + + + + + +
    textRange

    A range of text. Must be valid for the receiver’s string.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to detect all the different rectangles the highlights of a given range of text occupies. +The rects returned are not guaranteed to be contiguous (for example, if the given text range spans +a line break, the rects returned will be on opposite sides and different lines). The rects returned +are in the coordinate system of the receiver. This method is useful for visual coordination with a +highlighted range of text.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – frameForTextRange: +

    + +
    +
    + +
    + + +
    +

    Returns a bounding rect for the given text range.

    +
    + + + +
    - (CGRect)frameForTextRange:(NSRange)textRange
    + + + +
    +

    Parameters

    + + + + + + + +
    textRange

    A range of text. Must be valid for the receiver’s string.

    +
    + + + + + + + +
    +

    Discussion

    +

    The height of the frame returned is that of the receiver’s line-height; adjustment for +cap-height and descenders is not performed. This method raises an exception if textRange is not +a valid substring range of the receiver’s string.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – trailingRect +

    + +
    +
    + +
    + + +
    +

    Returns the trailing rectangle of space in the receiver, after the final character.

    +
    + + + +
    - (CGRect)trailingRect
    + + + + + + + + + +
    +

    Discussion

    +

    Use this method to detect which portion of the receiver is not occupied by characters. +The rect returned is in the coordinate system of the receiver.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      linkAttributeNames +

    + +
    +
    + +
    + + +
    +

    The set of attribute names to consider links. Defaults to NSLinkAttributeName.

    +
    + + + +
    @property (nonatomic, copy) NSArray<NSString*> *linkAttributeNames
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – linkAttributeValueAtPoint:attributeName:range: +

    + +
    +
    + +
    + + +
    +

    Indicates whether the receiver has an entity at a given point.

    +
    + + + +
    - (nullable id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString *_Nullable *_Nullable)attributeNameOut range:(out NSRange *_Nullable)rangeOut
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    point

    The point, in the receiver’s coordinate system.

    attributeNameOut

    The name of the attribute at the point. Can be NULL.

    rangeOut

    The ultimate range of the found text. Can be NULL.

    +
    + + + +
    +

    Return Value

    +

    YES if an entity exists at point; NO otherwise.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      highlightStyle +

    + +
    +
    + +
    + + +
    +

    The style to use when highlighting text.

    +
    + + + +
    @property (nonatomic, assign) ASTextNodeHighlightStyle highlightStyle
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      highlightRange +

    + +
    +
    + +
    + + +
    +

    The range of text highlighted by the receiver. Changes to this property are not animated by default.

    +
    + + + +
    @property (nonatomic, assign) NSRange highlightRange
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – setHighlightRange:animated: +

    + +
    +
    + +
    + + +
    +

    Set the range of text to highlight, with optional animation.

    +
    + + + +
    - (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    highlightRange

    The range of text to highlight.

    animated

    Whether the text should be highlighted with an animation.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    Responds to actions from links in the text node.

    +
    + + + +
    @property (nonatomic, weak) id<ASTextNodeDelegate> delegate
    + + + + + + + + + +
    +

    Discussion

    +

    The delegate must be set before the node is loaded, and implement + textNode:longPressedLinkAttribute:value:atPoint:textRange: in order for + the long press gesture recognizer to be installed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      longPressCancelsTouches +

    + +
    +
    + +
    + + +
    +

    If YES and a long press is recognized, touches are cancelled. Default is NO

    +
    + + + +
    @property (nonatomic, assign) BOOL longPressCancelsTouches
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

      passthroughNonlinkTouches +

    + +
    +
    + +
    + + +
    +

    if YES will not intercept touches for non-link areas of the text. Default is NO.

    +
    + + + +
    @property (nonatomic, assign) BOOL passthroughNonlinkTouches
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASVideoNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASVideoNode.html new file mode 100755 index 0000000000..f9b2b28315 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASVideoNode.html @@ -0,0 +1,265 @@ + + + + + + ASVideoNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASVideoNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASNetworkImageNode : ASImageNode : ASControlNode : ASDisplayNode : ASDealloc2MainObject
    Declared inASVideoNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      assetURL +

    + +
    +
    + +
    + + +
    +
      +
    • @abstract The URL with which the asset was initialized.
    • +
    • @discussion Setting the URL will overwrite the current asset with a newly created AVURLAsset created from the given URL, and AVAsset *asset will point to that newly created AVURLAsset. Please don’t set both assetURL and asset.
    • +
    • @return Current URL the asset was initialized or nil if no URL was given.
    • +
    + +
    + + + +
    @property (nullable, nonatomic, strong, readwrite) NSURL *assetURL
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

      shouldAutoplay +

    + +
    +
    + +
    + + +
    +

    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
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

      delegate +

    + +
    +
    + +
    + + +
    +

    The delegate, which must conform to the ASNetworkImageNodeDelegate protocol.

    +
    + + + +
    @property (nullable, nonatomic, weak, readwrite) id<ASVideoNodeDelegate,ASNetworkImageNodeDelegate> delegate
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASVideoPlayerNode.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASVideoPlayerNode.html new file mode 100755 index 0000000000..3d6645c850 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASVideoPlayerNode.html @@ -0,0 +1,174 @@ + + + + + + ASVideoPlayerNode Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASVideoPlayerNode Class Reference

    + + +
    + + + + + + + +
    Inherits fromASDisplayNode : ASDealloc2MainObject
    Declared inASVideoPlayerNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

      shouldAutoPlay +

    + +
    +
    + +
    + + +
    +

    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
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASViewController.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASViewController.html new file mode 100755 index 0000000000..d29e08e034 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASViewController.html @@ -0,0 +1,450 @@ + + + + + + ASViewController Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASViewController Class Reference

    + + +
    + + + + + + + +
    Conforms to*
    :
    ASDisplayNode
    DisplayNodeType
    __covariant
    Declared inASViewController.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – initWithNode: +

    + +
    +
    + +
    + + +
    +

    ASViewController Designated initializer.

    +
    + + + +
    - (instancetype)initWithNode:(DisplayNodeType)node
    + + + +
    +

    Parameters

    + + + + + + + +
    node

    An ASDisplayNode which will provide the root view (self.view)

    +
    + + + +
    +

    Return Value

    +

    An ASViewController instance whose root view will be backed by the provided ASDisplayNode.

    +
    + + + + + +
    +

    Discussion

    +

    ASViewController allows you to have a completely node backed heirarchy. It automatically +handles @c ASVisibilityDepth, automatic range mode and propogating @c ASDisplayTraits to contained nodes.

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    + +

      node +

    + +
    +
    + +
    + + +
    +

    node Returns the ASDisplayNode which provides the backing view to the view controller.

    +
    + + + +
    @property (nonatomic, strong, readonly) DisplayNodeType node
    + + + + + +
    +

    Return Value

    +

    node Returns the ASDisplayNode which provides the backing view to the view controller.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    + +

      overrideDisplayTraitsWithTraitCollection +

    + +
    +
    + +
    + + +
    +

    Set this block to customize the ASDisplayTraits returned when the VC transitions to the given traitCollection.

    +
    + + + +
    @property (nonatomic, copy) ASDisplayTraitsForTraitCollectionBlock overrideDisplayTraitsWithTraitCollection
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    + +

      overrideDisplayTraitsWithWindowSize +

    + +
    +
    + +
    + + +
    +

    Set this block to customize the ASDisplayTraits returned when the VC transitions to the given window size.

    +
    + + + +
    @property (nonatomic, copy) ASDisplayTraitsForTraitWindowSizeBlock overrideDisplayTraitsWithWindowSize
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    + +

      interfaceState +

    + +
    +
    + +
    + + +
    +

    Passthrough property to the the .interfaceState of the node.

    +
    + + + +
    @property (nonatomic, readonly) ASInterfaceState interfaceState
    + + + + + +
    +

    Return Value

    +

    The current ASInterfaceState of the node, indicating whether it is visible and other situational properties.

    +
    + + + + + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    + +

    – nodeConstrainedSize +

    + +
    +
    + +
    + + +
    +

    The constrained size used to measure the backing node.

    +
    + + + +
    - (ASSizeRange)nodeConstrainedSize
    + + + + + + + + + +
    +

    Discussion

    +

    Defaults to providing a size range that uses the view controller view’s bounds as +both the min and max definitions. Override this method to provide a custom size range to the +backing node.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASViewController.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASWrapperLayoutSpec.html b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASWrapperLayoutSpec.html new file mode 100755 index 0000000000..844114f859 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Classes/ASWrapperLayoutSpec.html @@ -0,0 +1,123 @@ + + + + + + ASWrapperLayoutSpec Class Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASWrapperLayoutSpec Class Reference

    + + +
    + + + + + + + +
    Inherits fromASLayoutSpec : NSObject
    Declared inASLayoutSpec.h
    + + + + +
    + +

    Overview

    +

    An ASLayoutSpec subclass that can wrap one or more ASLayoutElement and calculates the layout based on the +sizes of the children. If multiple children are provided the size of the biggest child will be used to for +size of this layout spec.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASAbsoluteLayoutSpecSizing.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASAbsoluteLayoutSpecSizing.html new file mode 100755 index 0000000000..ae500f68a5 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASAbsoluteLayoutSpecSizing.html @@ -0,0 +1,175 @@ + + + + + + ASAbsoluteLayoutSpecSizing Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASAbsoluteLayoutSpecSizing Constants Reference

    + + +
    + + + + +
    Declared inASAbsoluteLayoutSpec.h
    + + + + + + + +

    ASAbsoluteLayoutSpecSizing

    + + +
    +

    How much space the spec will take up.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSInteger, ASAbsoluteLayoutSpecSizing ) {
    + +    ASAbsoluteLayoutSpecSizingDefault,
    + +    ASAbsoluteLayoutSpecSizingSizeToFit,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASAbsoluteLayoutSpecSizingDefault
    +
    + + +

    The spec will take up the maximum size possible.

    + + + + + + +

    + Declared In ASAbsoluteLayoutSpec.h. +

    + +
    + +
    ASAbsoluteLayoutSpecSizingSizeToFit
    +
    + + +

    Computes a size for the spec that is the union of all childrens' frames.

    + + + + + + +

    + Declared In ASAbsoluteLayoutSpec.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASAbsoluteLayoutSpec.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASButtonNodeImageAlignment.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASButtonNodeImageAlignment.html new file mode 100755 index 0000000000..d03b419517 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASButtonNodeImageAlignment.html @@ -0,0 +1,175 @@ + + + + + + ASButtonNodeImageAlignment Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASButtonNodeImageAlignment Constants Reference

    + + +
    + + + + +
    Declared inASButtonNode.h
    + + + + + + + +

    ASButtonNodeImageAlignment

    + + +
    +

    Image alignment defines where the image will be placed relative to the text.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSInteger, ASButtonNodeImageAlignment ) {
    + +    ASButtonNodeImageAlignmentBeginning,
    + +    ASButtonNodeImageAlignmentEnd,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASButtonNodeImageAlignmentBeginning
    +
    + + +

    Places the image before the text.

    + + + + + + +

    + Declared In ASButtonNode.h. +

    + +
    + +
    ASButtonNodeImageAlignmentEnd
    +
    + + +

    Places the image after the text.

    + + + + + + +

    + Declared In ASButtonNode.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASButtonNode.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCellNodeVisibilityEvent.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCellNodeVisibilityEvent.html new file mode 100755 index 0000000000..eefd793fcd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCellNodeVisibilityEvent.html @@ -0,0 +1,218 @@ + + + + + + ASCellNodeVisibilityEvent Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCellNodeVisibilityEvent Constants Reference

    + + +
    + + + + +
    Declared inASCellNode.h
    + + + + + + + +

    ASCellNodeVisibilityEvent

    + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent ) {
    + +    ASCellNodeVisibilityEventVisible,
    + +    ASCellNodeVisibilityEventVisibleRectChanged,
    + +    ASCellNodeVisibilityEventInvisible,
    + +    ASCellNodeVisibilityEventWillBeginDragging,
    + +    ASCellNodeVisibilityEventDidEndDragging,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASCellNodeVisibilityEventVisible
    +
    + + +

    Indicates a cell has just became visible

    + + + + + + +

    + Declared In ASCellNode.h. +

    + +
    + +
    ASCellNodeVisibilityEventVisibleRectChanged
    +
    + + +

    Its position (determined by scrollView.contentOffset) has changed while at least 1px remains visible. +It is possible that 100% of the cell is visible both before and after and only its position has changed, +or that the position change has resulted in more or less of the cell being visible. +Use CGRectIntersect between cellFrame and scrollView.bounds to get this rectangle

    + + + + + + +

    + Declared In ASCellNode.h. +

    + +
    + +
    ASCellNodeVisibilityEventInvisible
    +
    + + +

    Indicates a cell is no longer visible

    + + + + + + +

    + Declared In ASCellNode.h. +

    + +
    + +
    ASCellNodeVisibilityEventWillBeginDragging
    +
    + + +

    Indicates user has started dragging the visible cell

    + + + + + + +

    + Declared In ASCellNode.h. +

    + +
    + +
    ASCellNodeVisibilityEventDidEndDragging
    +
    + + +

    Indicates user has ended dragging the visible cell

    + + + + + + +

    + Declared In ASCellNode.h. +

    + +
    + +
    +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCenterLayoutSpecCenteringOptions.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCenterLayoutSpecCenteringOptions.html new file mode 100755 index 0000000000..d99b9e76ed --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCenterLayoutSpecCenteringOptions.html @@ -0,0 +1,213 @@ + + + + + + ASCenterLayoutSpecCenteringOptions Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCenterLayoutSpecCenteringOptions Constants Reference

    + + +
    + + + + +
    Declared inASCenterLayoutSpec.h
    + + + + + + + +

    ASCenterLayoutSpecCenteringOptions

    + + +
    +

    How the child is centered within the spec.

    +
    + + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecCenteringOptions ) {
    + +    ASCenterLayoutSpecCenteringNone = 0,
    + +    ASCenterLayoutSpecCenteringX = 1 < < 0,
    + +    ASCenterLayoutSpecCenteringY = 1 < < 1,
    + +    ASCenterLayoutSpecCenteringXY = ASCenterLayoutSpecCenteringX | ASCenterLayoutSpecCenteringY,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASCenterLayoutSpecCenteringNone
    +
    + + +

    The child is positioned in {0,0} relatively to the layout bounds

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    ASCenterLayoutSpecCenteringX
    +
    + + +

    The child is centered along the X axis

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    ASCenterLayoutSpecCenteringY
    +
    + + +

    The child is centered along the Y axis

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    ASCenterLayoutSpecCenteringXY
    +
    + + +

    Convenience option to center both along the X and Y axis

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASCenterLayoutSpec.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCenterLayoutSpecSizingOptions.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCenterLayoutSpecSizingOptions.html new file mode 100755 index 0000000000..f7dea9c8bc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASCenterLayoutSpecSizingOptions.html @@ -0,0 +1,213 @@ + + + + + + ASCenterLayoutSpecSizingOptions Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCenterLayoutSpecSizingOptions Constants Reference

    + + +
    + + + + +
    Declared inASCenterLayoutSpec.h
    + + + + + + + +

    ASCenterLayoutSpecSizingOptions

    + + +
    +

    How much space the spec will take up.

    +
    + + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecSizingOptions ) {
    + +    ASCenterLayoutSpecSizingOptionDefault = ASRelativeLayoutSpecSizingOptionDefault,
    + +    ASCenterLayoutSpecSizingOptionMinimumX = ASRelativeLayoutSpecSizingOptionMinimumWidth,
    + +    ASCenterLayoutSpecSizingOptionMinimumY = ASRelativeLayoutSpecSizingOptionMinimumHeight,
    + +    ASCenterLayoutSpecSizingOptionMinimumXY = ASRelativeLayoutSpecSizingOptionMinimumSize,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASCenterLayoutSpecSizingOptionDefault
    +
    + + +

    The spec will take up the maximum size possible

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    ASCenterLayoutSpecSizingOptionMinimumX
    +
    + + +

    The spec will take up the minimum size possible along the X axis

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    ASCenterLayoutSpecSizingOptionMinimumY
    +
    + + +

    The spec will take up the minimum size possible along the Y axis

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    ASCenterLayoutSpecSizingOptionMinimumXY
    +
    + + +

    Convenience option to take up the minimum size along both the X and Y axis

    + + + + + + +

    + Declared In ASCenterLayoutSpec.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASCenterLayoutSpec.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASControlNodeEvent.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASControlNodeEvent.html new file mode 100755 index 0000000000..fba63ceab7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASControlNodeEvent.html @@ -0,0 +1,308 @@ + + + + + + ASControlNodeEvent Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASControlNodeEvent Constants Reference

    + + +
    + + + + +
    Declared inASControlNode.h
    + + + + + + + +

    ASControlNodeEvent

    + + +
    +

    These events are identical to their UIControl counterparts.

    +
    + + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASControlNodeEvent ) {
    + +    ASControlNodeEventTouchDown = 1 < < 0,
    + +    ASControlNodeEventTouchDownRepeat = 1 < < 1,
    + +    ASControlNodeEventTouchDragInside = 1 < < 2,
    + +    ASControlNodeEventTouchDragOutside = 1 < < 3,
    + +    ASControlNodeEventTouchUpInside = 1 < < 4,
    + +    ASControlNodeEventTouchUpOutside = 1 < < 5,
    + +    ASControlNodeEventTouchCancel = 1 < < 6,
    + +    ASControlNodeEventPrimaryActionTriggered = 1 < < 13,
    + +    ASControlNodeEventAllEvents = 0 xFFFFFFFF,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASControlNodeEventTouchDown
    +
    + + +

    A touch-down event in the control node.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventTouchDownRepeat
    +
    + + +

    A repeated touch-down event in the control node; for this event the value of the UITouch tapCount method is greater than one.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventTouchDragInside
    +
    + + +

    An event where a finger is dragged inside the bounds of the control node.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventTouchDragOutside
    +
    + + +

    An event where a finger is dragged just outside the bounds of the control.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventTouchUpInside
    +
    + + +

    A touch-up event in the control node where the finger is inside the bounds of the node.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventTouchUpOutside
    +
    + + +

    A touch-up event in the control node where the finger is outside the bounds of the node.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventTouchCancel
    +
    + + +

    A system event canceling the current touches for the control node.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventPrimaryActionTriggered
    +
    + + +

    A system event when the Play/Pause button on the Apple TV remote is pressed.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlNodeEventAllEvents
    +
    + + +

    All events, including system events.

    + + + + + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASControlNode.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASControlState.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASControlState.html new file mode 100755 index 0000000000..64744c12dc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASControlState.html @@ -0,0 +1,180 @@ + + + + + + ASControlState Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASControlState Constants Reference

    + + +
    + + + + +
    Declared inASControlNode.h
    + + + + + + + +

    ASControlState

    + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASControlState ) {
    + +    ASControlStateNormal = 0,
    + +    ASControlStateHighlighted = 1 < < 0,
    + +    ASControlStateDisabled = 1 < < 1,
    + +    ASControlStateSelected = 1 < < 2,
    + +    ASControlStateReserved = 0 xFF000000,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASControlStateNormal
    +
    + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlStateHighlighted
    +
    + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlStateDisabled
    +
    + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlStateSelected
    +
    + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    ASControlStateReserved
    +
    + + +

    + Declared In ASControlNode.h. +

    + +
    + +
    +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASDimensionUnit.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASDimensionUnit.html new file mode 100755 index 0000000000..aabf7a9803 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASDimensionUnit.html @@ -0,0 +1,201 @@ + + + + + + ASDimensionUnit Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDimensionUnit Constants Reference

    + + +
    + + + + +
    Declared inASDimension.h
    + + + + + + + +

    ASDimensionUnit

    + + +
    +

    A dimension relative to constraints to be provided in the future. +A ASDimension can be one of three types:

    + +

    “Auto” - This indicated “I have no opinion” and may be resolved in whatever way makes most sense given the circumstances.

    + +

    “Points” - Just a number. It will always resolve to exactly this amount.

    + +

    “Percent” - Multiplied to a provided parent amount to resolve a final amount.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSInteger, ASDimensionUnit ) {
    + +    ASDimensionUnitAuto,
    + +    ASDimensionUnitPoints,
    + +    ASDimensionUnitFraction,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASDimensionUnitAuto
    +
    + + +

    This indicates “I have no opinion” and may be resolved in whatever way makes most sense given the circumstances.

    + + + + + + +

    + Declared In ASDimension.h. +

    + +
    + +
    ASDimensionUnitPoints
    +
    + + +

    Just a number. It will always resolve to exactly this amount. This is the default type.

    + + + + + + +

    + Declared In ASDimension.h. +

    + +
    + +
    ASDimensionUnitFraction
    +
    + + +

    Multiplied to a provided parent amount to resolve a final amount.

    + + + + + + +

    + Declared In ASDimension.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASDimension.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASDisplayNodePerformanceMeasurementOptions.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASDisplayNodePerformanceMeasurementOptions.html new file mode 100755 index 0000000000..ea8cb0d7ec --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASDisplayNodePerformanceMeasurementOptions.html @@ -0,0 +1,175 @@ + + + + + + ASDisplayNodePerformanceMeasurementOptions Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASDisplayNodePerformanceMeasurementOptions Constants Reference

    + + +
    + + + + +
    Declared inASDisplayNode+Beta.h
    + + + + + + + +

    ASDisplayNodePerformanceMeasurementOptions

    + + +
    +

    Bitmask to indicate what performance measurements the cell should record.

    +
    + + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASDisplayNodePerformanceMeasurementOptions ) {
    + +    ASDisplayNodePerformanceMeasurementOptionLayoutSpec = 1 < < 0,
    + +    ASDisplayNodePerformanceMeasurementOptionLayoutComputation = 1 < < 1,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASDisplayNodePerformanceMeasurementOptionLayoutSpec
    +
    + + +

    Bitmask to indicate what performance measurements the cell should record.

    + + + + + + +

    + Declared In ASDisplayNode+Beta.h. +

    + +
    + +
    ASDisplayNodePerformanceMeasurementOptionLayoutComputation
    +
    + + +

    Bitmask to indicate what performance measurements the cell should record.

    + + + + + + +

    + Declared In ASDisplayNode+Beta.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode+Beta.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASHorizontalAlignment.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASHorizontalAlignment.html new file mode 100755 index 0000000000..cfc38bf091 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASHorizontalAlignment.html @@ -0,0 +1,270 @@ + + + + + + ASHorizontalAlignment Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASHorizontalAlignment Constants Reference

    + + +
    + + + + +
    Declared inASStackLayoutDefines.h
    + + + + + + + +

    ASHorizontalAlignment

    + + +
    +

    Orientation of children along horizontal axis

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASHorizontalAlignment ) {
    + +    ASHorizontalAlignmentNone,
    + +    ASHorizontalAlignmentLeft,
    + +    ASHorizontalAlignmentMiddle,
    + +    ASHorizontalAlignmentRight,
    + +    ASAlignmentLeft = ASHorizontalAlignmentLeft,
    + +    ASAlignmentMiddle = ASHorizontalAlignmentMiddle,
    + +    ASAlignmentRight = ASHorizontalAlignmentRight,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASHorizontalAlignmentNone
    +
    + + +

    No alignment specified. Default value

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASHorizontalAlignmentLeft
    +
    + + +

    Left aligned

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASHorizontalAlignmentMiddle
    +
    + + +

    Center aligned

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASHorizontalAlignmentRight
    +
    + + +

    Right aligned

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASAlignmentLeft
    +
    + + +

    Use ASHorizontalAlignmentLeft instead (Deprecated: Use ASHorizontalAlignmentLeft instead)

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASAlignmentMiddle
    +
    + + +

    Use ASHorizontalAlignmentMiddle instead (Deprecated: Use ASHorizontalAlignmentMiddle instead)

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASAlignmentRight
    +
    + + +

    Use ASHorizontalAlignmentRight instead (Deprecated: Use ASHorizontalAlignmentRight instead)

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutDefines.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASInterfaceState.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASInterfaceState.html new file mode 100755 index 0000000000..ab0706f83a --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASInterfaceState.html @@ -0,0 +1,254 @@ + + + + + + ASInterfaceState Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASInterfaceState Constants Reference

    + + +
    + + + + +
    Declared inASDisplayNode.h
    + + + + + + + +

    ASInterfaceState

    + + +
    +

    Interface state is available on ASDisplayNode and ASViewController, and +allows checking whether a node is in an interface situation where it is prudent to trigger certain +actions: measurement, data loading, display, and visibility (the latter for animations or other onscreen-only effects).

    +
    + + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASInterfaceState ) {
    + +    ASInterfaceStateNone = 0,
    + +    ASInterfaceStateMeasureLayout = 1 < < 0,
    + +    ASInterfaceStatePreload = 1 < < 1,
    + +    ASInterfaceStateDisplay = 1 < < 2,
    + +    ASInterfaceStateVisible = 1 < < 3,
    + +    ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASInterfaceStateNone
    +
    + + +

    The element is not predicted to be onscreen soon and preloading should not be performed

    + + + + + + +

    + Declared In ASDisplayNode.h. +

    + +
    + +
    ASInterfaceStateMeasureLayout
    +
    + + +

    The element may be added to a view soon that could become visible. Measure the layout, including size calculation.

    + + + + + + +

    + Declared In ASDisplayNode.h. +

    + +
    + +
    ASInterfaceStatePreload
    +
    + + +

    The element is likely enough to come onscreen that disk and/or network data required for display should be fetched.

    + + + + + + +

    + Declared In ASDisplayNode.h. +

    + +
    + +
    ASInterfaceStateDisplay
    +
    + + +

    The element is very likely to become visible, and concurrent rendering should be executed for any -setNeedsDisplay.

    + + + + + + +

    + Declared In ASDisplayNode.h. +

    + +
    + +
    ASInterfaceStateVisible
    +
    + + +

    The element is physically onscreen by at least 1 pixel. + In practice, all other bit fields should also be set when this flag is set.

    + + + + + + +

    + Declared In ASDisplayNode.h. +

    + +
    + +
    ASInterfaceStateInHierarchy
    +
    + + +

    The node is not contained in a cell but it is in a window.

    + + + + + + +

    + Declared In ASDisplayNode.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASDisplayNode.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASLayoutElementType.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASLayoutElementType.html new file mode 100755 index 0000000000..0cf6cc9244 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASLayoutElementType.html @@ -0,0 +1,175 @@ + + + + + + ASLayoutElementType Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutElementType Constants Reference

    + + +
    + + + + +
    Declared inASLayoutElement.h
    + + + + + + + +

    ASLayoutElementType

    + + +
    +

    Type of ASLayoutElement

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASLayoutElementType ) {
    + +    ASLayoutElementTypeLayoutSpec,
    + +    ASLayoutElementTypeDisplayNode,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASLayoutElementTypeLayoutSpec
    +
    + + +

    Type of ASLayoutElement

    + + + + + + +

    + Declared In ASLayoutElement.h. +

    + +
    + +
    ASLayoutElementTypeDisplayNode
    +
    + + +

    Type of ASLayoutElement

    + + + + + + +

    + Declared In ASLayoutElement.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASMapNodeShowAnnotationsOptions.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASMapNodeShowAnnotationsOptions.html new file mode 100755 index 0000000000..0f7414e76e --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASMapNodeShowAnnotationsOptions.html @@ -0,0 +1,177 @@ + + + + + + ASMapNodeShowAnnotationsOptions Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASMapNodeShowAnnotationsOptions Constants Reference

    + + +
    + + + + +
    Declared inASMapNode.h
    + + + + + + + +

    ASMapNodeShowAnnotationsOptions

    + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASMapNodeShowAnnotationsOptions ) {
    + +    ASMapNodeShowAnnotationsOptionsIgnored = 0,
    + +    ASMapNodeShowAnnotationsOptionsZoomed = 1 < < 0,
    + +    ASMapNodeShowAnnotationsOptionsAnimated = 1 < < 1,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASMapNodeShowAnnotationsOptionsIgnored
    +
    + + +

    The annotations' positions are ignored, use the region or options specified instead.

    + + + + + + +

    + Declared In ASMapNode.h. +

    + +
    + +
    ASMapNodeShowAnnotationsOptionsZoomed
    +
    + + +

    The annotations' positions are used to calculate the region to show in the map, equivalent to showAnnotations:animated.

    + + + + + + +

    + Declared In ASMapNode.h. +

    + +
    + +
    ASMapNodeShowAnnotationsOptionsAnimated
    +
    + + +

    This will only have an effect if combined with the Zoomed state with liveMap turned on.

    + + + + + + +

    + Declared In ASMapNode.h. +

    + +
    + +
    +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASMultiplexImageNodeErrorCode.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASMultiplexImageNodeErrorCode.html new file mode 100755 index 0000000000..3843f6a12f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASMultiplexImageNodeErrorCode.html @@ -0,0 +1,217 @@ + + + + + + ASMultiplexImageNodeErrorCode Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASMultiplexImageNodeErrorCode Constants Reference

    + + +
    + + + + +
    Declared inASMultiplexImageNode.h
    + + + + + + + +

    ASMultiplexImageNodeErrorCode

    + + +
    +

    ASMultiplexImageNode error codes.

    +
    + + + + +
    +

    Constants

    +
    + +
    ASMultiplexImageNodeErrorCodeNoSourceForImage
    +
    + + +

    Indicates that the data source didn’t provide a source for an image identifier.

    + + + + + + +

    + Declared In ASMultiplexImageNode.h. +

    + +
    + +
    ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged
    +
    + + +

    Indicates that the best image identifier changed before a download for a worse identifier began.

    + + + + + + +

    + Declared In ASMultiplexImageNode.h. +

    + +
    + +
    ASMultiplexImageNodeErrorCodePhotosImageManagerFailedWithoutError
    +
    + + +

    Indicates that the Photos framework returned no image and no error. +This may happen if the image is in iCloud and the user did not specify allowsNetworkAccess +in their image request.

    + + + + + + +

    + Declared In ASMultiplexImageNode.h. +

    + +
    + +
    ASMultiplexImageNodeErrorCodePHAssetIsUnavailable
    +
    + + +

    Indicates that the image node could not retrieve the PHAsset for a given asset identifier. +This typically means that the user has not given Photos framework permissions yet or the asset +has been removed from the device.

    + + + + + + +

    + Declared In ASMultiplexImageNode.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeDimensionType.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeDimensionType.html new file mode 100755 index 0000000000..f700916c7f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeDimensionType.html @@ -0,0 +1,201 @@ + + + + + + ASRelativeDimensionType Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRelativeDimensionType Constants Reference

    + + +
    + + + + +
    Declared inASDimension.h
    + + + + + + + +

    ASRelativeDimensionType

    + + +
    +

    A dimension relative to constraints to be provided in the future. +A ASDimension can be one of three types:

    + +

    “Auto” - This indicated “I have no opinion” and may be resolved in whatever way makes most sense given the circumstances.

    + +

    “Points” - Just a number. It will always resolve to exactly this amount.

    + +

    “Percent” - Multiplied to a provided parent amount to resolve a final amount.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSInteger, ASRelativeDimensionType ) {
    + +    ASRelativeDimensionTypeAuto,
    + +    ASRelativeDimensionTypePoints,
    + +    ASRelativeDimensionTypeFraction,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASRelativeDimensionTypeAuto
    +
    + + +

    This indicates “I have no opinion” and may be resolved in whatever way makes most sense given the circumstances.

    + + + + + + +

    + Declared In ASDimension.h. +

    + +
    + +
    ASRelativeDimensionTypePoints
    +
    + + +

    Just a number. It will always resolve to exactly this amount. This is the default type.

    + + + + + + +

    + Declared In ASDimension.h. +

    + +
    + +
    ASRelativeDimensionTypeFraction
    +
    + + +

    Multiplied to a provided parent amount to resolve a final amount.

    + + + + + + +

    + Declared In ASDimension.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASDimension.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeLayoutSpecPosition.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeLayoutSpecPosition.html new file mode 100755 index 0000000000..3f78b744af --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeLayoutSpecPosition.html @@ -0,0 +1,213 @@ + + + + + + ASRelativeLayoutSpecPosition Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRelativeLayoutSpecPosition Constants Reference

    + + +
    + + + + +
    Declared inASRelativeLayoutSpec.h
    + + + + + + + +

    ASRelativeLayoutSpecPosition

    + + +
    +

    How the child is positioned within the spec.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASRelativeLayoutSpecPosition ) {
    + +    ASRelativeLayoutSpecPositionNone = 0,
    + +    ASRelativeLayoutSpecPositionStart = 1,
    + +    ASRelativeLayoutSpecPositionCenter = 2,
    + +    ASRelativeLayoutSpecPositionEnd = 3,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASRelativeLayoutSpecPositionNone
    +
    + + +

    The child is positioned at point 0

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    ASRelativeLayoutSpecPositionStart
    +
    + + +

    The child is positioned at point 0 relatively to the layout axis (ie left / top most)

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    ASRelativeLayoutSpecPositionCenter
    +
    + + +

    The child is centered along the specified axis

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    ASRelativeLayoutSpecPositionEnd
    +
    + + +

    The child is positioned at the maximum point of the layout axis (ie right / bottom most)

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASRelativeLayoutSpec.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeLayoutSpecSizingOption.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeLayoutSpecSizingOption.html new file mode 100755 index 0000000000..75d49d9737 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASRelativeLayoutSpecSizingOption.html @@ -0,0 +1,213 @@ + + + + + + ASRelativeLayoutSpecSizingOption Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRelativeLayoutSpecSizingOption Constants Reference

    + + +
    + + + + +
    Declared inASRelativeLayoutSpec.h
    + + + + + + + +

    ASRelativeLayoutSpecSizingOption

    + + +
    +

    How much space the spec will take up.

    +
    + + +
    + + +

    Definition

    + typedef NS_OPTIONS(NSUInteger, ASRelativeLayoutSpecSizingOption ) {
    + +    ASRelativeLayoutSpecSizingOptionDefault,
    + +    ASRelativeLayoutSpecSizingOptionMinimumWidth = 1 < < 0,
    + +    ASRelativeLayoutSpecSizingOptionMinimumHeight = 1 < < 1,
    + +    ASRelativeLayoutSpecSizingOptionMinimumSize = ASRelativeLayoutSpecSizingOptionMinimumWidth | ASRelativeLayoutSpecSizingOptionMinimumHeight,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASRelativeLayoutSpecSizingOptionDefault
    +
    + + +

    The spec will take up the maximum size possible

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    ASRelativeLayoutSpecSizingOptionMinimumWidth
    +
    + + +

    The spec will take up the minimum size possible along the X axis

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    ASRelativeLayoutSpecSizingOptionMinimumHeight
    +
    + + +

    The spec will take up the minimum size possible along the Y axis

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    ASRelativeLayoutSpecSizingOptionMinimumSize
    +
    + + +

    Convenience option to take up the minimum size along both the X and Y axis

    + + + + + + +

    + Declared In ASRelativeLayoutSpec.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASRelativeLayoutSpec.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutAlignItems.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutAlignItems.html new file mode 100755 index 0000000000..7a809bbe35 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutAlignItems.html @@ -0,0 +1,251 @@ + + + + + + ASStackLayoutAlignItems Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASStackLayoutAlignItems Constants Reference

    + + +
    + + + + +
    Declared inASStackLayoutDefines.h
    + + + + + + + +

    ASStackLayoutAlignItems

    + + +
    +

    Orientation of children along cross axis

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASStackLayoutAlignItems ) {
    + +    ASStackLayoutAlignItemsStart,
    + +    ASStackLayoutAlignItemsEnd,
    + +    ASStackLayoutAlignItemsCenter,
    + +    ASStackLayoutAlignItemsStretch,
    + +    ASStackLayoutAlignItemsBaselineFirst,
    + +    ASStackLayoutAlignItemsBaselineLast,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASStackLayoutAlignItemsStart
    +
    + + +

    Align children to start of cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignItemsEnd
    +
    + + +

    Align children with end of cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignItemsCenter
    +
    + + +

    Center children on cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignItemsStretch
    +
    + + +

    Expand children to fill cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignItemsBaselineFirst
    +
    + + +

    Children align to their first baseline. Only available for horizontal stack spec

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignItemsBaselineLast
    +
    + + +

    Children align to their last baseline. Only available for horizontal stack spec

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutDefines.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutAlignSelf.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutAlignSelf.html new file mode 100755 index 0000000000..45606fd4db --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutAlignSelf.html @@ -0,0 +1,244 @@ + + + + + + ASStackLayoutAlignSelf Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASStackLayoutAlignSelf Constants Reference

    + + +
    + + + + + + + +
    Declared inASStackLayoutDefines.h
    ReferencesASStackLayoutAlignItems
    + + + + + + + +

    ASStackLayoutAlignSelf

    + + +
    +

    Each child may override their parent stack’s cross axis alignment.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf ) {
    + +    ASStackLayoutAlignSelfAuto,
    + +    ASStackLayoutAlignSelfStart,
    + +    ASStackLayoutAlignSelfEnd,
    + +    ASStackLayoutAlignSelfCenter,
    + +    ASStackLayoutAlignSelfStretch,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASStackLayoutAlignSelfAuto
    +
    + + +

    Inherit alignment value from containing stack.

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignSelfStart
    +
    + + +

    Align to start of cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignSelfEnd
    +
    + + +

    Align with end of cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignSelfCenter
    +
    + + +

    Center on cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutAlignSelfStretch
    +
    + + +

    Expand to fill cross axis

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    +
    + + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASStackLayoutDefines.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutDirection.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutDirection.html new file mode 100755 index 0000000000..ac22df4f8b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutDirection.html @@ -0,0 +1,175 @@ + + + + + + ASStackLayoutDirection Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASStackLayoutDirection Constants Reference

    + + +
    + + + + +
    Declared inASStackLayoutDefines.h
    + + + + + + + +

    ASStackLayoutDirection

    + + +
    +

    The direction children are stacked in

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASStackLayoutDirection ) {
    + +    ASStackLayoutDirectionVertical,
    + +    ASStackLayoutDirectionHorizontal,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASStackLayoutDirectionVertical
    +
    + + +

    Children are stacked vertically

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutDirectionHorizontal
    +
    + + +

    Children are stacked horizontally

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutDefines.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutJustifyContent.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutJustifyContent.html new file mode 100755 index 0000000000..946d43e0dd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASStackLayoutJustifyContent.html @@ -0,0 +1,242 @@ + + + + + + ASStackLayoutJustifyContent Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASStackLayoutJustifyContent Constants Reference

    + + +
    + + + + +
    Declared inASStackLayoutDefines.h
    + + + + + + + +

    ASStackLayoutJustifyContent

    + + +
    +

    If no children are flexible, how should this spec justify its children in the available space?

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent ) {
    + +    ASStackLayoutJustifyContentStart,
    + +    ASStackLayoutJustifyContentCenter,
    + +    ASStackLayoutJustifyContentEnd,
    + +    ASStackLayoutJustifyContentSpaceBetween,
    + +    ASStackLayoutJustifyContentSpaceAround,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASStackLayoutJustifyContentStart
    +
    + + +

    On overflow, children overflow out of this spec’s bounds on the right/bottom side. + On underflow, children are left/top-aligned within this spec’s bounds.

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutJustifyContentCenter
    +
    + + +

    On overflow, children are centered and overflow on both sides. + On underflow, children are centered within this spec’s bounds in the stacking direction.

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutJustifyContentEnd
    +
    + + +

    On overflow, children overflow out of this spec’s bounds on the left/top side. + On underflow, children are right/bottom-aligned within this spec’s bounds.

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutJustifyContentSpaceBetween
    +
    + + +

    On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentStart. + Otherwise, the starting edge of the first child is at the starting edge of the stack, + the ending edge of the last child is at the ending edge of the stack, and the remaining children + are distributed so that the spacing between any two adjacent ones is the same. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last 2 children).

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASStackLayoutJustifyContentSpaceAround
    +
    + + +

    On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentCenter. + Otherwise, children are distributed such that the spacing between any two adjacent ones is the same, + and the spacing between the first/last child and the stack edges is half the size of the spacing between children. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last child and the stack ending edge).

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutDefines.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASTextNodeHighlightStyle.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASTextNodeHighlightStyle.html new file mode 100755 index 0000000000..e47cd046cf --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASTextNodeHighlightStyle.html @@ -0,0 +1,175 @@ + + + + + + ASTextNodeHighlightStyle Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTextNodeHighlightStyle Constants Reference

    + + +
    + + + + +
    Declared inASTextNode.h
    + + + + + + + +

    ASTextNodeHighlightStyle

    + + +
    +

    Highlight styles.

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle ) {
    + +    ASTextNodeHighlightStyleLight,
    + +    ASTextNodeHighlightStyleDark,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASTextNodeHighlightStyleLight
    +
    + + +

    Highlight style for text on a light background.

    + + + + + + +

    + Declared In ASTextNode.h. +

    + +
    + +
    ASTextNodeHighlightStyleDark
    +
    + + +

    Highlight style for text on a dark background.

    + + + + + + +

    + Declared In ASTextNode.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASVerticalAlignment.html b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASVerticalAlignment.html new file mode 100755 index 0000000000..640a47dc9b --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Constants/ASVerticalAlignment.html @@ -0,0 +1,270 @@ + + + + + + ASVerticalAlignment Constants Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASVerticalAlignment Constants Reference

    + + +
    + + + + +
    Declared inASStackLayoutDefines.h
    + + + + + + + +

    ASVerticalAlignment

    + + +
    +

    Orientation of children along vertical axis

    +
    + + +
    + + +

    Definition

    + typedef NS_ENUM(NSUInteger, ASVerticalAlignment ) {
    + +    ASVerticalAlignmentNone,
    + +    ASVerticalAlignmentTop,
    + +    ASVerticalAlignmentCenter,
    + +    ASVerticalAlignmentBottom,
    + +    ASAlignmentTop = ASVerticalAlignmentTop,
    + +    ASAlignmentCenter = ASVerticalAlignmentCenter,
    + +    ASAlignmentBottom = ASVerticalAlignmentBottom,
    + + };
    + +
    + +
    +

    Constants

    +
    + +
    ASVerticalAlignmentNone
    +
    + + +

    No alignment specified. Default value

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASVerticalAlignmentTop
    +
    + + +

    Top aligned

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASVerticalAlignmentCenter
    +
    + + +

    Center aligned

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASVerticalAlignmentBottom
    +
    + + +

    Bottom aligned

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASAlignmentTop
    +
    + + +

    Use ASVerticalAlignmentTop instead (Deprecated: Use ASVerticalAlignmentTop instead)

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASAlignmentCenter
    +
    + + +

    Use ASVerticalAlignmentCenter instead (Deprecated: Use ASVerticalAlignmentCenter instead)

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    ASAlignmentBottom
    +
    + + +

    Use ASVerticalAlignmentBottom instead (Deprecated: Use ASVerticalAlignmentBottom instead)

    + + + + + + +

    + Declared In ASStackLayoutDefines.h. +

    + +
    + +
    +
    + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutDefines.h

    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASAbsoluteLayoutElement.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASAbsoluteLayoutElement.html new file mode 100755 index 0000000000..ecaff9596c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASAbsoluteLayoutElement.html @@ -0,0 +1,180 @@ + + + + + + ASAbsoluteLayoutElement Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASAbsoluteLayoutElement Protocol Reference

    + + +
    + + + + +
    Declared inASAbsoluteLayoutElement.h
    + + + + +
    + +

    Overview

    +

    Layout options that can be defined for an ASLayoutElement being added to a ASAbsoluteLayoutSpec.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      layoutPosition +required method

    + +
    +
    + +
    + + +
    +

    The position of this object within its parent spec.

    +
    + + + +
    @property (nonatomic, assign) CGPoint layoutPosition
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAbsoluteLayoutElement.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCellNodeInteractionDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCellNodeInteractionDelegate.html new file mode 100755 index 0000000000..70d2a92642 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCellNodeInteractionDelegate.html @@ -0,0 +1,198 @@ + + + + + + ASCellNodeInteractionDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCellNodeInteractionDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASCellNode+Internal.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – nodeDidRelayout:sizeChanged: +required method

    + +
    +
    + +
    + + +
    +

    Notifies the delegate that the specified cell node has done a relayout. +The notification is done on main thread.

    +
    + + + +
    - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    node

    A node informing the delegate about the relayout.

    sizeChanged

    YES if the node’s calculatedSize changed during the relayout, NO otherwise.

    +
    + + + + + + + +
    +

    Discussion

    +

    This will not be called due to measurement passes before the node has loaded +its view, even if triggered by -setNeedsLayout, as it is assumed these are +not relevant to UIKit. Indeed, these calls can cause consistency issues.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCellNode+Internal.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionDataSource.html new file mode 100755 index 0000000000..1e4b1ec080 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionDataSource.html @@ -0,0 +1,807 @@ + + + + + + ASCollectionDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASCommonCollectionDataSource
    Declared inASCollectionNode.h
    + + + + +
    + +

    Overview

    +

    This is a node-based UICollectionViewDataSource.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – collectionNode:numberOfItemsInSection: +

    + +
    +
    + +
    + + +
    +

    Asks the data source for the number of items in the given section of the collection node.

    +
    + + + +
    - (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – numberOfSectionsInCollectionNode: +

    + +
    +
    + +
    + + +
    +

    Asks the data source for the number of sections in the collection node.

    +
    + + + +
    - (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionNode:nodeBlockForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -collectionNode:nodeForItemAtIndexPath: +This method takes precedence over collectionNode:nodeForItemAtIndexPath: if implemented.

    +
    + + + +
    - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionNode

    The sender.

    indexPath

    The index path of the item.

    +
    + + + +
    +

    Return Value

    +

    a block that creates the node for display for this item. +Must be thread-safe (can be called on the main thread or a background +queue) and should not implement reuse (it will be called once per row).

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionNode:nodeForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -collectionView:cellForItemAtIndexPath:.

    +
    + + + +
    - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path of the item.

    collectionView

    The sender.

    +
    + + + +
    +

    Return Value

    +

    A node to display for the given item. This will be called on the main thread and should +not implement reuse (it will be called once per item). Unlike UICollectionView’s version, +this method is not called when the item is about to display.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionNode:nodeForSupplementaryElementOfKind:atIndexPath: +

    + +
    +
    + +
    + + +
    +

    Asks the data source to provide a node to display for the given supplementary element in the collection view.

    +
    + + + +
    - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    collectionNode

    The sender.

    kind

    The kind of supplementary element.

    indexPath

    The index path of the supplementary element.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionNode:contextForSection: +

    + +
    +
    + +
    + + +
    +

    Asks the data source to provide a context object for the given section. This object +can later be retrieved by calling @c contextForSection: and is useful when implementing +custom @c UICollectionViewLayout subclasses. The context object is ret

    +
    + + + +
    - (nullable id<ASSectionContext>)collectionNode:(ASCollectionNode *)collectionNode contextForSection:(NSInteger)section
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionNode

    The sender.

    section

    The index of the section to provide context for.

    +
    + + + +
    +

    Return Value

    +

    A context object to assign to the given section, or @c nil.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:nodeForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -collectionView:cellForItemAtIndexPath:.

    +
    + + + +
    - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionView

    The sender.

    indexPath

    The index path of the requested node.

    +
    + + + +
    +

    Return Value

    +

    a node for display at this indexpath. This will be called on the main thread and should +not implement reuse (it will be called once per row). Unlike UICollectionView’s version, +this method is not called when the row is about to display.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:nodeBlockForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -collectionView:nodeForItemAtIndexPath: +This method takes precedence over collectionView:nodeForItemAtIndexPath: if implemented.

    +
    + + + +
    - (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionView

    The sender.

    indexPath

    The index path of the requested node.

    +
    + + + +
    +

    Return Value

    +

    a block that creates the node for display at this indexpath. +Must be thread-safe (can be called on the main thread or a background +queue) and should not implement reuse (it will be called once per row).

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:nodeForSupplementaryElementOfKind:atIndexPath: +

    + +
    +
    + +
    + + +
    +

    Asks the collection view to provide a supplementary node to display in the collection view.

    +
    + + + +
    - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    collectionView

    An object representing the collection view requesting this information.

    kind

    The kind of supplementary node to provide.

    indexPath

    The index path that specifies the location of the new supplementary node.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionViewLockDataSource: +

    + +
    +
    + +
    + + +
    +

    Indicator to lock the data source for data fetching in async mode. +We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception +due to the data access in async mode. (Deprecated: The data source is always accessed on the main thread, and this method will not be called.)

    +
    + + + +
    - (void)collectionViewLockDataSource:(ASCollectionView *)collectionView
    + + + +
    +

    Parameters

    + + + + + + + +
    collectionView

    The sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionViewUnlockDataSource: +

    + +
    +
    + +
    + + +
    +

    Indicator to unlock the data source for data fetching in async mode. +We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception +due to the data access in async mode. (Deprecated: The data source is always accessed on the main thread, and this method will not be called.)

    +
    + + + +
    - (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView
    + + + +
    +

    Parameters

    + + + + + + + +
    collectionView

    The sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionDelegate.html new file mode 100755 index 0000000000..8b40171190 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionDelegate.html @@ -0,0 +1,685 @@ + + + + + + ASCollectionDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASCommonCollectionDelegate
    NSObject
    Declared inASCollectionNode.h
    + + + + +
    + +

    Overview

    +

    This is a node-based UICollectionViewDelegate.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – collectionNode:constrainedSizeForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Provides the constrained size range for measuring the given item.

    +
    + + + +
    - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionNode

    The sender.

    indexPath

    The index path of the item.

    +
    + + + +
    +

    Return Value

    +

    A constrained size range for layout for the item at this index path.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionNode:willBeginBatchFetchWithContext: +

    + +
    +
    + +
    + + +
    +

    Receive a message that the collection node is near the end of its data set and more data should be fetched if +necessary.

    +
    + + + +
    - (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionNode

    The sender.

    context

    A context object that must be notified when the batch fetch is completed.

    +
    + + + + + + + +
    +

    Discussion

    +

    You must eventually call -completeBatchFetching: with an argument of YES in order to receive future +notifications to do batch fetches. This method is called on a background queue.

    + +

    ASCollectionNode currently only supports batch events for tail loads. If you require a head load, consider +implementing a UIRefreshControl.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – shouldBatchFetchForCollectionNode: +

    + +
    +
    + +
    + + +
    +

    Tell the collection node if batch fetching should begin.

    +
    + + + +
    - (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode
    + + + +
    +

    Parameters

    + + + + + + + +
    collectionNode

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to conditionally fetch batches. Example use cases are: limiting the total number of +objects that can be fetched or no network connection.

    + +

    If not implemented, the collection node assumes that it should notify its asyncDelegate when batch fetching +should occur.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:constrainedSizeForNodeAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Provides the constrained size range for measuring the node at the index path.

    +
    + + + +
    - (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionView

    The sender.

    indexPath

    The index path of the node.

    +
    + + + +
    +

    Return Value

    +

    A constrained size range for layout the node at this index path.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:willDisplayNode:forItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the collection view will add the given node +at the given index path to the view hierarchy.

    +
    + + + +
    - (void)collectionView:(ASCollectionView *)collectionView willDisplayNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    collectionView

    The sender.

    node

    The node that will be displayed.

    indexPath

    The index path of the item that will be displayed.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: AsyncDisplayKit processes collection view edits asynchronously. The index path +passed into this method may not correspond to the same item in your data source +if your data source has been updated since the last edit was processed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:didEndDisplayingNode:forItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the collection view did remove the provided node from the view hierarchy. +This may be caused by the node scrolling out of view, or by deleting the item +or its containing section with @c deleteItemsAtIndexPaths: or @c deleteSections: .

    +
    + + + +
    - (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    collectionView

    The sender.

    node

    The node which was removed from the view hierarchy.

    indexPath

    The index path at which the node was located before it was removed.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: AsyncDisplayKit processes collection view edits asynchronously. The index path +passed into this method may not correspond to the same item in your data source +if your data source has been updated since the last edit was processed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – shouldBatchFetchForCollectionView: +

    + +
    +
    + +
    + + +
    +

    Tell the collectionView if batch fetching should begin.

    +
    + + + +
    - (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)collectionView
    + + + +
    +

    Parameters

    + + + + + + + +
    collectionView

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to conditionally fetch batches. Example use cases are: limiting the total number of +objects that can be fetched or no network connection.

    + +

    If not implemented, the collectionView assumes that it should notify its asyncDelegate when batch fetching +should occur.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    + +

    – collectionView:willDisplayNodeForItemAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the collection view will add the node +at the given index path to the view hierarchy.

    +
    + + + +
    - (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    collectionView

    The sender.

    indexPath

    The index path of the item that will be displayed.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: AsyncDisplayKit processes collection view edits asynchronously. The index path +passed into this method may not correspond to the same item in your data source +if your data source has been updated since the last edit was processed.

    + +

    This method is deprecated. Use @c collectionView:willDisplayNode:forItemAtIndexPath: instead.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionViewDelegateFlowLayout.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionViewDelegateFlowLayout.html new file mode 100755 index 0000000000..2185f480bd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionViewDelegateFlowLayout.html @@ -0,0 +1,286 @@ + + + + + + ASCollectionViewDelegateFlowLayout Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionViewDelegateFlowLayout Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASCollectionDelegate
    Declared inASCollectionView.h
    + + + + +
    + +

    Overview

    +

    Defines methods that let you coordinate with a UICollectionViewFlowLayout in combination with an ASCollectionView.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – collectionView:layout:insetForSectionAtIndex: +

    + +
    +
    + +
    + + +
    +

    This method is deprecated and does nothing from 1.9.7 and up +Previously it applies the section inset to every cells within the corresponding section. +The expected behavior is to apply the section inset to the whole section rather than +shrinking each cell individually. +If you want this behavior, you can integrate your insets calculation into +constrainedSizeForNodeAtIndexPath +please file a github issue if you would like this to be restored.

    +
    + + + +
    - (UIEdgeInsets)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
    + + + + + + + + + +
    +

    Discussion

    +

    This method is deprecated and does nothing from 1.9.7 and up +Previously it applies the section inset to every cells within the corresponding section. +The expected behavior is to apply the section inset to the whole section rather than +shrinking each cell individually. +If you want this behavior, you can integrate your insets calculation into +constrainedSizeForNodeAtIndexPath +please file a github issue if you would like this to be restored.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – collectionView:layout:referenceSizeForHeaderInSection: +

    + +
    +
    + +
    + + +
    +

    Asks the delegate for the size of the header in the specified section.

    +
    + + + +
    - (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    + +

    – collectionView:layout:referenceSizeForFooterInSection: +

    + +
    +
    + +
    + + +
    +

    Asks the delegate for the size of the footer in the specified section.

    +
    + + + +
    - (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionView.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionViewLayoutFacilitatorProtocol.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionViewLayoutFacilitatorProtocol.html new file mode 100755 index 0000000000..946ec7bdcd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCollectionViewLayoutFacilitatorProtocol.html @@ -0,0 +1,308 @@ + + + + + + ASCollectionViewLayoutFacilitatorProtocol Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCollectionViewLayoutFacilitatorProtocol Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASCollectionViewLayoutFacilitatorProtocol.h
    + + + + +
    + +

    Overview

    +

    This facilitator protocol is intended to help Layout to better +gel with the CollectionView

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – collectionViewWillEditCellsAtIndexPaths:batched: +required method

    + +
    +
    + +
    + + +
    +

    Inform that the collectionView is editing the cells at a list of indexPaths

    +
    + + + +
    - (void)collectionViewWillEditCellsAtIndexPaths:(NSArray *)indexPaths batched:(BOOL)isBatched
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPaths

    an array of NSIndexPath objects of cells being/will be edited.

    isBatched

    indicates whether the editing operation will be batched by the collectionView

    + +

    NOTE: when isBatched, used in combination with -collectionViewWillPerformBatchUpdates

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionViewLayoutFacilitatorProtocol.h

    +
    + + +
    +
    +
    + +

    – collectionViewWillEditSectionsAtIndexSet:batched: +required method

    + +
    +
    + +
    + + +
    +

    Inform that the collectionView is editing the sections at a set of indexes

    +
    + + + +
    - (void)collectionViewWillEditSectionsAtIndexSet:(NSIndexSet *)indexes batched:(BOOL)batched
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexes

    an NSIndexSet of section indexes being/will be edited.

    batched

    indicates whether the editing operation will be batched by the collectionView

    + +

    NOTE: when batched, used in combination with -collectionViewWillPerformBatchUpdates

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionViewLayoutFacilitatorProtocol.h

    +
    + + +
    +
    +
    + +

    – collectionViewWillPerformBatchUpdates +required method

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the collectionView is about to call performBatchUpdates

    +
    + + + +
    - (void)collectionViewWillPerformBatchUpdates
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASCollectionViewLayoutFacilitatorProtocol.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonCollectionDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonCollectionDataSource.html new file mode 100755 index 0000000000..0fef7a7747 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonCollectionDataSource.html @@ -0,0 +1,121 @@ + + + + + + ASCommonCollectionDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCommonCollectionDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASCollectionViewProtocols.h
    + + + + +
    + +

    Overview

    +

    This is a subset of UICollectionViewDataSource.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonCollectionDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonCollectionDelegate.html new file mode 100755 index 0000000000..8901487e47 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonCollectionDelegate.html @@ -0,0 +1,121 @@ + + + + + + ASCommonCollectionDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCommonCollectionDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    UIScrollViewDelegate
    Declared inASCollectionViewProtocols.h
    + + + + +
    + +

    Overview

    +

    This is a subset of UICollectionViewDelegate.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonTableDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonTableDataSource.html new file mode 100755 index 0000000000..9068e3ecab --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonTableDataSource.html @@ -0,0 +1,121 @@ + + + + + + ASCommonTableDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCommonTableDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASTableViewProtocols.h
    + + + + +
    + +

    Overview

    +

    This is a subset of UITableViewDataSource.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonTableViewDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonTableViewDelegate.html new file mode 100755 index 0000000000..4032159da3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASCommonTableViewDelegate.html @@ -0,0 +1,121 @@ + + + + + + ASCommonTableViewDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASCommonTableViewDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    UIScrollViewDelegate
    Declared inASTableViewProtocols.h
    + + + + +
    + +

    Overview

    +

    This is a subset of UITableViewDelegate.

    +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASContextTransitioning.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASContextTransitioning.html new file mode 100755 index 0000000000..dde53dd1e5 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASContextTransitioning.html @@ -0,0 +1,532 @@ + + + + + + ASContextTransitioning Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASContextTransitioning Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASContextTransitioning.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – isAnimated +required method

    + +
    +
    + +
    + + +
    +

    Defines if the given transition is animated

    +
    + + + +
    - (BOOL)isAnimated
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – layoutForKey: +required method

    + +
    +
    + +
    + + +
    +

    Retrieve either the “from” or “to” layout

    +
    + + + +
    - (nullable ASLayout *)layoutForKey:(NSString *)key
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – constrainedSizeForKey: +required method

    + +
    +
    + +
    + + +
    +

    Retrieve either the “from” or “to” constrainedSize

    +
    + + + +
    - (ASSizeRange)constrainedSizeForKey:(NSString *)key
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – subnodesForKey: +required method

    + +
    +
    + +
    + + +
    +

    Retrieve the subnodes from either the “from” or “to” layout

    +
    + + + +
    - (NSArray<ASDisplayNode*> *)subnodesForKey:(NSString *)key
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – insertedSubnodes +required method

    + +
    +
    + +
    + + +
    +

    Subnodes that have been inserted in the layout transition

    +
    + + + +
    - (NSArray<ASDisplayNode*> *)insertedSubnodes
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – removedSubnodes +required method

    + +
    +
    + +
    + + +
    +

    Subnodes that will be removed in the layout transition

    +
    + + + +
    - (NSArray<ASDisplayNode*> *)removedSubnodes
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – initialFrameForNode: +required method

    + +
    +
    + +
    + + +
    +

    The frame for the given node before the transition began.

    +
    + + + +
    - (CGRect)initialFrameForNode:(ASDisplayNode *)node
    + + + + + + + + + +
    +

    Discussion

    +

    Returns CGRectNull if the node was not in the hierarchy before the transition.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – finalFrameForNode: +required method

    + +
    +
    + +
    + + +
    +

    The frame for the given node when the transition completes.

    +
    + + + +
    - (CGRect)finalFrameForNode:(ASDisplayNode *)node
    + + + + + + + + + +
    +

    Discussion

    +

    Returns CGRectNull if the node is no longer in the hierarchy after the transition.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    + +

    – completeTransition: +required method

    + +
    +
    + +
    + + +
    +

    Invoke this method when the transition is completed in animateLayoutTransition:

    +
    + + + +
    - (void)completeTransition:(BOOL)didComplete
    + + + + + + + + + +
    +

    Discussion

    +

    Passing NO to didComplete will set the original layout as the new layout.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASContextTransitioning.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASEditableTextNodeDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASEditableTextNodeDelegate.html new file mode 100755 index 0000000000..9fed893533 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASEditableTextNodeDelegate.html @@ -0,0 +1,472 @@ + + + + + + ASEditableTextNodeDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASEditableTextNodeDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASEditableTextNode.h
    + + + + +
    + +

    Overview

    +

    The methods declared by the ASEditableTextNodeDelegate protocol allow the adopting delegate to +respond to notifications such as began and finished editing, selection changed and text updated; +and manage whether a specified text should be replaced.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – editableTextNodeDidBeginEditing: +

    + +
    +
    + +
    + + +
    +

    Indicates to the delegate that the text node began editing.

    +
    + + + +
    - (void)editableTextNodeDidBeginEditing:(ASEditableTextNode *)editableTextNode
    + + + +
    +

    Parameters

    + + + + + + + +
    editableTextNode

    An editable text node.

    +
    + + + + + + + +
    +

    Discussion

    +

    The invocation of this method coincides with the keyboard animating to become visible.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – editableTextNode:shouldChangeTextInRange:replacementText: +

    + +
    +
    + +
    + + +
    +

    Asks the delegate whether the specified text should be replaced in the editable text node.

    +
    + + + +
    - (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    editableTextNode

    An editable text node.

    range

    The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character.

    text

    The text to insert.

    +
    + + + +
    +

    Return Value

    +

    The text node calls this method whenever the user types a new character or deletes an existing character. Implementation of this method is optional – the default implementation returns YES.

    +
    + + + + + +
    +

    Discussion

    +

    YES if the old text should be replaced by the new text; NO if the replacement operation should be aborted.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – editableTextNodeDidChangeSelection:fromSelectedRange:toSelectedRange:dueToEditing: +

    + +
    +
    + +
    + + +
    +

    Indicates to the delegate that the text node’s selection has changed.

    +
    + + + +
    - (void)editableTextNodeDidChangeSelection:(ASEditableTextNode *)editableTextNode fromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange dueToEditing:(BOOL)dueToEditing
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    editableTextNode

    An editable text node.

    fromSelectedRange

    The previously selected range.

    toSelectedRange

    The current selected range. Equivalent to the property.

    dueToEditing

    YES if the selection change was due to editing; NO otherwise.

    +
    + + + + + + + +
    +

    Discussion

    +

    You can access the selection of the receiver via .

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – editableTextNodeDidUpdateText: +

    + +
    +
    + +
    + + +
    +

    Indicates to the delegate that the text node’s text was updated.

    +
    + + + +
    - (void)editableTextNodeDidUpdateText:(ASEditableTextNode *)editableTextNode
    + + + +
    +

    Parameters

    + + + + + + + +
    editableTextNode

    An editable text node.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method is called each time the user updated the text node’s text. It is not called for programmatic changes made to the text via the property.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    + +

    – editableTextNodeDidFinishEditing: +

    + +
    +
    + +
    + + +
    +

    Indicates to the delegate that teh text node has finished editing.

    +
    + + + +
    - (void)editableTextNodeDidFinishEditing:(ASEditableTextNode *)editableTextNode
    + + + +
    +

    Parameters

    + + + + + + + +
    editableTextNode

    An editable text node.

    +
    + + + + + + + +
    +

    Discussion

    +

    The invocation of this method coincides with the keyboard animating to become hidden.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASEditableTextNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElement.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElement.html new file mode 100755 index 0000000000..fc275151ba --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElement.html @@ -0,0 +1,657 @@ + + + + + + ASLayoutElement Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutElement Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASEnvironment
    ASLayoutElementExtensibility
    ASLayoutElementPrivate
    ASLayoutElementStylability
    NSFastEnumeration
    Declared inASLayoutElement.h
    + + + + +
    + +

    Overview

    +

    The ASLayoutElement protocol declares a method for measuring the layout of an object. A layout +is defined by an ASLayout return value, and must specify 1) the size (but not position) of the +layoutElement object, and 2) the size and position of all of its immediate child objects. The tree +recursion is driven by parents requesting layouts from their children in order to determine their +size, followed by the parents setting the position of the children once the size is known

    + +

    The protocol also implements a “family” of LayoutElement protocols. These protocols contain layout +options that can be used for specific layout specs. For example, ASStackLayoutSpec has options +defining how a layoutElement should shrink or grow based upon available space.

    + +

    These layout options are all stored in an ASLayoutOptions class (that is defined in ASLayoutElementPrivate). +Generally you needn’t worry about the layout options class, as the layoutElement protocols allow all direct +access to the options via convenience properties. If you are creating custom layout spec, then you can +extend the backing layout options class to accommodate any new layout options.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      layoutElementType +required method

    + +
    +
    + +
    + + +
    +

    Returns type of layoutElement

    +
    + + + +
    @property (nonatomic, assign, readonly) ASLayoutElementType layoutElementType
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      canLayoutAsynchronous +required method

    + +
    +
    + +
    + + +
    +

    Returns if the layoutElement can be used to layout in an asynchronous way on a background thread.

    +
    + + + +
    @property (nonatomic, assign, readonly) BOOL canLayoutAsynchronous
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      style +required method

    + +
    +
    + +
    + + +
    +

    A size constraint that should apply to this ASLayoutElement.

    +
    + + + +
    @property (nonatomic, assign, readonly) ASLayoutElementStyle *style
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

      debugName +required method

    + +
    +
    + +
    + + +
    +

    Optional name that is printed by ascii art string and displayed in description.

    +
    + + + +
    @property (nullable, nonatomic, copy) NSString *debugName
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

    – layoutThatFits: +required method

    + +
    +
    + +
    + + +
    +

    Asks the node to return a layout based on given size range.

    +
    + + + +
    - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The minimum and maximum sizes the receiver should fit in.

    +
    + + + +
    +

    Return Value

    +

    An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used).

    +
    + + + + + +
    +

    Discussion

    +

    Though this method does not set the bounds of the view, it does have side effects–caching both the +constraint and the result.

    Warning: Subclasses must not override this; it caches results from -calculateLayoutThatFits:. Calling this method may +be expensive if result is not cached.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

    – layoutThatFits:parentSize: +required method

    + +
    +
    + +
    + + +
    +

    Call this on children layoutElements to compute their layouts within your implementation of -calculateLayoutThatFits:.

    +
    + + + +
    - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    constrainedSize

    Specifies a minimum and maximum size. The receiver must choose a size that is in this range.

    parentSize

    The parent node’s size. If the parent component does not have a final size in a given dimension, +then it should be passed as ASLayoutElementParentDimensionUndefined (for example, if the parent’s width +depends on the child’s size).

    +
    + + + +
    +

    Return Value

    +

    An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used).

    +
    + + + + + +
    +

    Discussion

    +

    Warning: You may not override this method. Override -calculateLayoutThatFits: instead.

    Warning: In almost all cases, prefer the use of ASCalculateLayout in ASLayout

    Though this method does not set the bounds of the view, it does have side effects–caching both the +constraint and the result.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

    – calculateLayoutThatFits: +required method

    + +
    +
    + +
    + + +
    +

    Override this method to compute your layoutElement’s layout.

    +
    + + + +
    - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    A min and max size. This is computed as described in the description. The ASLayout you +return MUST have a size between these two sizes.

    +
    + + + + + + + +
    +

    Discussion

    +

    Why do you need to override -calculateLayoutThatFits: instead of -layoutThatFits:parentSize:? +The base implementation of -layoutThatFits:parentSize: does the following for you: +1. First, it uses the parentSize parameter to resolve the nodes’s size (the one assigned to the size property). +2. Then, it intersects the resolved size with the constrainedSize parameter. If the two don’t intersect, +constrainedSize wins. This allows a component to always override its childrens' sizes when computing its layout. +(The analogy for UIView: you might return a certain size from -sizeThatFits:, but a parent view can always override +that size and set your frame to any size.) +3. It caches it result for reuse

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

    – calculateLayoutThatFits:restrictedToSize:relativeToParentSize: +required method

    + +
    +
    + +
    + + +
    +

    In certain advanced cases, you may want to override this method. Overriding this method allows you to receive the +layoutElement’s size, parentSize, and constrained size. With these values you could calculate the final constrained size +and call -calculateLayoutThatFits: with the result.

    +
    + + + +
    - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize
    + + + + + + + + + +
    +

    Discussion

    +

    Warning: Overriding this method should be done VERY rarely.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    + +

    – measureWithSizeRange: +required method

    + +
    +
    + +
    + + +
    +

    Calculate a layout based on given size range. (Deprecated: Deprecated in version 2.0: Use layoutThatFits: or layoutThatFits:parentSize: if used in +ASLayoutSpec subclasses)

    +
    + + + +
    - (nonnull ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
    + + + +
    +

    Parameters

    + + + + + + + +
    constrainedSize

    The minimum and maximum sizes the receiver should fit in.

    +
    + + + +
    +

    Return Value

    +

    An ASLayout instance defining the layout of the receiver and its children.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElement.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElementAsciiArtProtocol.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElementAsciiArtProtocol.html new file mode 100755 index 0000000000..c17c1fe76c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElementAsciiArtProtocol.html @@ -0,0 +1,225 @@ + + + + + + ASLayoutElementAsciiArtProtocol Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutElementAsciiArtProtocol Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASAsciiArtBoxCreator.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – asciiArtString +required method

    + +
    +
    + +
    + + +
    +

    Returns an ascii-art representation of this object and its children. +For example, an ASInsetSpec may return something like this:

    +
    + + + +
    - (NSString *)asciiArtString
    + + + + + + + + + +
    +

    Discussion

    +

    –ASInsetLayoutSpec–

    + +

    | ASTextNode |

    +
    + + + + + + + +
    +

    Declared In

    +

    ASAsciiArtBoxCreator.h

    +
    + + +
    +
    +
    + +

    – asciiArtName +required method

    + +
    +
    + +
    + + +
    +

    returns the name of this object that will display in the ascii art. Usually this can +simply be NSStringFromClass([self class]).

    +
    + + + +
    - (NSString *)asciiArtName
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASAsciiArtBoxCreator.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElementPrivate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElementPrivate.html new file mode 100755 index 0000000000..01647a4d78 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASLayoutElementPrivate.html @@ -0,0 +1,243 @@ + + + + + + ASLayoutElementPrivate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASLayoutElementPrivate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASLayoutElementPrivate.h
    + + + + +
    + +

    Overview

    +

    The base protocol for ASLayoutElement. Generally the methods/properties in this class do not need to be +called by the end user and are only called internally. However, there may be a case where the methods are useful.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – finalLayoutElement +required method

    + +
    +
    + +
    + + +
    +

    This method can be used to give the user a chance to wrap an ASLayoutElement in an ASLayoutSpec +just before it is added to a parent ASLayoutSpec. For example, if you wanted an ASTextNode that was always +inside of an ASInsetLayoutSpec, you could subclass ASTextNode and implement finalLayoutElement so that it wraps +itself in an inset spec.

    + +

    Note that any ASLayoutElement other than self that is returned MUST set isFinalLayoutElement to YES. Make sure +to do this BEFORE adding a child to the ASLayoutElement.

    +
    + + + +
    - (id<ASLayoutElement>)finalLayoutElement
    + + + + + +
    +

    Return Value

    +

    The layoutElement that will be added to the parent layout spec. Defaults to self.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElementPrivate.h

    +
    + + +
    +
    +
    + +

      isFinalLayoutElement +required method

    + +
    +
    + +
    + + +
    +

    A flag to indicate that this ASLayoutElement was created in finalLayoutElement. This MUST be set to YES +before adding a child to this layoutElement.

    +
    + + + +
    @property (nonatomic, assign) BOOL isFinalLayoutElement
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASLayoutElementPrivate.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASManagesChildVisibilityDepth.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASManagesChildVisibilityDepth.html new file mode 100755 index 0000000000..817097a2bd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASManagesChildVisibilityDepth.html @@ -0,0 +1,200 @@ + + + + + + ASManagesChildVisibilityDepth Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASManagesChildVisibilityDepth Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASVisibilityDepth
    Declared inASVisibilityProtocols.h
    + + + + +
    + +

    Overview

    +

    ASManagesChildVisibilityDepth

    A protocol which should be implemented by container view controllers to allow proper +propagation of visibility depth

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – visibilityDepthOfChildViewController: +required method

    + +
    +
    + +
    + + +
    +

    Container view controllers should adopt this protocol to indicate that they will manage their child’s +visibilityDepth. For example, ASNavigationController adopts this protocol and manages its childrens visibility +depth.

    + +

    If you adopt this protocol, you must also emit visibilityDepthDidChange messages to child view controllers.

    +
    + + + +
    - (NSInteger)visibilityDepthOfChildViewController:(UIViewController *)childViewController
    + + + +
    +

    Parameters

    + + + + + + + +
    childViewController

    Expected to return the visibility depth of the child view controller.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVisibilityProtocols.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASMultiplexImageNodeDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASMultiplexImageNodeDataSource.html new file mode 100755 index 0000000000..b979e068fd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASMultiplexImageNodeDataSource.html @@ -0,0 +1,364 @@ + + + + + + ASMultiplexImageNodeDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASMultiplexImageNodeDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASMultiplexImageNode.h
    + + + + +
    + +

    Overview

    +

    The ASMultiplexImageNodeDataSource protocol is adopted by an object that provides the multiplex image node, +for each image identifier, an image or a URL the image node should load.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – multiplexImageNode:imageForImageIdentifier: +

    + +
    +
    + +
    + + +
    +

    An image for the specified identifier.

    +
    + + + +
    - (nullable UIImage *)multiplexImageNode:(ASMultiplexImageNode *)imageNode imageForImageIdentifier:(ASImageIdentifier)imageIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    imageNode

    The sender.

    imageIdentifier

    The identifier for the image that should be returned.

    +
    + + + +
    +

    Return Value

    +

    A UIImage corresponding to imageIdentifier, or nil if none is available.

    +
    + + + + + +
    +

    Discussion

    +

    If the image is already available to the data source, this method should be used in lieu of providing the +URL to the image via -multiplexImageNode:URLForImageIdentifier:.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNode:URLForImageIdentifier: +

    + +
    +
    + +
    + + +
    +

    An image URL for the specified identifier.

    +
    + + + +
    - (nullable NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(ASImageIdentifier)imageIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    imageNode

    The sender.

    imageIdentifier

    The identifier for the image that will be downloaded.

    +
    + + + +
    +

    Return Value

    +

    An NSURL for the image identified by imageIdentifier, or nil if none is available.

    +
    + + + + + +
    +

    Discussion

    +

    Supported URLs include HTTP, HTTPS, AssetsLibrary, and FTP URLs as well as Photos framework URLs (see note).

    + +

    If the image is already available to the data source, it should be provided via [ASMultiplexImageNodeDataSource multiplexImageNode:imageForImageIdentifier:] instead.

    +
    + + + + + +
    +

    See Also

    +
      + +
    • [NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:] below.

    • + +
    +
    + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNode:assetForLocalIdentifier: +

    + +
    +
    + +
    + + +
    +

    A PHAsset for the specific asset local identifier

    +
    + + + +
    - (nullable PHAsset *)multiplexImageNode:(ASMultiplexImageNode *)imageNode assetForLocalIdentifier:(NSString *)assetLocalIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    imageNode

    The sender.

    assetLocalIdentifier

    The local identifier for a PHAsset that this image node is loading.

    +
    + + + +
    +

    Return Value

    +

    A PHAsset corresponding to assetLocalIdentifier, or nil if none is available.

    +
    + + + + + +
    +

    Discussion

    +

    This optional method can improve image performance if your data source already has the PHAsset available. +If this method is not implemented, or returns nil, the image node will request the asset from the Photos framework.

    Note: This method may be called from any thread.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASMultiplexImageNodeDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASMultiplexImageNodeDelegate.html new file mode 100755 index 0000000000..100ffd6038 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASMultiplexImageNodeDelegate.html @@ -0,0 +1,550 @@ + + + + + + ASMultiplexImageNodeDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASMultiplexImageNodeDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASMultiplexImageNode.h
    + + + + +
    + +

    Overview

    +

    The methods declared by the ASMultiplexImageNodeDelegate protocol allow the adopting delegate to respond to +notifications such as began, progressed and finished downloading, updated and displayed an image.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – multiplexImageNode:didStartDownloadOfImageWithIdentifier: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node began downloading an image.

    +
    + + + +
    - (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didStartDownloadOfImageWithIdentifier:(id)imageIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    imageNode

    The sender.

    imageIdentifier

    The identifier for the image that is downloading.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNode:didUpdateDownloadProgress:forImageWithIdentifier: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node’s download progressed.

    +
    + + + +
    - (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didUpdateDownloadProgress:(CGFloat)downloadProgress forImageWithIdentifier:(ASImageIdentifier)imageIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    imageNode

    The sender.

    downloadProgress

    The progress of the download. Value is between 0.0 and 1.0.

    imageIdentifier

    The identifier for the image that is downloading.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNode:didFinishDownloadingImageWithIdentifier:error: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node’s download has finished.

    +
    + + + +
    - (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier error:(nullable NSError *)error
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    imageNode

    The sender.

    imageIdentifier

    The identifier for the image that finished downloading.

    error

    The error that occurred while downloading, if one occurred; nil otherwise.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNode:didUpdateImage:withIdentifier:fromImage:withIdentifier: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node’s image was updated.

    +
    + + + +
    - (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didUpdateImage:(nullable UIImage *)image withIdentifier:(nullable ASImageIdentifier)imageIdentifier fromImage:(nullable UIImage *)previousImage withIdentifier:(nullable ASImageIdentifier)previousImageIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    imageNode

    The sender.

    image

    The new image, ready for display.

    imageIdentifier

    The identifier for image.

    previousImage

    The old, previously-loaded image.

    previousImageIdentifier

    The identifier for previousImage.

    +
    + + + + + + + +
    +

    Discussion

    +

    Note: This method does not indicate that image has been displayed.

    +
    + + + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNode:didDisplayUpdatedImage:withIdentifier: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node displayed a new image.

    +
    + + + +
    - (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didDisplayUpdatedImage:(nullable UIImage *)image withIdentifier:(nullable ASImageIdentifier)imageIdentifier
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    imageNode

    The sender.

    image

    The new image, now being displayed.

    imageIdentifier

    The identifier for image.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method is only called when image changes, and not on subsequent redisplays of the same image.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    + +

    – multiplexImageNodeDidFinishDisplay: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node finished displaying an image.

    +
    + + + +
    - (void)multiplexImageNodeDidFinishDisplay:(ASMultiplexImageNode *)imageNode
    + + + +
    +

    Parameters

    + + + + + + + +
    imageNode

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method is called every time an image is displayed, whether or not it has changed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASMultiplexImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASNetworkImageNodeDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASNetworkImageNodeDelegate.html new file mode 100755 index 0000000000..993331c3f7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASNetworkImageNodeDelegate.html @@ -0,0 +1,386 @@ + + + + + + ASNetworkImageNodeDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASNetworkImageNodeDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASNetworkImageNode.h
    + + + + +
    + +

    Overview

    +

    The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to +notifications such as finished decoding and downloading an image.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – imageNode:didLoadImage: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node finished downloading an image.

    +
    + + + +
    - (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    imageNode

    The sender.

    image

    The newly-loaded image.

    +
    + + + + + + + +
    +

    Discussion

    +

    Called on a background queue.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

    – imageNodeDidStartFetchingData: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node started to load

    +
    + + + +
    - (void)imageNodeDidStartFetchingData:(ASNetworkImageNode *)imageNode
    + + + +
    +

    Parameters

    + + + + + + + +
    imageNode

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    Called on a background queue.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

    – imageNode:didFailWithError: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node failed to download the image.

    +
    + + + +
    - (void)imageNode:(ASNetworkImageNode *)imageNode didFailWithError:(NSError *)error
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    imageNode

    The sender.

    error

    The error with details.

    +
    + + + + + + + +
    +

    Discussion

    +

    Called on a background queue.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    + +

    – imageNodeDidFinishDecoding: +

    + +
    +
    + +
    + + +
    +

    Notification that the image node finished decoding an image.

    +
    + + + +
    - (void)imageNodeDidFinishDecoding:(ASNetworkImageNode *)imageNode
    + + + +
    +

    Parameters

    + + + + + + + +
    imageNode

    The sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASNetworkImageNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASPagerDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASPagerDataSource.html new file mode 100755 index 0000000000..6cea3bee54 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASPagerDataSource.html @@ -0,0 +1,325 @@ + + + + + + ASPagerDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASPagerDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASPagerNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – numberOfPagesInPagerNode: +required method

    + +
    +
    + +
    + + +
    +

    This method replaces -collectionView:numberOfItemsInSection:

    +
    + + + +
    - (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode
    + + + +
    +

    Parameters

    + + + + + + + +
    pagerNode

    The sender.

    +
    + + + +
    +

    Return Value

    +

    The total number of pages that can display in the pagerNode.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – pagerNode:nodeAtIndex: +

    + +
    +
    + +
    + + +
    +

    This method replaces -collectionView:nodeForItemAtIndexPath:

    +
    + + + +
    - (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    pagerNode

    The sender.

    index

    The index of the requested node.

    +
    + + + +
    +

    Return Value

    +

    a node for display at this index. This will be called on the main thread and should +not implement reuse (it will be called once per row). Unlike UICollectionView’s version, +this method is not called when the row is about to display.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    + +

    – pagerNode:nodeBlockAtIndex: +

    + +
    +
    + +
    + + +
    +

    This method replaces -collectionView:nodeBlockForItemAtIndexPath: +This method takes precedence over pagerNode:nodeAtIndex: if implemented.

    +
    + + + +
    - (ASCellNodeBlock)pagerNode:(ASPagerNode *)pagerNode nodeBlockAtIndex:(NSInteger)index
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    pagerNode

    The sender.

    index

    The index of the requested node.

    +
    + + + +
    +

    Return Value

    +

    a block that creates the node for display at this index. +Must be thread-safe (can be called on the main thread or a background +queue) and should not implement reuse (it will be called once per row).

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASPagerDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASPagerDelegate.html new file mode 100755 index 0000000000..6f7e47f1f7 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASPagerDelegate.html @@ -0,0 +1,195 @@ + + + + + + ASPagerDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASPagerDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASCollectionDelegate
    Declared inASPagerNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – pagerNode:constrainedSizeForNodeAtIndex: +

    + +
    +
    + +
    + + +
    +

    Provides the constrained size range for measuring the node at the index.

    +
    + + + +
    - (ASSizeRange)pagerNode:(ASPagerNode *)pagerNode constrainedSizeForNodeAtIndex:(NSInteger)index
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    pagerNode

    The sender.

    index

    The index of the node.

    +
    + + + +
    +

    Return Value

    +

    A constrained size range for layout the node at this index.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASPagerNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASRangeControllerDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASRangeControllerDataSource.html new file mode 100755 index 0000000000..2aa1157e93 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASRangeControllerDataSource.html @@ -0,0 +1,385 @@ + + + + + + ASRangeControllerDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRangeControllerDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASRangeController.h
    + + + + +
    + +

    Overview

    +

    Data source for ASRangeController.

    + +

    Allows the range controller to perform external queries on the range. +Ex. range nodes, visible index paths, and viewport size.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – visibleNodeIndexPathsForRangeController: +required method

    + +
    +
    + +
    + + +
    +

    Sender.

    +
    + + + +
    - (NSArray<NSIndexPath*> *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeController

    Sender.

    +
    + + + +
    +

    Return Value

    +

    an array of index paths corresponding to the nodes currently visible onscreen (i.e., the visible range).

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – scrollDirectionForRangeController: +required method

    + +
    +
    + +
    + + +
    +

    Sender.

    +
    + + + +
    - (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeController

    Sender.

    +
    + + + +
    +

    Return Value

    +

    the current scroll direction of the view using this range controller.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – viewportSizeForRangeController: +required method

    + +
    +
    + +
    + + +
    +

    Sender.

    +
    + + + +
    - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeController

    Sender.

    +
    + + + +
    +

    Return Value

    +

    the receiver’s viewport size (i.e., the screen space occupied by the visible range).

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – interfaceStateForRangeController: +required method

    + +
    +
    + +
    + + +
    +

    Sender.

    +
    + + + +
    - (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeController

    Sender.

    +
    + + + +
    +

    Return Value

    +

    the ASInterfaceState of the node that this controller is powering. This allows nested range controllers +to collaborate with one another, as an outer controller may set bits in .interfaceState such as Visible. +If this controller is an orthogonally scrolling element, it waits until it is visible to preload outside the viewport.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASRangeControllerDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASRangeControllerDelegate.html new file mode 100755 index 0000000000..e97a5829f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASRangeControllerDelegate.html @@ -0,0 +1,585 @@ + + + + + + ASRangeControllerDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASRangeControllerDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASRangeController.h
    + + + + +
    + +

    Overview

    +

    Delegate for ASRangeController.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – didBeginUpdatesInRangeController: +required method

    + +
    +
    + +
    + + +
    +

    Begin updates.

    +
    + + + +
    - (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeController

    Sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – rangeController:didEndUpdatesAnimated:completion: +required method

    + +
    +
    + +
    + + +
    +

    End updates.

    +
    + + + +
    - (void)rangeController:(ASRangeController *)rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void ( ^ ) ( BOOL ))completion
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    rangeController

    Sender.

    animated

    NO if all animations are disabled. YES otherwise.

    completion

    Completion block.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – didCompleteUpdatesInRangeController: +required method

    + +
    +
    + +
    + + +
    +

    Completed updates to cell node addition and removal.

    +
    + + + +
    - (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController
    + + + +
    +

    Parameters

    + + + + + + + +
    rangeController

    Sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – rangeController:didInsertNodes:atIndexPaths:withAnimationOptions: +required method

    + +
    +
    + +
    + + +
    +

    Called for nodes insertion.

    +
    + + + +
    - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray<ASCellNode*> *)nodes atIndexPaths:(NSArray<NSIndexPath*> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    rangeController

    Sender.

    nodes

    Inserted nodes.

    indexPaths

    Index path of inserted nodes.

    animationOptions

    Animation options. See ASDataControllerAnimationOptions.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – rangeController:didDeleteNodes:atIndexPaths:withAnimationOptions: +required method

    + +
    +
    + +
    + + +
    +

    Called for nodes deletion.

    +
    + + + +
    - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray<ASCellNode*> *)nodes atIndexPaths:(NSArray<NSIndexPath*> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    rangeController

    Sender.

    nodes

    Deleted nodes.

    indexPaths

    Index path of deleted nodes.

    animationOptions

    Animation options. See ASDataControllerAnimationOptions.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – rangeController:didInsertSectionsAtIndexSet:withAnimationOptions: +required method

    + +
    +
    + +
    + + +
    +

    Called for section insertion.

    +
    + + + +
    - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    rangeController

    Sender.

    indexSet

    Index set of inserted sections.

    animationOptions

    Animation options. See ASDataControllerAnimationOptions.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    + +

    – rangeController:didDeleteSectionsAtIndexSet:withAnimationOptions: +required method

    + +
    +
    + +
    + + +
    +

    Called for section deletion.

    +
    + + + +
    - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    rangeController

    Sender.

    indexSet

    Index set of deleted sections.

    animationOptions

    Animation options. See ASDataControllerAnimationOptions.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASRangeController.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASStackLayoutElement.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASStackLayoutElement.html new file mode 100755 index 0000000000..97a44dd6a6 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASStackLayoutElement.html @@ -0,0 +1,493 @@ + + + + + + ASStackLayoutElement Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASStackLayoutElement Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASStackLayoutElement.h
    + + + + +
    + +

    Overview

    +

    Layout options that can be defined for an ASLayoutElement being added to a ASStackLayoutSpec.

    +
    + + + + + +
    + + + + + + +
    +
    + +

      spacingBefore +required method

    + +
    +
    + +
    + + +
    +

    Additional space to place before this object in the stacking direction. +Used when attached to a stack layout.

    +
    + + + +
    @property (nonatomic, readwrite) CGFloat spacingBefore
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      spacingAfter +required method

    + +
    +
    + +
    + + +
    +

    Additional space to place after this object in the stacking direction. +Used when attached to a stack layout.

    +
    + + + +
    @property (nonatomic, readwrite) CGFloat spacingAfter
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      flexGrow +required method

    + +
    +
    + +
    + + +
    +

    If the sum of childrens' stack dimensions is less than the minimum size, how much should this component grow? +This value represents the “flex grow factor” and determines how much this component should grow in relation to any +other flexible children.

    +
    + + + +
    @property (nonatomic, readwrite) CGFloat flexGrow
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      flexShrink +required method

    + +
    +
    + +
    + + +
    +

    If the sum of childrens' stack dimensions is greater than the maximum size, how much should this component shrink? +This value represents the “flex shrink factor” and determines how much this component should shink in relation to +other flexible children.

    +
    + + + +
    @property (nonatomic, readwrite) CGFloat flexShrink
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      flexBasis +required method

    + +
    +
    + +
    + + +
    +

    Specifies the initial size in the stack dimension for this object. +Default to ASDimensionAuto +Used when attached to a stack layout.

    +
    + + + +
    @property (nonatomic, readwrite) ASDimension flexBasis
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      alignSelf +required method

    + +
    +
    + +
    + + +
    +

    Orientation of the object along cross axis, overriding alignItems +Used when attached to a stack layout.

    +
    + + + +
    @property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      ascender +required method

    + +
    +
    + +
    + + +
    +

    Used for baseline alignment. The distance from the top of the object to its baseline.

    +
    + + + +
    @property (nonatomic, readwrite) CGFloat ascender
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    + +

      descender +required method

    + +
    +
    + +
    + + +
    +

    Used for baseline alignment. The distance from the baseline of the object to its bottom.

    +
    + + + +
    @property (nonatomic, readwrite) CGFloat descender
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASStackLayoutElement.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTableDataSource.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTableDataSource.html new file mode 100755 index 0000000000..c6c4cc407c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTableDataSource.html @@ -0,0 +1,614 @@ + + + + + + ASTableDataSource Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTableDataSource Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASCommonTableDataSource
    NSObject
    Declared inASTableNode.h
    + + + + +
    + +

    Overview

    +

    This is a node-based UITableViewDataSource.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – numberOfSectionsInTableNode: +

    + +
    +
    + +
    + + +
    +

    Asks the data source for the number of sections in the table node.

    +
    + + + +
    - (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableNode:numberOfRowsInSection: +

    + +
    +
    + +
    + + +
    +

    Asks the data source for the number of rows in the given section of the table node.

    +
    + + + +
    - (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableNode:nodeBlockForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Asks the data source for a block to create a node to represent the row at the given index path. +The block will be run by the table node concurrently in the background before the row is inserted +into the table view.

    +
    + + + +
    - (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableNode

    The sender.

    indexPath

    The index path of the row.

    +
    + + + +
    +

    Return Value

    +

    a block that creates the node for display at this indexpath. +Must be thread-safe (can be called on the main thread or a background +queue) and should not implement reuse (it will be called once per row).

    +
    + + + + + +
    +

    Discussion

    +

    Note: This method takes precedence over tableNode:nodeForRowAtIndexPath: if implemented.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableNode:nodeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Asks the data source for a node to represent the row at the given index path.

    +
    + + + +
    - (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableNode

    The sender.

    indexPath

    The index path of the row.

    +
    + + + +
    +

    Return Value

    +

    a node to display for this row. This will be called on the main thread and should not implement reuse (it will be called once per row). Unlike UITableView’s version, this method +is not called when the row is about to display.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:nodeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -tableView:cellForRowAtIndexPath:.

    +
    + + + +
    - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    indexPath

    The index path of the requested node.

    tableNode

    The sender.

    +
    + + + +
    +

    Return Value

    +

    a node for display at this indexpath. This will be called on the main thread and should not implement reuse (it will be called once per row). Unlike UITableView’s version, this method +is not called when the row is about to display.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:nodeBlockForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Similar to -tableView:nodeForRowAtIndexPath: +This method takes precedence over tableView:nodeForRowAtIndexPath: if implemented.

    +
    + + + +
    - (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableView

    The sender.

    indexPath

    The index path of the requested node.

    +
    + + + +
    +

    Return Value

    +

    a block that creates the node for display at this indexpath. +Must be thread-safe (can be called on the main thread or a background +queue) and should not implement reuse (it will be called once per row).

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableViewLockDataSource: +

    + +
    +
    + +
    + + +
    +

    Indicator to lock the data source for data fetching in async mode. +We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception +due to the data access in async mode. (Deprecated: The data source is always accessed on the main thread, and this method will not be called.)

    +
    + + + +
    - (void)tableViewLockDataSource:(ASTableView *)tableView
    + + + +
    +

    Parameters

    + + + + + + + +
    tableView

    The sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableViewUnlockDataSource: +

    + +
    +
    + +
    + + +
    +

    Indicator to unlock the data source for data fetching in asyn mode. +We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistency or exception +due to the data access in async mode. (Deprecated: The data source is always accessed on the main thread, and this method will not be called.)

    +
    + + + +
    - (void)tableViewUnlockDataSource:(ASTableView *)tableView
    + + + +
    +

    Parameters

    + + + + + + + +
    tableView

    The sender.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTableDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTableDelegate.html new file mode 100755 index 0000000000..993e7cac16 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTableDelegate.html @@ -0,0 +1,758 @@ + + + + + + ASTableDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTableDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASCommonTableViewDelegate
    NSObject
    Declared inASTableNode.h
    + + + + +
    + +

    Overview

    +

    This is a node-based UITableViewDelegate.

    + +

    Note that -tableView:heightForRowAtIndexPath: has been removed; instead, your custom ASCellNode subclasses are +responsible for deciding their preferred onscreen height in -calculateSizeThatFits:.

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – tableNode:constrainedSizeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Provides the constrained size range for measuring the row at the index path. +Note: the widths in the returned size range are ignored!

    +
    + + + +
    - (ASSizeRange)tableNode:(ASTableNode *)tableNode constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableNode

    The sender.

    indexPath

    The index path of the node.

    +
    + + + +
    +

    Return Value

    +

    A constrained size range for layout the node at this index path.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableNode:willBeginBatchFetchWithContext: +

    + +
    +
    + +
    + + +
    +

    Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary.

    +
    + + + +
    - (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    context

    A context object that must be notified when the batch fetch is completed.

    tableView

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    You must eventually call -completeBatchFetching: with an argument of YES in order to receive future +notifications to do batch fetches. This method is called on a background queue.

    + +

    ASTableView currently only supports batch events for tail loads. If you require a head load, consider implementing a +UIRefreshControl.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – shouldBatchFetchForTableNode: +

    + +
    +
    + +
    + + +
    +

    Tell the tableView if batch fetching should begin.

    +
    + + + +
    - (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode
    + + + +
    +

    Parameters

    + + + + + + + +
    tableView

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to conditionally fetch batches. Example use cases are: limiting the total number of +objects that can be fetched or no network connection.

    + +

    If not implemented, the tableView assumes that it should notify its asyncDelegate when batch fetching +should occur.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:willDisplayNode:forRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the table view will add the given node +at the given index path to the view hierarchy.

    +
    + + + +
    - (void)tableView:(ASTableView *)tableView willDisplayNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    tableView

    The sender.

    node

    The node that will be displayed.

    indexPath

    The index path of the row that will be displayed.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: AsyncDisplayKit processes table view edits asynchronously. The index path +passed into this method may not correspond to the same item in your data source +if your data source has been updated since the last edit was processed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:didEndDisplayingNode:forRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the table view did remove the provided node from the view hierarchy. +This may be caused by the node scrolling out of view, or by deleting the row +or its containing section with @c deleteRowsAtIndexPaths:withRowAnimation: or @c deleteSections:withRowAnimation: .

    +
    + + + +
    - (void)tableView:(ASTableView *)tableView didEndDisplayingNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    tableView

    The sender.

    node

    The node which was removed from the view hierarchy.

    indexPath

    The index path at which the node was located before the removal.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: AsyncDisplayKit processes table view edits asynchronously. The index path +passed into this method may not correspond to the same item in your data source +if your data source has been updated since the last edit was processed.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:willBeginBatchFetchWithContext: +

    + +
    +
    + +
    + + +
    +

    Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary.

    +
    + + + +
    - (void)tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableView

    The sender.

    context

    A context object that must be notified when the batch fetch is completed.

    +
    + + + + + + + +
    +

    Discussion

    +

    You must eventually call -completeBatchFetching: with an argument of YES in order to receive future +notifications to do batch fetches. This method is called on a background queue.

    + +

    ASTableView currently only supports batch events for tail loads. If you require a head load, consider implementing a +UIRefreshControl.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – shouldBatchFetchForTableView: +

    + +
    +
    + +
    + + +
    +

    Tell the tableView if batch fetching should begin.

    +
    + + + +
    - (BOOL)shouldBatchFetchForTableView:(ASTableView *)tableView
    + + + +
    +

    Parameters

    + + + + + + + +
    tableView

    The sender.

    +
    + + + + + + + +
    +

    Discussion

    +

    Use this method to conditionally fetch batches. Example use cases are: limiting the total number of +objects that can be fetched or no network connection.

    + +

    If not implemented, the tableView assumes that it should notify its asyncDelegate when batch fetching +should occur.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:constrainedSizeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Provides the constrained size range for measuring the row at the index path. +Note: the widths in the returned size range are ignored!

    +
    + + + +
    - (ASSizeRange)tableView:(ASTableView *)tableView constrainedSizeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableView

    The sender.

    indexPath

    The index path of the node.

    +
    + + + +
    +

    Return Value

    +

    A constrained size range for layout the node at this index path.

    +
    + + + + + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    + +

    – tableView:willDisplayNodeForRowAtIndexPath: +

    + +
    +
    + +
    + + +
    +

    Informs the delegate that the table view will add the node +at the given index path to the view hierarchy.

    +
    + + + +
    - (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    tableView

    The sender.

    indexPath

    The index path of the row that will be displayed.

    +
    + + + + + + + +
    +

    Discussion

    +

    Warning: AsyncDisplayKit processes table view edits asynchronously. The index path +passed into this method may not correspond to the same item in your data source +if your data source has been updated since the last edit was processed.

    + +

    This method is deprecated. Use @c tableView:willDisplayNode:forRowAtIndexPath: instead.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTableNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTextNodeDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTextNodeDelegate.html new file mode 100755 index 0000000000..a5945ed623 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASTextNodeDelegate.html @@ -0,0 +1,450 @@ + + + + + + ASTextNodeDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASTextNodeDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASTextNode.h
    + + + + + + + + +
    + + + + + + +
    +
    + +

    – textNode:tappedLinkAttribute:value:atPoint:textRange: +

    + +
    +
    + +
    + + +
    +

    Indicates to the delegate that a link was tapped within a text node.

    +
    + + + +
    - (void)textNode:(ASTextNode *)textNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    textNode

    The ASTextNode containing the link that was tapped.

    attribute

    The attribute that was tapped. Will not be nil.

    value

    The value of the tapped attribute.

    point

    The point within textNode, in textNode’s coordinate system, that was tapped.

    textRange

    The range of highlighted text.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – textNode:longPressedLinkAttribute:value:atPoint:textRange: +

    + +
    +
    + +
    + + +
    +

    Indicates to the delegate that a link was tapped within a text node.

    +
    + + + +
    - (void)textNode:(ASTextNode *)textNode longPressedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    textNode

    The ASTextNode containing the link that was tapped.

    attribute

    The attribute that was tapped. Will not be nil.

    value

    The value of the tapped attribute.

    point

    The point within textNode, in textNode’s coordinate system, that was tapped.

    textRange

    The range of highlighted text.

    +
    + + + + + + + +
    +

    Discussion

    +

    In addition to implementing this method, the delegate must be set on the text + node before it is loaded (the recognizer is created in -didLoad)

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – textNode:shouldHighlightLinkAttribute:value:atPoint: +

    + +
    +
    + +
    + + +
    +

    Indicates to the text node if an attribute should be considered a link.

    +
    + + + +
    - (BOOL)textNode:(ASTextNode *)textNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    textNode

    The text node containing the entity attribute.

    attribute

    The attribute that was tapped. Will not be nil.

    value

    The value of the tapped attribute.

    point

    The point within textNode, in textNode’s coordinate system, that was touched to trigger a highlight.

    +
    + + + +
    +

    Return Value

    +

    YES if the entity attribute should be a link, NO otherwise.

    +
    + + + + + +
    +

    Discussion

    +

    If not implemented, the default value is YES.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    + +

    – textNode:shouldLongPressLinkAttribute:value:atPoint: +

    + +
    +
    + +
    + + +
    +

    Indicates to the text node if an attribute is a valid long-press target

    +
    + + + +
    - (BOOL)textNode:(ASTextNode *)textNode shouldLongPressLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + + + + + + +
    textNode

    The text node containing the entity attribute.

    attribute

    The attribute that was tapped. Will not be nil.

    value

    The value of the tapped attribute.

    point

    The point within textNode, in textNode’s coordinate system, that was long-pressed.

    +
    + + + +
    +

    Return Value

    +

    YES if the entity attribute should be treated as a long-press target, NO otherwise.

    +
    + + + + + +
    +

    Discussion

    +

    If not implemented, the default value is NO.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASTextNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVideoNodeDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVideoNodeDelegate.html new file mode 100755 index 0000000000..10b8f8505f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVideoNodeDelegate.html @@ -0,0 +1,729 @@ + + + + + + ASVideoNodeDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASVideoNodeDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toASNetworkImageNodeDelegate
    Declared inASVideoNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – videoDidPlayToEnd: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the node’s video has played to its end time.

    +
    + + + +
    - (void)videoDidPlayToEnd:(ASVideoNode *)videoNode
    + + + +
    +

    Parameters

    + + + + + + + +
    videoNode

    The video node has played to its end time.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – didTapVideoNode: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked the node is tapped.

    +
    + + + +
    - (void)didTapVideoNode:(ASVideoNode *)videoNode
    + + + +
    +

    Parameters

    + + + + + + + +
    videoNode

    The video node that was tapped.

    +
    + + + + + + + +
    +

    Discussion

    +

    The video’s play state is toggled if this method is not implemented.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNode:willChangePlayerState:toState: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when player changes state.

    +
    + + + +
    - (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    videoNode

    The video node.

    state

    player state before this change.

    toState

    player new state.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method is called after each state change

    +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNode:shouldChangePlayerStateTo: +

    + +
    +
    + +
    + + +
    +

    Ssks delegate if state change is allowed +ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused. +asks delegate if state change is allowed.

    +
    + + + +
    - (BOOL)videoNode:(ASVideoNode *)videoNode shouldChangePlayerStateTo:(ASVideoNodePlayerState)state
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    videoNode

    The video node.

    state

    player state that is going to be set.

    +
    + + + + + + + +
    +

    Discussion

    +

    Delegate method invoked when player changes it’s state to +ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused +and asks delegate if state change is valid

    +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNode:didPlayToTimeInterval: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when player playback time is updated.

    +
    + + + +
    - (void)videoNode:(ASVideoNode *)videoNode didPlayToTimeInterval:(NSTimeInterval)timeInterval
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    videoNode

    The video node.

    second

    current playback time in seconds.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNode:didStallAtTimeInterval: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the video player stalls.

    +
    + + + +
    - (void)videoNode:(ASVideoNode *)videoNode didStallAtTimeInterval:(NSTimeInterval)timeInterval
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    videoNode

    The video node that has experienced the stall

    second

    Current playback time when the stall happens

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNodeDidStartInitialLoading: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the video player starts the inital asset loading

    +
    + + + +
    - (void)videoNodeDidStartInitialLoading:(ASVideoNode *)videoNode
    + + + +
    +

    Parameters

    + + + + + + + +
    videoNode

    The videoNode

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNodeDidFinishInitialLoading: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the video is done loading the asset and can start the playback

    +
    + + + +
    - (void)videoNodeDidFinishInitialLoading:(ASVideoNode *)videoNode
    + + + +
    +

    Parameters

    + + + + + + + +
    videoNode

    The videoNode

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNode:didSetCurrentItem: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the AVPlayerItem for the asset has been set up and can be accessed throught currentItem.

    +
    + + + +
    - (void)videoNode:(ASVideoNode *)videoNode didSetCurrentItem:(AVPlayerItem *)currentItem
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    videoNode

    The videoNode.

    currentItem

    The AVPlayerItem that was constructed from the asset.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    + +

    – videoNodeDidRecoverFromStall: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the video node has recovered from the stall

    +
    + + + +
    - (void)videoNodeDidRecoverFromStall:(ASVideoNode *)videoNode
    + + + +
    +

    Parameters

    + + + + + + + +
    videoNode

    The videoNode

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVideoPlayerNodeDelegate.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVideoPlayerNodeDelegate.html new file mode 100755 index 0000000000..89faa40314 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVideoPlayerNodeDelegate.html @@ -0,0 +1,940 @@ + + + + + + ASVideoPlayerNodeDelegate Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASVideoPlayerNodeDelegate Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASVideoPlayerNode.h
    + + + + + + +
    + + + + + + +
    +
    + +

    – videoPlayerNodeNeededDefaultControls: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked before creating controlbar controls +@param videoPlayer

    +
    + + + +
    - (NSArray *)videoPlayerNodeNeededDefaultControls:(ASVideoPlayerNode *)videoPlayer
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeCustomControls: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked before creating default controls, asks delegate for custom controls dictionary. +This dictionary must constain only ASDisplayNode subclass objects. +@param videoPlayer

    +
    + + + +
    - (NSDictionary *)videoPlayerNodeCustomControls:(ASVideoPlayerNode *)videoPlayer
    + + + + + + + + + +
    +

    Discussion

    +
      +
    • This method is invoked only when developer implements videoPlayerNodeLayoutSpec:forControls:forMaximumSize: +and gives ability to add custom constrols to ASVideoPlayerNode, for example mute button.
    • +
    + +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeLayoutSpec:forControls:forMaximumSize: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked in layoutSpecThatFits: +@param videoPlayer

    +
    + + + +
    - (ASLayoutSpec *)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer forControls:(NSDictionary *)controls forMaximumSize:(CGSize)maxSize
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    controls
      +
    • Dictionary of controls which are used in videoPlayer; Dictionary keys are ASVideoPlayerNodeControlType
    • +
    +
    maxSize
      +
    • Maximum size for ASVideoPlayerNode
    • +
    +
    +
    + + + + + + + +
    +

    Discussion

    +
      +
    • Developer can layout whole ASVideoPlayerNode as he wants. ASVideoNode is locked and it can’t be changed
    • +
    + +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeTimeLabelAttributes:timeLabelType: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked before creating ASVideoPlayerNodeControlTypeElapsedText and ASVideoPlayerNodeControlTypeDurationText +@param videoPlayer +@param timeLabelType

    +
    + + + +
    - (NSDictionary *)videoPlayerNodeTimeLabelAttributes:(ASVideoPlayerNode *)videoPlayerNode timeLabelType:(ASVideoPlayerNodeControlType)timeLabelType
    + + + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – didTapVideoPlayerNode: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when ASVideoPlayerNode is taped.

    +
    + + + +
    - (void)didTapVideoPlayerNode:(ASVideoPlayerNode *)videoPlayer
    + + + +
    +

    Parameters

    + + + + + + + +
    videoPlayerNode

    The ASVideoPlayerNode that was tapped.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNode:didPlayToTime: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when ASVideoNode playback time is updated.

    +
    + + + +
    - (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer didPlayToTime:(CMTime)time
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    second

    current playback time.

    videoPlayerNode

    The video player node

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNode:willChangeVideoNodeState:toVideoNodeState: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when ASVideoNode changes state.

    +
    + + + +
    - (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer willChangeVideoNodeState:(ASVideoNodePlayerState)state toVideoNodeState:(ASVideoNodePlayerState)toState
    + + + +
    +

    Parameters

    + + + + + + + + + + + + + + + + + +
    state

    ASVideoNode state before this change.

    videoPlayerNode

    The ASVideoPlayerNode whose ASVideoNode is changing state.

    toSate

    ASVideoNode new state.

    +
    + + + + + + + +
    +

    Discussion

    +

    This method is called after each state change

    +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNode:shouldChangeVideoNodeStateTo: +

    + +
    +
    + +
    + + +
    +

    Delegate method is invoked when ASVideoNode decides to change state.

    +
    + + + +
    - (BOOL)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer shouldChangeVideoNodeStateTo:(ASVideoNodePlayerState)state
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    state

    ASVideoNode that is going to be set.

    videoPlayerNode

    The ASVideoPlayerNode whose ASVideoNode is changing state.

    +
    + + + + + + + +
    +

    Discussion

    +

    Delegate method invoked when player changes it’s state to +ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused +and asks delegate if state change is valid

    +
    + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeDidPlayToEnd: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the ASVideoNode has played to its end time.

    +
    + + + +
    - (void)videoPlayerNodeDidPlayToEnd:(ASVideoPlayerNode *)videoPlayer
    + + + +
    +

    Parameters

    + + + + + + + +
    videoPlayer

    The video node has played to its end time.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNode:didSetCurrentItem: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the ASVideoNode has constructed its AVPlayerItem for the asset.

    +
    + + + +
    - (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer didSetCurrentItem:(AVPlayerItem *)currentItem
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    videoPlayer

    The video player node.

    currentItem

    The AVPlayerItem that was constructed from the asset.

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNode:didStallAtTimeInterval: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the ASVideoNode stalls.

    +
    + + + +
    - (void)videoPlayerNode:(ASVideoPlayerNode *)videoPlayer didStallAtTimeInterval:(NSTimeInterval)timeInterval
    + + + +
    +

    Parameters

    + + + + + + + + + + + + +
    videoPlayer

    The video player node that has experienced the stall

    second

    Current playback time when the stall happens

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeDidStartInitialLoading: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the ASVideoNode starts the inital asset loading

    +
    + + + +
    - (void)videoPlayerNodeDidStartInitialLoading:(ASVideoPlayerNode *)videoPlayer
    + + + +
    +

    Parameters

    + + + + + + + +
    videoPlayer

    The videoPlayer

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeDidFinishInitialLoading: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the ASVideoNode is done loading the asset and can start the playback

    +
    + + + +
    - (void)videoPlayerNodeDidFinishInitialLoading:(ASVideoPlayerNode *)videoPlayer
    + + + +
    +

    Parameters

    + + + + + + + +
    videoPlayer

    The videoPlayer

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    + +

    – videoPlayerNodeDidRecoverFromStall: +

    + +
    +
    + +
    + + +
    +

    Delegate method invoked when the ASVideoNode has recovered from the stall

    +
    + + + +
    - (void)videoPlayerNodeDidRecoverFromStall:(ASVideoPlayerNode *)videoPlayer
    + + + +
    +

    Parameters

    + + + + + + + +
    videoPlayer

    The videoplayer

    +
    + + + + + + + + + + + + + +
    +

    Declared In

    +

    ASVideoPlayerNode.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVisibilityDepth.html b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVisibilityDepth.html new file mode 100755 index 0000000000..29597c887f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/Protocols/ASVisibilityDepth.html @@ -0,0 +1,269 @@ + + + + + + ASVisibilityDepth Protocol Reference + + + + + + +
    +
    + +

    + +

    + +

    + Texture +

    + +
    +
    + + + +
    +
    +
    +
    +

    ASVisibilityDepth Protocol Reference

    + + +
    + + + + + + + +
    Conforms toNSObject
    Declared inASVisibilityProtocols.h
    + + + + +
    + +

    Overview

    +

    ASVisibilityDepth

    “Visibility Depth” represents the number of user actions required to make an ASDisplayNode or +ASViewController visibile. Texture uses this information to intelligently manage memory and focus +resources where they are most visible to the user.

    + +

    The ASVisibilityDepth protocol describes how custom view controllers can integrate with this system.

    + +

    Parent view controllers should also implement @c ASManagesChildVisibilityDepth

    +
    + + + + + +
    + + + + + + +
    +
    + +

    – visibilityDepth +required method

    + +
    +
    + +
    + + +
    +

    Visibility depth

    +
    + + + +
    - (NSInteger)visibilityDepth
    + + + + + + + + + +
    +

    Discussion

    +

    Represents the number of user actions necessary to reach the view controller. An increased visibility +depth indicates a higher number of user interactions for the view controller to be visible again. For example, +an onscreen navigation controller’s top view controller should have a visibility depth of 0. The view controller +one from the top should have a visibility deptch of 1 as should the root view controller in the stack (because +the user can hold the back button to pop to the root view controller).

    + +

    Visibility depth is used to automatically adjust ranges on range controllers (and thus free up memory) and can +be used to reduce memory usage of other items as well.

    +
    + + + + + + + +
    +

    Declared In

    +

    ASVisibilityProtocols.h

    +
    + + +
    +
    +
    + +

    – visibilityDepthDidChange +required method

    + +
    +
    + +
    + + +
    +

    Called when visibility depth changes

    +
    + + + +
    - (void)visibilityDepthDidChange
    + + + + + + + + + +
    +

    Discussion

    +

    @c visibilityDepthDidChange is called whenever the visibility depth of the represented view controller +has changed.

    + +

    If implemented by a view controller container, use this method to notify child view controllers that their view +depth has changed @see ASNavigationController.m

    + +

    If implemented on an ASViewController, use this method to reduce or increase the resources that your +view controller uses. A higher visibility depth view controller should decrease it’s resource usage, a lower +visibility depth controller should pre-warm resources in preperation for a display at 0 depth.

    + +

    ASViewController implements this method and reduces / increases range mode of supporting nodes (such as ASCollectionNode +and ASTableNode).

    +
    + + + + + +
    +

    See Also

    + +
    + + + +
    +

    Declared In

    +

    ASVisibilityProtocols.h

    +
    + + +
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_index.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_index.scss new file mode 100755 index 0000000000..6a57ec5dc3 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_index.scss @@ -0,0 +1,17 @@ +.index-container { + -webkit-flex-direction: column; + flex-direction: column; + + @media (min-width: $desktop-min-width) { + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; + } + + .index-column { + -webkit-flex: 1 1 33%; + flex: 1 1 33%; + } +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_layout.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_layout.scss new file mode 100755 index 0000000000..da46aef079 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_layout.scss @@ -0,0 +1,302 @@ +* { + box-sizing: border-box; +} + +.clear { + clear: both; +} + +.clearfix { + &:before, &:after { + clear: both; + display: table; + content: ""; + } +} + +.xcode .hide-in-xcode { + display: none; +} + +body { + font: 62.5% $body-font; + background: $body-background; + + @media (max-width: $mobile-max-width) { + background-color: $content-background; + } +} + +h1, h2, h3 { + font-weight: 300; + color: #808080; +} + +h1 { + font-size: 2em; + color: #000; +} + +h4 { + font-size: 13px; + line-height: 1.5; + margin: 21px 0 0 0; +} + +a { + color: $tint-color; + text-decoration: none; +} + +pre, code { + font-family: $code-font; + word-wrap: break-word; +} + +pre > code, .method-declaration code { + display: inline-block; + font-size: .85em; + padding: 4px 0 4px 10px; + border-left: 5px solid rgba(0, 155, 51, .2); + + &:before { + content: "Objective-C"; + display: block; + + font: 9px/1 $body-font; + color: #009b33; + text-transform: uppercase; + letter-spacing: 2px; + padding-bottom: 6px; + } +} + +pre > code { + font-size: inherit; +} + +table, th, td { + border: 1px solid #e9e9e9; +} + +table { + width: 100%; +} + +th, td { + padding: 7px; + + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } +} + +.container { + @extend .clearfix; + + max-width: 980px; + padding: 0 10px; + margin: 0 auto; + + @media (max-width: $mobile-max-width) { + padding: 0; + } +} + +header { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 2; + + background: #414141; + color: #fff; + font-size: 1.1em; + line-height: 25px; + letter-spacing: .05em; + + #library-title { + float: left; + } + + #developer-home { + float: right; + } + + h1 { + font-size: inherit; + font-weight: inherit; + margin: 0; + } + + p { + margin: 0; + } + + h1, a { + color: inherit; + } + + @media (max-width: $mobile-max-width) { + .container { + padding: 0 10px; + } + } +} + +aside { + position: fixed; + top: 25px; + left: 0; + width: 100%; + height: 25px; + z-index: 2; + + font-size: 1.1em; + + #header-buttons { + background: rgba(255, 255, 255, .8); + margin: 0 1px; + padding: 0; + list-style: none; + text-align: right; + line-height: 32px; + + li { + display: inline-block; + cursor: pointer; + padding: 0 10px; + } + + label, select { + cursor: inherit; + } + + #on-this-page { + position: relative; + + .chevron { + display: inline-block; + width: 14px; + height: 4px; + position: relative; + + .chevy { + background: #878787; + height: 2px; + position: absolute; + width: 10px; + + &.chevron-left { + left: 0; + transform: rotateZ(45deg) scale(0.6); + } + + &.chevron-right { + right: 0; + transform: rotateZ(-45deg) scale(0.6); + } + } + } + + #jump-to { + opacity: 0; + font-size: 16px; + + position: absolute; + top: 5px; + left: 0; + width: 100%; + height: 100%; + } + } + } +} + +article { + margin-top: 25px; + + #content { + @extend .clearfix; + + background: $content-background; + border: 1px solid $content-border; + padding: 15px 25px 30px 25px; + + font-size: 1.4em; + line-height: 1.45; + + position: relative; + + @media (max-width: $mobile-max-width) { + padding: 15px 10px 20px 10px; + border: none; + } + + .navigation-top { + position: absolute; + top: 15px; + right: 25px; + } + + .title { + margin: 21px 0 0 0; + padding: 15px 0; + } + + p { + color: #414141; + margin: 0 0 15px 0; + } + + th, td { + p:last-child { + margin-bottom: 0; + } + } + + main { + ul { + list-style: none; + margin-left: 24px; + margin-bottom: 12px; + padding: 0; + + li { + position: relative; + padding-left: 1.3em; + + &:before { + content: "\02022"; + + color: #414141; + font-size: 1.08em; + line-height: 1; + + position: absolute; + left: 0; + padding-top: 2px; + } + } + } + } + + footer { + @extend .clearfix; + + .footer-copyright { + margin: 70px 25px 10px 0; + } + + p { + font-size: .71em; + color: #a0a0a0; + } + } + } +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_normalize.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_normalize.scss new file mode 100755 index 0000000000..9b8848a5cf --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_normalize.scss @@ -0,0 +1,581 @@ +/* ========================================================================== + Normalize.scss settings + ========================================================================== */ +/** + * Includes legacy browser support IE6/7 + * + * Set to false if you want to drop support for IE6 and IE7 + */ + +$legacy_browser_support: false !default; + +/* Base + ========================================================================== */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + * 3. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using + * `em` units. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ + @if $legacy_browser_support { + *font-size: 100%; /* 3 */ + } +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ + @if $legacy_browser_support { + *display: inline; + *zoom: 1; + } +} + +/** + * Prevents modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a { + &:active, &:hover { + outline: 0; + }; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +@if $legacy_browser_support { + blockquote { + margin: 1em 40px; + } +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +@if $legacy_browser_support { + h2 { + font-size: 1.5em; + margin: 0.83em 0; + } + + h3 { + font-size: 1.17em; + margin: 1em 0; + } + + h4 { + font-size: 1em; + margin: 1.33em 0; + } + + h5 { + font-size: 0.83em; + margin: 1.67em 0; + } + + h6 { + font-size: 0.67em; + margin: 2.33em 0; + } +} + +/** + * Addresses styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +@if $legacy_browser_support { + + /** + * Addresses margins set differently in IE 6/7. + */ + + p, + pre { + *margin: 1em 0; + } + + /* + * Addresses CSS quotes not supported in IE 6/7. + */ + + q { + *quotes: none; + } + + /* + * Addresses `quotes` property not supported in Safari 4. + */ + + q:before, + q:after { + content: ''; + content: none; + } +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +@if $legacy_browser_support { + + /* ========================================================================== + Lists + ========================================================================== */ + + /* + * Addresses margins set differently in IE 6/7. + */ + + dl, + menu, + ol, + ul { + *margin: 1em 0; + } + + dd { + *margin: 0 0 0 40px; + } + + /* + * Addresses paddings set differently in IE 6/7. + */ + + menu, + ol, + ul { + *padding: 0 0 0 40px; + } + + /* + * Corrects list images handled incorrectly in IE 7. + */ + + nav ul, + nav ol { + *list-style: none; + *list-style-image: none; + } + +} + +/* Embedded content + ========================================================================== */ + +/** + * 1. Remove border when inside `a` element in IE 8/9/10. + * 2. Improves image quality when scaled in IE 7. + */ + +img { + border: 0; + @if $legacy_browser_support { + *-ms-interpolation-mode: bicubic; /* 2 */ + } +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + @if $legacy_browser_support { + _font-family: 'courier new', monospace; + } + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + * 4. Improves appearance and consistency in all browsers. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ + @if $legacy_browser_support { + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ + } +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + * 4. Removes inner spacing in IE 7 without affecting normal text inputs. + * Known issue: inner spacing remains in IE 6. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ + @if $legacy_browser_support { + *overflow: visible; /* 4 */ + } +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + * Known issue: excess padding remains in IE 6. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + @if $legacy_browser_support { + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ + } +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + * 3. Corrects text not wrapping in Firefox 3. + * 4. Corrects alignment displayed oddly in IE 6/7. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ + @if $legacy_browser_support { + white-space: normal; /* 3 */ + *margin-left: -7px; /* 4 */ + } +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_object.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_object.scss new file mode 100755 index 0000000000..22eebd87d0 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_object.scss @@ -0,0 +1,89 @@ +.section-specification { + table { + width: auto; + + th { + text-align: left; + } + } +} + +.method-title { + margin-left: -15px; + margin-bottom: 8px; + transition: margin-left .3s ease-out; + + .section-method.hide & { + margin-left: 0; + } + + code { + font-weight: 400; + font-size: .85em; + } +} + +.method-info { + background: $object-background; + border-bottom: 1px solid $object-border; + margin: 0 -25px; + padding: 20px 25px 0 25px; + transition: height .3s ease-out; + + position: relative; + + .pointy-thing { + background: $content-background; + height: 10px; + border-bottom: 1px solid $object-border; + margin: -20px -25px 16px -25px; + + &:before { + display: inline-block; + content: ""; + + background: $object-background; + border: 1px solid $object-border; + border-bottom: 0; + border-right: 0; + + position: absolute; + left: 21px; + top: 3px; + width: 12px; + height: 12px; + transform: rotate(45deg); + } + } + + .method-subsection { + margin-bottom: 15px; + + .argument-name { + width: 1px; + text-align: right; + + code { + color: #808080; + font-style: italic; + font-weight: 400; + } + } + } +} + +.section-method { + &.hide .method-info { + height: 0 !important; + overflow: hidden; + display: none; + } + + &.hide.animating .method-info { + display: block; + } + + &.animating .method-info { + overflow: hidden; + } +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_print.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_print.scss new file mode 100755 index 0000000000..61bdf99f86 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_print.scss @@ -0,0 +1,42 @@ +@media print { + body { + background: #fff; + padding: 8px; + } + + header { + position: static; + background: #fff; + color: #000; + } + + aside { + display: none; + } + + .container { + max-width: none; + padding: 0; + } + + article { + margin-top: 0; + + #content { + border: 0; + background: #fff; + padding: 15px 0 0 0; + + .title { + margin-top: 0; + padding-top: 0; + } + } + } + + .method-info { + &, & .pointy-thing { + background: #fff; + } + } +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_variables.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_variables.scss new file mode 100755 index 0000000000..38e072d310 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_variables.scss @@ -0,0 +1,12 @@ +$body-font: -apple-system-font, "Helvetica Neue", Helvetica, sans-serif; +$code-font: "Source Code Pro", Monaco, Menlo, Consolas, monospace; + +$body-background: #f2f2f2; +$content-background: #fff; +$content-border: #e9e9e9; +$tint-color: #08c; +$object-background: #f9f9f9; +$object-border: #e9e9e9; + +$mobile-max-width: 650px; +$desktop-min-width: 768px; \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_xcode.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_xcode.scss new file mode 100755 index 0000000000..340b1f6b80 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/_xcode.scss @@ -0,0 +1,29 @@ +.xcode { + header, aside { + display: none; + } + + .container { + padding: 0; + } + + article { + margin-top: 0; + + #content { + border: 0; + margin: 0; + } + } + + .method-info { + &, .section-method.hide & { + max-height: auto; + overflow: visible; + + &.hiding { + display: block; + } + } + } +} diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/scss/style.scss b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/style.scss new file mode 100755 index 0000000000..648a6086ba --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/scss/style.scss @@ -0,0 +1 @@ +@import "variables", "normalize", "layout", "index", "object", "print", "xcode"; diff --git a/submodules/AsyncDisplayKit/docs/appledoc/css/style.css b/submodules/AsyncDisplayKit/docs/appledoc/css/style.css new file mode 100755 index 0000000000..d9d59dd080 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/css/style.css @@ -0,0 +1,2 @@ +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{box-sizing:border-box}.clear{clear:both}.clearfix:before,.container:before,article #content:before,article #content footer:before,.clearfix:after,.container:after,article #content:after,article #content footer:after{clear:both;display:table;content:""}.xcode .hide-in-xcode{display:none}body{font:62.5% -apple-system-font,"Helvetica Neue",Helvetica,sans-serif;background:#f2f2f2}@media (max-width: 650px){body{background-color:#fff}}h1,h2,h3{font-weight:300;color:#808080}h1{font-size:2em;color:#000}h4{font-size:13px;line-height:1.5;margin:21px 0 0 0}a{color:#08c;text-decoration:none}pre,code{font-family:"Source Code Pro",Monaco,Menlo,Consolas,monospace;word-wrap:break-word}pre>code,.method-declaration code{display:inline-block;font-size:.85em;padding:4px 0 4px 10px;border-left:5px solid rgba(0,155,51,0.2)}pre>code:before,.method-declaration code:before{content:"Objective-C";display:block;font:9px/1 -apple-system-font,"Helvetica Neue",Helvetica,sans-serif;color:#009b33;text-transform:uppercase;letter-spacing:2px;padding-bottom:6px}pre>code{font-size:inherit}table,th,td{border:1px solid #e9e9e9}table{width:100%}th,td{padding:7px}th>:first-child,td>:first-child{margin-top:0}th>:last-child,td>:last-child{margin-bottom:0}.container{max-width:980px;padding:0 10px;margin:0 auto}@media (max-width: 650px){.container{padding:0}}header{position:fixed;top:0;left:0;width:100%;z-index:2;background:#414141;color:#fff;font-size:1.1em;line-height:25px;letter-spacing:.05em}header #library-title{float:left}header #developer-home{float:right}header h1{font-size:inherit;font-weight:inherit;margin:0}header p{margin:0}header h1,header a{color:inherit}@media (max-width: 650px){header .container{padding:0 10px}}aside{position:fixed;top:25px;left:0;width:100%;height:25px;z-index:2;font-size:1.1em}aside #header-buttons{background:rgba(255,255,255,0.8);margin:0 1px;padding:0;list-style:none;text-align:right;line-height:32px}aside #header-buttons li{display:inline-block;cursor:pointer;padding:0 10px}aside #header-buttons label,aside #header-buttons select{cursor:inherit}aside #header-buttons #on-this-page{position:relative}aside #header-buttons #on-this-page .chevron{display:inline-block;width:14px;height:4px;position:relative}aside #header-buttons #on-this-page .chevron .chevy{background:#878787;height:2px;position:absolute;width:10px}aside #header-buttons #on-this-page .chevron .chevy.chevron-left{left:0;transform:rotateZ(45deg) scale(0.6)}aside #header-buttons #on-this-page .chevron .chevy.chevron-right{right:0;transform:rotateZ(-45deg) scale(0.6)}aside #header-buttons #on-this-page #jump-to{opacity:0;font-size:16px;position:absolute;top:5px;left:0;width:100%;height:100%}article{margin-top:25px}article #content{background:#fff;border:1px solid #e9e9e9;padding:15px 25px 30px 25px;font-size:1.4em;line-height:1.45;position:relative}@media (max-width: 650px){article #content{padding:15px 10px 20px 10px;border:none}}article #content .navigation-top{position:absolute;top:15px;right:25px}article #content .title{margin:21px 0 0 0;padding:15px 0}article #content p{color:#414141;margin:0 0 15px 0}article #content th p:last-child,article #content td p:last-child{margin-bottom:0}article #content main ul{list-style:none;margin-left:24px;margin-bottom:12px;padding:0}article #content main ul li{position:relative;padding-left:1.3em}article #content main ul li:before{content:"\02022";color:#414141;font-size:1.08em;line-height:1;position:absolute;left:0;padding-top:2px}article #content footer .footer-copyright{margin:70px 25px 10px 0}article #content footer p{font-size:.71em;color:#a0a0a0}.index-container{-webkit-flex-direction:column;flex-direction:column}@media (min-width: 768px){.index-container{display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap}}.index-container .index-column{-webkit-flex:1 1 33%;flex:1 1 33%}.section-specification table{width:auto}.section-specification table th{text-align:left}.method-title{margin-left:-15px;margin-bottom:8px;transition:margin-left .3s ease-out}.section-method.hide .method-title{margin-left:0}.method-title code{font-weight:400;font-size:.85em}.method-info{background:#f9f9f9;border-bottom:1px solid #e9e9e9;margin:0 -25px;padding:20px 25px 0 25px;transition:height .3s ease-out;position:relative}.method-info .pointy-thing{background:#fff;height:10px;border-bottom:1px solid #e9e9e9;margin:-20px -25px 16px -25px}.method-info .pointy-thing:before{display:inline-block;content:"";background:#f9f9f9;border:1px solid #e9e9e9;border-bottom:0;border-right:0;position:absolute;left:21px;top:3px;width:12px;height:12px;-webkit-transform:rotate(45deg);transform:rotate(45deg) }.method-info .method-subsection{margin-bottom:15px}.method-info .method-subsection .argument-name{width:1px;text-align:right}.method-info .method-subsection .argument-name code{color:#808080;font-style:italic;font-weight:400}.section-method.hide .method-info{height:0 !important;overflow:hidden;display:none}.section-method.hide.animating .method-info{display:block}.section-method.animating .method-info{overflow:hidden}@media print{body{background:#fff;padding:8px}header{position:static;background:#fff;color:#000}aside{display:none}.container{max-width:none;padding:0}article{margin-top:0}article #content{border:0;background:#fff;padding:15px 0 0 0}article #content .title{margin-top:0;padding-top:0}.method-info,.method-info .pointy-thing{background:#fff}}.xcode header,.xcode aside{display:none}.xcode .container{padding:0}.xcode article{margin-top:0}.xcode article #content{border:0;margin:0}.xcode .method-info,.section-method.hide .xcode .method-info{max-height:auto;overflow:visible}.xcode .method-info.hiding,.section-method.hide .xcode .method-info.hiding{display:block} + diff --git a/submodules/AsyncDisplayKit/docs/appledoc/hierarchy.html b/submodules/AsyncDisplayKit/docs/appledoc/hierarchy.html new file mode 100755 index 0000000000..f9eb61138c --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/hierarchy.html @@ -0,0 +1,402 @@ + + + + + + Hierarchy + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    Hierarchy

    + + +
    +

    Class Hierarchy

    + + + +
    + + + +
    + +

    Protocol References

    + + + +

    Constant References

    + + + +

    Category References

    + + +
    + + +
    + +
    +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/img/button_bar_background.png b/submodules/AsyncDisplayKit/docs/appledoc/img/button_bar_background.png new file mode 100755 index 0000000000..71d1019bc0 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/appledoc/img/button_bar_background.png differ diff --git a/submodules/AsyncDisplayKit/docs/appledoc/img/disclosure.png b/submodules/AsyncDisplayKit/docs/appledoc/img/disclosure.png new file mode 100755 index 0000000000..4c5cbf4456 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/appledoc/img/disclosure.png differ diff --git a/submodules/AsyncDisplayKit/docs/appledoc/img/disclosure_open.png b/submodules/AsyncDisplayKit/docs/appledoc/img/disclosure_open.png new file mode 100755 index 0000000000..82396fed29 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/appledoc/img/disclosure_open.png differ diff --git a/submodules/AsyncDisplayKit/docs/appledoc/img/library_background.png b/submodules/AsyncDisplayKit/docs/appledoc/img/library_background.png new file mode 100755 index 0000000000..3006248afe Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/appledoc/img/library_background.png differ diff --git a/submodules/AsyncDisplayKit/docs/appledoc/img/title_background.png b/submodules/AsyncDisplayKit/docs/appledoc/img/title_background.png new file mode 100755 index 0000000000..846e4968d0 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/appledoc/img/title_background.png differ diff --git a/submodules/AsyncDisplayKit/docs/appledoc/index.html b/submodules/AsyncDisplayKit/docs/appledoc/index.html new file mode 100755 index 0000000000..27076d4fb6 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/index.html @@ -0,0 +1,340 @@ + + + + + + Reference + + + + + + +
    +
    + +

    + +

    + +

    + AsyncDisplayKit +

    + +
    +
    + + + +
    +
    +
    +
    +

    Reference

    + + + +
    + + + + + + + +
    + +

    Protocol References

    + + + + +

    Constant References

    + + + + +

    Category References

    + + +
    + +
    + +
    + +
    +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/appledoc/js/script.js b/submodules/AsyncDisplayKit/docs/appledoc/js/script.js new file mode 100755 index 0000000000..4074361c44 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledoc/js/script.js @@ -0,0 +1,59 @@ +function $() { + return document.querySelector.apply(document, arguments); +} + +if (navigator.userAgent.indexOf("Xcode") != -1) { + document.documentElement.classList.add("xcode"); +} + +var jumpTo = $("#jump-to"); + +if (jumpTo) { + jumpTo.addEventListener("change", function(e) { + location.hash = this.options[this.selectedIndex].value; + }); +} + +function hashChanged() { + if (/^#\/\/api\//.test(location.hash)) { + var element = document.querySelector("a[name='" + location.hash.substring(1) + "']"); + + if (!element) { + return; + } + + element = element.parentNode; + + element.classList.remove("hide"); + fixScrollPosition(element); + } +} + +function fixScrollPosition(element) { + var scrollTop = element.offsetTop - 150; + document.documentElement.scrollTop = scrollTop; + document.body.scrollTop = scrollTop; +} + +[].forEach.call(document.querySelectorAll(".section-method"), function(element) { + element.classList.add("hide"); + + element.querySelector(".method-title a").addEventListener("click", function(e) { + var info = element.querySelector(".method-info"), + infoContainer = element.querySelector(".method-info-container"); + + element.classList.add("animating"); + info.style.height = (infoContainer.clientHeight + 40) + "px"; + fixScrollPosition(element); + element.classList.toggle("hide"); + if (element.classList.contains("hide")) { + e.preventDefault(); + } + setTimeout(function() { + element.classList.remove("animating"); + }, 300); + }); +}); + +window.addEventListener("hashchange", hashChanged); +hashChanged(); diff --git a/submodules/AsyncDisplayKit/docs/appledocs.md b/submodules/AsyncDisplayKit/docs/appledocs.md new file mode 100755 index 0000000000..541ea17cfc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/appledocs.md @@ -0,0 +1,7 @@ +--- +title: api +layout: appledocs +permalink: /appledocs.html +--- + + diff --git a/submodules/AsyncDisplayKit/docs/index.md b/submodules/AsyncDisplayKit/docs/index.md new file mode 100755 index 0000000000..ad660777f4 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/index.md @@ -0,0 +1,23 @@ +--- +layout: default +title: A UI Framework for Effortless Responsiveness +id: home +--- + +
    +
    +
    Keeps the most complex iOS user interfaces smooth and responsive.
    + Get Started + View on GitHub +
    +
    + +
    +
    +

    Texture is an iOS framework built on top of UIKit that keeps even the most complex user interfaces smooth and responsive. It was originally built to make Facebook's Paper possible, and goes hand-in-hand with pop's physics-based animations — but it's just as powerful with UIKit Dynamics and conventional app designs. More recently, it was used to power Pinterest's app rewrite.

    + +

    As the framework has grown, many features have been added that can save developers tons of time by eliminating common boilerplate style structures common in modern iOS apps. If you've ever dealt with cell reuse bugs, tried to performantly preload data for a page or scroll style interface or even just tried to keep your app from dropping too many frames you can benefit from integrating Texture.

    + +

    To learn more, check out our docs!

    +
    +
    diff --git a/submodules/AsyncDisplayKit/docs/showcase.md b/submodules/AsyncDisplayKit/docs/showcase.md new file mode 100755 index 0000000000..57f9ca127f --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/showcase.md @@ -0,0 +1,290 @@ +--- +title: Showcase +layout: default +permalink: /showcase.html +--- + +
    + +
    +

    Who's using Texture?

    +

    If you're curious to see what can be accomplished with Texture, check out these apps.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + Facebook +
    + Introducing AsyncDisplayKit: For smooth and responsive apps on iOS +
    + +
    + Pinterest +
    + Re-architecting Pinterest's iOS app +
    + +
    + Buffer +
    + Smooth Scrolling in Buffer for iOS: How (and Why) We Implemented AsyncDisplayKit

    + Texture: What, Why and How +
    + +
    + Auxy +
    + 2016 Apple Design Award Winner +
    + +
    + NYT +
    + +
    + NFL +
    + +
    + Yahoo +
    + +
    + Kwaver Music +
    + +
    + This is Money +
    + +
    + JSwipe +
    + +
    + Peloton Cycle +
    + +
    + 即刻 - 不错过你惦记的每一件小事 +
    + +
    + Tripstr +
    + +
    + Fyuse +
    + +
    + ClassDojo +
    + Powering Class Story With Texture +
    + +
    + GitBucket +
    + +
    + Roposo +
    + +
    + Mishu +
    + +
    + InstaBuy +
    + +
    + Eniro +
    + +
    + Kayako +
    + +
    + Yep +
    + +
    + HakkerJobs +
    + +
    + TraceMe +
    + +
    + Pairs +
    + +
    + Sorted: Master Your Day +
    + +
    + Vingle +
    + Improvement feed performance with Texture +
    + +
    + Blendle +
    + +
    + MensXP +
    + +
    + iDiva +
    + +
    + Waplog +
    + +
    + Apollo for Reddit +
    + +
    + Wishpoke +
    + +
    + Bluebird +
    + +
    + +
    +

    If you built an app using Texture, we'd love to have your app in this showcase!

    +

    If you would like to have your app added or do not want your app featured on this page, please email textureframework@gmail.com.

    + +
    + +
    diff --git a/submodules/AsyncDisplayKit/docs/slack.md b/submodules/AsyncDisplayKit/docs/slack.md new file mode 100755 index 0000000000..121dc4d51e --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/slack.md @@ -0,0 +1,10 @@ +--- +title: slack +layout: slack +permalink: /slack.html +--- + + + + +If the auto-invite link above does not work for you, please email textureframework@gmail.com for an invite. diff --git a/submodules/AsyncDisplayKit/docs/static/images/1-shuffle-crop.png b/submodules/AsyncDisplayKit/docs/static/images/1-shuffle-crop.png new file mode 100755 index 0000000000..d1e0a83b0c Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/1-shuffle-crop.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/Texturelogo.png b/submodules/AsyncDisplayKit/docs/static/images/Texturelogo.png new file mode 100644 index 0000000000..cf669bef16 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/Texturelogo.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/Texturelogo@2x.png b/submodules/AsyncDisplayKit/docs/static/images/Texturelogo@2x.png new file mode 100644 index 0000000000..3ad259d962 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/Texturelogo@2x.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/basicMap.png b/submodules/AsyncDisplayKit/docs/static/images/basicMap.png new file mode 100755 index 0000000000..b387adc082 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/basicMap.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/catsButt.png b/submodules/AsyncDisplayKit/docs/static/images/catsButt.png new file mode 100755 index 0000000000..0d442faf4d Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/catsButt.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/catsFace.png b/submodules/AsyncDisplayKit/docs/static/images/catsFace.png new file mode 100755 index 0000000000..4be93671f9 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/catsFace.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/catsMiddle.png b/submodules/AsyncDisplayKit/docs/static/images/catsMiddle.png new file mode 100755 index 0000000000..f6526eebef Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/catsMiddle.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/clip-corners.png b/submodules/AsyncDisplayKit/docs/static/images/clip-corners.png new file mode 100755 index 0000000000..cc43a0a974 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/clip-corners.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/component-controllers.png b/submodules/AsyncDisplayKit/docs/static/images/component-controllers.png new file mode 100755 index 0000000000..e882cd1f7e Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/component-controllers.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-flowchart-v2.png b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-flowchart-v2.png new file mode 100755 index 0000000000..71b50848a1 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-flowchart-v2.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-movement.png b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-movement.png new file mode 100755 index 0000000000..1749d6683c Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-movement.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-overlap.png b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-overlap.png new file mode 100755 index 0000000000..b69b3825e6 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-overlap.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-scrolling.png b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-scrolling.png new file mode 100755 index 0000000000..ef29151707 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/corner-rounding-scrolling.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASCollectionView.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASCollectionView.png new file mode 100755 index 0000000000..3aaff368e5 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASCollectionView.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png new file mode 100644 index 0000000000..2a3ac02101 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKLayoutTransition.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKLayoutTransition.gif new file mode 100755 index 0000000000..c75467164d Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKLayoutTransition.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKTube.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKTube.gif new file mode 100755 index 0000000000..956a5c0440 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKTube.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKgram.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKgram.png new file mode 100755 index 0000000000..358dae3805 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASDKgram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASMapNode.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASMapNode.png new file mode 100755 index 0000000000..aa605349fe Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASMapNode.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASTableViewStressTest.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASTableViewStressTest.png new file mode 100755 index 0000000000..cd7aae2d9d Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASTableViewStressTest.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASViewController.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASViewController.png new file mode 100755 index 0000000000..545f3c8d63 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/ASViewController.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/AsyncDisplayKitOverview.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/AsyncDisplayKitOverview.png new file mode 100755 index 0000000000..4941292c4b Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/AsyncDisplayKitOverview.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/BackgroundPropertySetting.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/BackgroundPropertySetting.gif new file mode 100755 index 0000000000..e2655ef235 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/BackgroundPropertySetting.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CatDealsCollectionView.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CatDealsCollectionView.png new file mode 100755 index 0000000000..72f9179e94 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CatDealsCollectionView.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CollectionViewWithViewControllerCells.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CollectionViewWithViewControllerCells.png new file mode 100755 index 0000000000..078b7b945f Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CollectionViewWithViewControllerCells.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CustomCollectionView.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CustomCollectionView.gif new file mode 100755 index 0000000000..8d8a2aed1e Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/CustomCollectionView.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/EditableText.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/EditableText.png new file mode 100755 index 0000000000..1aa8d6db9b Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/EditableText.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/HorizontalwithinVerticalScrolling.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/HorizontalwithinVerticalScrolling.gif new file mode 100755 index 0000000000..fe722a308d Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/HorizontalwithinVerticalScrolling.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Kittens.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Kittens.png new file mode 100755 index 0000000000..91d9bf36fa Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Kittens.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/LayoutSpecPlayground.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/LayoutSpecPlayground.png new file mode 100755 index 0000000000..9ed4db7c55 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/LayoutSpecPlayground.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Multiplex.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Multiplex.gif new file mode 100755 index 0000000000..fcb98cad62 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Multiplex.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/PagerNode.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/PagerNode.gif new file mode 100755 index 0000000000..8c13a0faea Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/PagerNode.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/SocialAppLayout.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/SocialAppLayout.png new file mode 100755 index 0000000000..33d6672102 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/SocialAppLayout.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Swift.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Swift.png new file mode 100755 index 0000000000..b099a17229 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Swift.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/SynchronousConcurrency.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/SynchronousConcurrency.png new file mode 100755 index 0000000000..e0eac3788e Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/SynchronousConcurrency.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/VerticalWithinHorizontalScrolling.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/VerticalWithinHorizontalScrolling.gif new file mode 100755 index 0000000000..c50474b19b Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/VerticalWithinHorizontalScrolling.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/VideoTableView.png b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/VideoTableView.png new file mode 100755 index 0000000000..ddeefdc773 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/VideoTableView.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Videos.gif b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Videos.gif new file mode 100755 index 0000000000..5e5c5f2ca9 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/example-app-screenshots/Videos.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-144x144.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-144x144.png new file mode 100755 index 0000000000..68d4423f04 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-144x144.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-192x192.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-192x192.png new file mode 100755 index 0000000000..c7e107a2e3 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-192x192.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-36x36.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-36x36.png new file mode 100755 index 0000000000..b0da36b5b0 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-36x36.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-48x48.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-48x48.png new file mode 100755 index 0000000000..ed53510513 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-48x48.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-72x72.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-72x72.png new file mode 100755 index 0000000000..97a2a20792 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-72x72.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-96x96.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-96x96.png new file mode 100755 index 0000000000..1ea7ba4148 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/android-icon-96x96.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-114x114.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-114x114.png new file mode 100755 index 0000000000..c4f63ca5cd Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-114x114.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-120x120.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-120x120.png new file mode 100755 index 0000000000..5e1cf1afe5 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-120x120.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-144x144.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-144x144.png new file mode 100755 index 0000000000..68d4423f04 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-144x144.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-152x152.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-152x152.png new file mode 100755 index 0000000000..d9a225aaa4 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-152x152.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-180x180.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-180x180.png new file mode 100755 index 0000000000..6c40cd7b1c Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-180x180.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-57x57.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-57x57.png new file mode 100755 index 0000000000..c9d74b6b30 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-57x57.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-60x60.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-60x60.png new file mode 100755 index 0000000000..7b440c6049 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-60x60.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-72x72.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-72x72.png new file mode 100755 index 0000000000..97a2a20792 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-72x72.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-76x76.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-76x76.png new file mode 100755 index 0000000000..0d5dd5f9f2 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-76x76.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-precomposed.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-precomposed.png new file mode 100755 index 0000000000..43549d71c3 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon-precomposed.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon.png new file mode 100755 index 0000000000..43549d71c3 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/apple-icon.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/browserconfig.xml b/submodules/AsyncDisplayKit/docs/static/images/favicon/browserconfig.xml new file mode 100755 index 0000000000..c554148223 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/static/images/favicon/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-16x16.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-16x16.png new file mode 100755 index 0000000000..4dc8bcf4ae Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-16x16.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-32x32.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-32x32.png new file mode 100755 index 0000000000..3ebcbef1d8 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-32x32.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-96x96.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-96x96.png new file mode 100755 index 0000000000..1ea7ba4148 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon-96x96.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon.ico b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon.ico new file mode 100755 index 0000000000..ecafdea634 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/favicon.ico differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/manifest.json b/submodules/AsyncDisplayKit/docs/static/images/favicon/manifest.json new file mode 100755 index 0000000000..013d4a6a53 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/static/images/favicon/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-144x144.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-144x144.png new file mode 100755 index 0000000000..68d4423f04 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-144x144.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-150x150.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-150x150.png new file mode 100755 index 0000000000..8c1fc25b26 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-150x150.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-310x310.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-310x310.png new file mode 100755 index 0000000000..8d43edcdc8 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-310x310.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-70x70.png b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-70x70.png new file mode 100755 index 0000000000..498e41ba17 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/favicon/ms-icon-70x70.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/flexbasis.png b/submodules/AsyncDisplayKit/docs/static/images/flexbasis.png new file mode 100755 index 0000000000..28a9ad0b8b Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/flexbasis.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges-screenfuls.png b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges-screenfuls.png new file mode 100755 index 0000000000..07b08f25c6 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges-screenfuls.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges-with-names.png b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges-with-names.png new file mode 100755 index 0000000000..b2ef62a767 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges-with-names.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges.png b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges.png new file mode 100755 index 0000000000..de884a1390 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading-ranges.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading.png b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading.png new file mode 100755 index 0000000000..a24f31c8ab Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/intelligent-preloading.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/kittenLink.png b/submodules/AsyncDisplayKit/docs/static/images/kittenLink.png new file mode 100755 index 0000000000..1c46390e5e Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/kittenLink.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-api-sizing-1.png b/submodules/AsyncDisplayKit/docs/static/images/layout-api-sizing-1.png new file mode 100755 index 0000000000..dd004c3a42 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-api-sizing-1.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-api-sizing-2.png b/submodules/AsyncDisplayKit/docs/static/images/layout-api-sizing-2.png new file mode 100755 index 0000000000..20814d8425 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-api-sizing-2.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-example-1.png b/submodules/AsyncDisplayKit/docs/static/images/layout-example-1.png new file mode 100755 index 0000000000..aac85798e5 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-example-1.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-example-2.png b/submodules/AsyncDisplayKit/docs/static/images/layout-example-2.png new file mode 100755 index 0000000000..673c16721b Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-example-2.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-example-3.png b/submodules/AsyncDisplayKit/docs/static/images/layout-example-3.png new file mode 100755 index 0000000000..72f9179e94 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-example-3.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay-diagram.png new file mode 100755 index 0000000000..dbf95847ed Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay-photo.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay-photo.png new file mode 100755 index 0000000000..2465aaa3cb Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay-photo.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay.png new file mode 100755 index 0000000000..020685ae10 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-inset-text-overlay.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png new file mode 100755 index 0000000000..22a1652b3f Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png new file mode 100755 index 0000000000..7d7bef7e13 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay.png new file mode 100755 index 0000000000..46669c860a Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-photo-with-outset-icon-overlay.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-header-with-left-right-justified-text-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-header-with-left-right-justified-text-diagram.png new file mode 100755 index 0000000000..c81e6f5122 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-header-with-left-right-justified-text-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-header-with-left-right-justified-text.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-header-with-left-right-justified-text.png new file mode 100755 index 0000000000..1296620146 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-header-with-left-right-justified-text.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-inset-text-cell-closeup.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-inset-text-cell-closeup.png new file mode 100755 index 0000000000..1b5b0d55a3 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-inset-text-cell-closeup.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-inset-text-cell.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-inset-text-cell.png new file mode 100755 index 0000000000..b161af9eca Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-simple-inset-text-cell.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-top-bottom-separator-line-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-top-bottom-separator-line-diagram.png new file mode 100755 index 0000000000..eac57973ce Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-top-bottom-separator-line-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-examples-top-bottom-separator-line.png b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-top-bottom-separator-line.png new file mode 100755 index 0000000000..38f1fd9564 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-examples-top-bottom-separator-line.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-spec-relationship-1.png b/submodules/AsyncDisplayKit/docs/static/images/layout-spec-relationship-1.png new file mode 100755 index 0000000000..a6ca2945c8 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-spec-relationship-1.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout-spec-relationship-2.png b/submodules/AsyncDisplayKit/docs/static/images/layout-spec-relationship-2.png new file mode 100755 index 0000000000..a21f052429 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout-spec-relationship-2.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layout2-api-sizing.png b/submodules/AsyncDisplayKit/docs/static/images/layout2-api-sizing.png new file mode 100755 index 0000000000..0a7f054365 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layout2-api-sizing.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-examples/layout-example-inset-overlay.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-examples/layout-example-inset-overlay.png new file mode 100755 index 0000000000..f10f8f0fe4 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-examples/layout-example-inset-overlay.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASBackgroundLayoutSpec-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASBackgroundLayoutSpec-diagram.png new file mode 100755 index 0000000000..5b365c0211 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASBackgroundLayoutSpec-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASCenterLayoutSpec-diagram-text.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASCenterLayoutSpec-diagram-text.png new file mode 100755 index 0000000000..7559e73d08 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASCenterLayoutSpec-diagram-text.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASCenterLayoutSpec-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASCenterLayoutSpec-diagram.png new file mode 100755 index 0000000000..040b5ce86e Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASCenterLayoutSpec-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-diagram-text.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-diagram-text.png new file mode 100755 index 0000000000..f4e3810af4 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-diagram-text.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-diagram.png new file mode 100755 index 0000000000..6ee3642fb3 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-example-complex.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-example-complex.png new file mode 100755 index 0000000000..ca08d0c9ec Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASInsetLayoutSpec-example-complex.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASOverlayLayouSpec-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASOverlayLayouSpec-diagram.png new file mode 100755 index 0000000000..7bba2cf067 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASOverlayLayouSpec-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASOverlayLayoutSpec-example-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASOverlayLayoutSpec-example-diagram.png new file mode 100755 index 0000000000..deda779f3f Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASOverlayLayoutSpec-example-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASRatioLayoutSpec-diagram.png b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASRatioLayoutSpec-diagram.png new file mode 100755 index 0000000000..eac1fd3b1f Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutSpec-types/ASRatioLayoutSpec-diagram.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/layoutable-types.png b/submodules/AsyncDisplayKit/docs/static/images/layoutable-types.png new file mode 100755 index 0000000000..6ac3e9d11d Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/layoutable-types.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/liveMap.gif b/submodules/AsyncDisplayKit/docs/static/images/liveMap.gif new file mode 100755 index 0000000000..633d1fb344 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/liveMap.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/logo.png b/submodules/AsyncDisplayKit/docs/static/images/logo.png new file mode 100644 index 0000000000..d1e84d62c7 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/logo.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/logo.svg b/submodules/AsyncDisplayKit/docs/static/images/logo.svg new file mode 100644 index 0000000000..bc30fe6cdc --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/static/images/logo.svg @@ -0,0 +1,24 @@ + + + + Texture Logo + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/static/images/mapWithAnnotation.png b/submodules/AsyncDisplayKit/docs/static/images/mapWithAnnotation.png new file mode 100755 index 0000000000..e6316293d1 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/mapWithAnnotation.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/node-hierarchy.png b/submodules/AsyncDisplayKit/docs/static/images/node-hierarchy.png new file mode 100755 index 0000000000..8f0c021b62 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/node-hierarchy.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/node-view-layer.png b/submodules/AsyncDisplayKit/docs/static/images/node-view-layer.png new file mode 100755 index 0000000000..544294af8f Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/node-view-layer.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/overlay-vs-inset-spec.png b/submodules/AsyncDisplayKit/docs/static/images/overlay-vs-inset-spec.png new file mode 100755 index 0000000000..2da1be2d80 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/overlay-vs-inset-spec.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/satelliteMap.png b/submodules/AsyncDisplayKit/docs/static/images/satelliteMap.png new file mode 100755 index 0000000000..702f12b248 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/satelliteMap.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/synchronous-concurrency.jpg b/submodules/AsyncDisplayKit/docs/static/images/synchronous-concurrency.jpg new file mode 100644 index 0000000000..b49ea5d4c9 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/synchronous-concurrency.jpg differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/textNodeTruncation.png b/submodules/AsyncDisplayKit/docs/static/images/textNodeTruncation.png new file mode 100755 index 0000000000..a5125a8143 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/textNodeTruncation.png differ diff --git a/submodules/AsyncDisplayKit/docs/static/images/video.gif b/submodules/AsyncDisplayKit/docs/static/images/video.gif new file mode 100755 index 0000000000..dd9bab6a78 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/images/video.gif differ diff --git a/submodules/AsyncDisplayKit/docs/static/linkify.js b/submodules/AsyncDisplayKit/docs/static/linkify.js new file mode 100755 index 0000000000..b6adfbd5f9 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/static/linkify.js @@ -0,0 +1,22 @@ +[].slice.apply( + document.querySelectorAll( + 'article h2, article h3, article h4' + ) +).forEach(function(header) { + var slug = header.innerText + .toLowerCase() + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + + var hashref = document.createElement('a'); + hashref.id = slug; + hashref.className = 'hashref'; + header.appendChild(hashref); + + var hash = document.createElement('a'); + hash.className = 'hash'; + hash.href = '#' + slug; + hash.innerText = '#'; + header.appendChild(hash); +}); diff --git a/submodules/AsyncDisplayKit/docs/static/talks/10_3_2016_ASCollectionNode_Sequence_Diagrams.pdf b/submodules/AsyncDisplayKit/docs/static/talks/10_3_2016_ASCollectionNode_Sequence_Diagrams.pdf new file mode 100755 index 0000000000..ce202e60be Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/talks/10_3_2016_ASCollectionNode_Sequence_Diagrams.pdf differ diff --git a/submodules/AsyncDisplayKit/docs/static/talks/ASCollectionView.pdf b/submodules/AsyncDisplayKit/docs/static/talks/ASCollectionView.pdf new file mode 100755 index 0000000000..43d834f1d9 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/talks/ASCollectionView.pdf differ diff --git a/submodules/AsyncDisplayKit/docs/static/talks/UICollectionView.pdf b/submodules/AsyncDisplayKit/docs/static/talks/UICollectionView.pdf new file mode 100755 index 0000000000..da3691d049 Binary files /dev/null and b/submodules/AsyncDisplayKit/docs/static/talks/UICollectionView.pdf differ diff --git a/submodules/AsyncDisplayKit/docs/static/toggle.js b/submodules/AsyncDisplayKit/docs/static/toggle.js new file mode 100755 index 0000000000..65b9b5e2bd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/static/toggle.js @@ -0,0 +1,27 @@ +document.addEventListener("DOMContentLoaded", function() { + var swiftButtons = document.getElementsByClassName("swiftButton"); + var objectiveCButons = document.getElementsByClassName("objcButton"); + var objcCodes = document.getElementsByClassName("objcCode"); + var swiftCodes = document.getElementsByClassName("swiftCode"); + + var totalCodeSections = swiftButtons.length; + for(var i = 0; i < totalCodeSections; i++) { + swiftButtons[i].onclick = function () { + for (var i = 0; i < totalCodeSections; i++) { + swiftCodes[i].classList.remove("hidden"); + objcCodes[i].classList.add("hidden"); + objectiveCButons[i].classList.remove("active"); + swiftButtons[i].classList.add("active"); + }; + } + + objectiveCButons[i].onclick = function () { + for (var i = 0; i < totalCodeSections; i++) { + swiftCodes[i].classList.add("hidden"); + objcCodes[i].classList.remove("hidden"); + objectiveCButons[i].classList.add("active"); + swiftButtons[i].classList.remove("active"); + }; + } + } +}); \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/docs/stylesheets/main.scss b/submodules/AsyncDisplayKit/docs/stylesheets/main.scss new file mode 100755 index 0000000000..ee270fa770 --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/stylesheets/main.scss @@ -0,0 +1,338 @@ +--- +--- + +@charset "utf-8"; + +* { + padding: 0; + margin: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-text-size-adjust: 100%; +} + +html { + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + background-color: #FFFFFF; +} + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 400; + color: #444; + margin: 0; + line-height: 22px; +} + +h1, h2, h3, h4 { + font-weight: normal; + margin: 1.3em 0 0.2em; + line-height: 1.4em; + position: relative; +} + +td { + vertical-align: top; +} + +h1:first-child, h2:first-child, h3:first-child, h4:first-child { + margin-top: 0; +} + +h1 { + font-size: 2em; + font-weight: bold; +} + +h2 { + font-size: 1.5em; +} + +h3 { + font-size: 1.3em; +} + +h4 { + font-size: 1.1em; +} + +p, ul, ol { + margin: 0 0 1em 0; +} + +a { + color: #0484f2; + text-decoration: none; +} + +.btn { + padding: 10px 20px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + background: #0f70dd; + color: #fff; + display: inline-block; + text-decoration: none; + font-weight: bold; +} + +.container { + width: 90%; + margin: 0 auto; + clear: both; + + &:after { content: ""; display: table; clear: both; } +} + +header { + padding: 30px 0 0 0; + background: #f8f8f8; + + h1 { + position: relative; + top: -5px; + } + + nav { + font-size: 18px; + + ul { + list-style: none; + + li { + float: left; + margin-right: 20px; + + a { + color: #0484f2; + text-decoration: none; + } + } + } + } +} + +#logo { + margin: 0; +} + +.hero { + background: #0f70dd; + margin-bottom: 30px; + padding: 30px 0; + + .hero-title { + font-size: 45px; + line-height: 110%; + color: #fff; + margin-bottom: 20px; + } + + .btn { + background: #fff; + color: #0f70dd; + border: 2px solid #fff; + margin-right: 10px; + } + + .btn.btn-outlined { + background: none; + border: 2px solid #fff; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + color: #fff; + } +} + +.container-margin { + margin-top: 30px; +} + +.container .sidebar { + font-size: 14px; + + ul { + list-style: none; + + li { + list-style: none; + } + } + + a.active { + font-weight: bold; + } +} + +.container tr > td { + padding-bottom: 30px; +} + +.container tr td img { + display: inline-block; + margin-bottom: 5px; +} + +.container tr td b { + padding-bottom: 5px; + display: inline-block; +} + +.roundrect { + border: 1px solid #f4f4f4; + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + border-radius: 20px; +} + +.container .content ul, .container .content ol { + margin-left: 20px; + margin-bottom: 30px; +} + +.content p img { + display: block; + margin: 1em auto; + max-width: 100%; +} + +.edit-page-link { + font-size: 14px; +} + +@import "code"; + +.language-toggle { + border-bottom: 2px #0484f2 solid; + background: white; + display: block; + box-sizing: border-box; + font-size: 125%; + -webkit-font-smoothing: antialiased; + line-height: 1.5; + padding-right: 10px; + + a { + cursor: pointer; + display: block; + float: right; + padding-left: 1em; + + color: #a3a39e !important; + text-decoration: none; + transition: color 0.1s linear; + + &.active { + color: #222220 !important; + } + } + + &:after { + content: ""; + display: table; + clear: both; + } +} + +.highlight-group { + font-family: 'Inconsolata' !important; + margin: 0; + margin-top: 20px; + margin-bottom: 20px; + background: #f8f7f5; +} + +.hidden { + display: none; +} +.code { + padding: 15px; + overflow: auto; +} +pre { + font-family: 'Inconsolata' !important; + font-weight: 500; + color: #333333; +} + +.note { + padding: 10px; + border-radius: 3px; + background-color: #EFF7FF; + border: 1px solid #CCDDFF; + margin-bottom: 20px; + + p:before { + content: "NOTE:"; + font-weight: bold; + } + + p { + margin-bottom: 0px; + } +} + +.note-important { + padding: 10px; + border-radius: 3px; + background-color: #FFCCCC; + border: 1px solid #FF8899; + margin-bottom: 20px; + + p:before { + content: "IMPORTANT:"; + font-weight: bold; + } + + p { + margin-bottom: 0px; + } +} + +.right { + float: right; +} + +footer { + padding: 20px 0; + + p { + text-align: center; + } +} + +/** Banner */ +.texture-banner { + padding: 20px 0 20px 0; + width: 100%; + background-color: #0e1e28; + box-shadow: inset 0 -2px 20px 0 rgba(0,0,0,0.5); + text-align: center; + + .announcement { + font-stretch: normal; + font-size: 16px; + line-height: 1.75; + letter-spacing: 0.1px; + text-align: center; + color: #d7e2e9; + font-weight: bold; + margin: 0; + } + + .learn-more-link { + font-size: 12px; + font-weight: bold; + font-style: normal; + font-stretch: normal; + line-height: 1.17; + letter-spacing: 0.5px; + color: #0484f2; + margin-left: 10px; + } +} diff --git a/submodules/AsyncDisplayKit/docs/stylesheets/media.css b/submodules/AsyncDisplayKit/docs/stylesheets/media.css new file mode 100755 index 0000000000..37235a7ceb --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/stylesheets/media.css @@ -0,0 +1,50 @@ +/* Media queries */ +@media (min-width: 600px) { + header { + display: block; + } + + header nav { + line-height: 55px; + } + + header ul { + } + + header nav ul { + display: inline-block; + float: right; + margin: 0 auto; + } + + #logo { + float: left; + } + + .container .sidebar { + width: 19%; + float: left; + } + + .container .content { + width: 75%; + float: right; + } + +} + +@media (min-width: 950px) { + .container { + width: 900px; + } + + .container .sidebar { + width: 19%; + float: left; + } + + .container .content { + width: 75%; + float: right; + } +} diff --git a/submodules/AsyncDisplayKit/docs/stylesheets/pygments.css b/submodules/AsyncDisplayKit/docs/stylesheets/pygments.css new file mode 100755 index 0000000000..91f27c87dd --- /dev/null +++ b/submodules/AsyncDisplayKit/docs/stylesheets/pygments.css @@ -0,0 +1,61 @@ +.hll { background-color: #ffffcc } +.c { color: #999988; font-style: italic } /* Comment */ +.err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.k { color: #000000; font-weight: bold } /* Keyword */ +.o { color: #000000; } /* Operator */ +.cm { color: #666666; font-weight: bold; font-style: italic } /* Comment.Multiline */ +.cp { color: #666666; font-weight: bold; font-style: italic } /* Comment.Preproc */ +.c1 { color: #666666; font-weight: bold; font-style: italic } /* Comment.Single */ +.cs { color: #666666; font-weight: bold; font-style: italic } /* Comment.Special */ +.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.ge { color: #000000; font-style: italic } /* Generic.Emph */ +.gr { color: #aa0000 } /* Generic.Error */ +.gh { color: #999999 } /* Generic.Heading */ +.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.go { color: #888888 } /* Generic.Output */ +.gp { color: #555555 } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #aaaaaa } /* Generic.Subheading */ +.gt { color: #aa0000 } /* Generic.Traceback */ +.kc { color: #000000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ +.kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.m { color: #009999 } /* Literal.Number */ +.s { color: #d01040 } /* Literal.String */ +.na { color: #008080 } /* Name.Attribute */ +.nb { } /* Name.Builtin */ +.nc { color: #445588; font-weight: bold } /* Name.Class */ +.no { color: #008080 } /* Name.Constant */ +.nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ +.ni { color: #800080 } /* Name.Entity */ +.ne { color: #990000; font-weight: bold } /* Name.Exception */ +.nf { color: #990000; font-weight: bold } /* Name.Function */ +.nl { color: #990000; font-weight: bold } /* Name.Label */ +.nn { color: #555555 } /* Name.Namespace */ +.nt { color: #000080 } /* Name.Tag */ +.nv { color: #008080 } /* Name.Variable */ +.ow { color: #000000; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #009999 } /* Literal.Number.Float */ +.mh { color: #009999 } /* Literal.Number.Hex */ +.mi { color: #009999 } /* Literal.Number.Integer */ +.mo { color: #009999 } /* Literal.Number.Oct */ +.sb { color: #d01040 } /* Literal.String.Backtick */ +.sc { color: #d01040 } /* Literal.String.Char */ +.sd { color: #d01040 } /* Literal.String.Doc */ +.s2 { color: #d01040 } /* Literal.String.Double */ +.se { color: #d01040 } /* Literal.String.Escape */ +.sh { color: #d01040 } /* Literal.String.Heredoc */ +.si { color: #d01040 } /* Literal.String.Interpol */ +.sx { color: #d01040 } /* Literal.String.Other */ +.sr { color: #009926 } /* Literal.String.Regex */ +.s1 { color: #d01040 } /* Literal.String.Single */ +.ss { color: #990073 } /* Literal.String.Symbol */ +.bp { color: #999999 } /* Name.Builtin.Pseudo */ +.vc { color: #008080 } /* Name.Variable.Class */ +.vg { color: #008080 } /* Name.Variable.Global */ +.vi { color: #008080 } /* Name.Variable.Instance */ +.il { color: #009999 } /* Literal.Number.Integer.Long */ diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Podfile b/submodules/AsyncDisplayKit/examples/ASCollectionView/Podfile new file mode 100644 index 0000000000..08d1b7add6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..967d1f7cd6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,402 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FDEC911BF31EE700CEB123 /* ItemNode.m */; }; + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; }; + AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + AF3289A5220868C808CB570A /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C8A73B07F48A6BF6CCC23B59 /* libPods-Sample.a */; }; + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 25FDEC901BF31EE700CEB123 /* ItemNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemNode.h; sourceTree = ""; }; + 25FDEC911BF31EE700CEB123 /* ItemNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemNode.m; sourceTree = ""; }; + 3AE14FE81840274F92ABA227 /* 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 = ""; }; + 4BB21270A5CD115520C634A3 /* 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 = ""; }; + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SupplementaryNode.h; sourceTree = ""; }; + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SupplementaryNode.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; + AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AC3C4A651A11F47200143C57 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AC3C4A661A11F47200143C57 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AC3C4A681A11F47200143C57 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + C8A73B07F48A6BF6CCC23B59 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F02BAF78E68BC56FD8C161B7 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PresentingViewController.h; sourceTree = ""; }; + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PresentingViewController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC3C4A5B1A11F47200143C57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AF3289A5220868C808CB570A /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 90A2B9C5397C46134C8A793B /* Pods */ = { + isa = PBXGroup; + children = ( + 3AE14FE81840274F92ABA227 /* Pods-Sample.debug.xcconfig */, + 4BB21270A5CD115520C634A3 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AC3C4A551A11F47200143C57 = { + isa = PBXGroup; + children = ( + AC3C4A601A11F47200143C57 /* Sample */, + AC3C4A5F1A11F47200143C57 /* Products */, + 90A2B9C5397C46134C8A793B /* Pods */, + D6E38FF0CB18E3F55CF06437 /* Frameworks */, + ); + sourceTree = ""; + }; + AC3C4A5F1A11F47200143C57 /* Products */ = { + isa = PBXGroup; + children = ( + AC3C4A5E1A11F47200143C57 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + AC3C4A601A11F47200143C57 /* Sample */ = { + isa = PBXGroup; + children = ( + AC3C4A651A11F47200143C57 /* AppDelegate.h */, + AC3C4A661A11F47200143C57 /* AppDelegate.m */, + AC3C4A681A11F47200143C57 /* ViewController.h */, + AC3C4A691A11F47200143C57 /* ViewController.m */, + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */, + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */, + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, + AC3C4A611A11F47200143C57 /* Supporting Files */, + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */, + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */, + 25FDEC901BF31EE700CEB123 /* ItemNode.h */, + 25FDEC911BF31EE700CEB123 /* ItemNode.m */, + ); + indentWidth = 2; + path = Sample; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + AC3C4A611A11F47200143C57 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + AC3C4A621A11F47200143C57 /* Info.plist */, + AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D6E38FF0CB18E3F55CF06437 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F02BAF78E68BC56FD8C161B7 /* libPods.a */, + C8A73B07F48A6BF6CCC23B59 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AC3C4A5D1A11F47200143C57 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + F868CFBB21824CC9521B6588 /* [CP] Check Pods Manifest.lock */, + AC3C4A5A1A11F47200143C57 /* Sources */, + AC3C4A5B1A11F47200143C57 /* Frameworks */, + AC3C4A5C1A11F47200143C57 /* Resources */, + A6902C454C7661D0D277AC62 /* [CP] Copy Pods Resources */, + 79E97C651F7D1ECDBE6B4793 /* [CP] Embed Pods Frameworks */, + 8C4782EECEE7F1205007D6DB /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = AC3C4A5E1A11F47200143C57 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AC3C4A561A11F47200143C57 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + AC3C4A5D1A11F47200143C57 = { + CreatedOnToolsVersion = 6.1; + }; + }; + }; + buildConfigurationList = AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AC3C4A551A11F47200143C57; + productRefGroup = AC3C4A5F1A11F47200143C57 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AC3C4A5D1A11F47200143C57 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AC3C4A5C1A11F47200143C57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 79E97C651F7D1ECDBE6B4793 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8C4782EECEE7F1205007D6DB /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A6902C454C7661D0D277AC62 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + F868CFBB21824CC9521B6588 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AC3C4A5A1A11F47200143C57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */, + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */, + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */, + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, + AC3C4A641A11F47200143C57 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AC3C4A7F1A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A801A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AC3C4A821A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3AE14FE81840274F92ABA227 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + AC3C4A831A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4BB21270A5CD115520C634A3 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A7F1A11F47200143C57 /* Debug */, + AC3C4A801A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A821A11F47200143C57 /* Debug */, + AC3C4A831A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AC3C4A561A11F47200143C57 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..f49edc75d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/AppDelegate.h new file mode 100644 index 0000000000..db20870a31 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define SIMULATE_WEB_RESPONSE 0 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/AppDelegate.m new file mode 100644 index 0000000000..ad36676ae0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/AppDelegate.m @@ -0,0 +1,49 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "PresentingViewController.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 = [[UINavigationController alloc] init]; + + [self pushNewViewControllerAnimated:NO]; + + [self.window makeKeyAndVisible]; + + return YES; +} + +- (void)pushNewViewControllerAnimated:(BOOL)animated +{ + UINavigationController *navController = (UINavigationController *)self.window.rootViewController; + +#if SIMULATE_WEB_RESPONSE + UIViewController *viewController = [[PresentingViewController alloc] init]; +#else + UIViewController *viewController = [[ViewController alloc] init]; + viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Another Copy" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; +#endif + + [navController pushViewController:viewController animated:animated]; +} + +- (void)pushNewViewController +{ + [self pushNewViewControllerAnimated:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..f0fce54771 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,39 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "scale" : "1x", + "orientation" : "portrait" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "orientation" : "portrait" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png new file mode 100644 index 0000000000..1547a98454 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Info.plist new file mode 100644 index 0000000000..eeb71a8d35 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launchboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ItemNode.h b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ItemNode.h new file mode 100644 index 0000000000..73eb2629e6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ItemNode.h @@ -0,0 +1,16 @@ +// +// ItemNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ItemNode : ASTextCellNode + +- (instancetype)initWithString:(NSString *)string; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ItemNode.m b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ItemNode.m new file mode 100644 index 0000000000..b9850a4b9e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ItemNode.m @@ -0,0 +1,51 @@ +// +// ItemNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ItemNode.h" + +@implementation ItemNode + +- (instancetype)initWithString:(NSString *)string +{ + self = [super init]; + + if (self != nil) { + self.text = string; + [self updateBackgroundColor]; + } + + return self; +} + +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [UIColor grayColor]; + } else if (self.selected) { + self.backgroundColor = [UIColor darkGrayColor]; + } else { + self.backgroundColor = [UIColor lightGrayColor]; + } +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + + [self updateBackgroundColor]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Launchboard.storyboard b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/PresentingViewController.h b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/PresentingViewController.h new file mode 100644 index 0000000000..44c07c6a76 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/PresentingViewController.h @@ -0,0 +1,14 @@ +// +// PresentingViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface PresentingViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/PresentingViewController.m b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/PresentingViewController.m new file mode 100644 index 0000000000..b2d1d5e812 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/PresentingViewController.m @@ -0,0 +1,35 @@ +// +// PresentingViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PresentingViewController.h" +#import "ViewController.h" + +@interface PresentingViewController () + +@end + +@implementation PresentingViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Details" + style:UIBarButtonItemStylePlain + target:self + action:@selector(pushNewViewController)]; +} + +- (void)pushNewViewController +{ + ViewController *controller = [[ViewController alloc] init]; + [self.navigationController pushViewController:controller animated:true]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/SupplementaryNode.h b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/SupplementaryNode.h new file mode 100644 index 0000000000..b29ec002b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/SupplementaryNode.h @@ -0,0 +1,16 @@ +// +// SupplementaryNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface SupplementaryNode : ASCellNode + +- (instancetype)initWithText:(NSString *)text; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/SupplementaryNode.m b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/SupplementaryNode.m new file mode 100644 index 0000000000..f4847ff00b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/SupplementaryNode.m @@ -0,0 +1,58 @@ +// +// SupplementaryNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "SupplementaryNode.h" + +#import +#import +#import + +static CGFloat kInsets = 15.0; + +@interface SupplementaryNode () +@property (nonatomic, strong) ASTextNode *textNode; +@end + +@implementation SupplementaryNode + +- (instancetype)initWithText:(NSString *)text +{ + self = [super init]; + + if (self != nil) { + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:text + attributes:[self textAttributes]]; + [self addSubnode:_textNode]; + } + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *center = [[ASCenterLayoutSpec alloc] init]; + center.centeringOptions = ASCenterLayoutSpecCenteringXY; + center.child = self.textNode; + UIEdgeInsets insets = UIEdgeInsetsMake(kInsets, kInsets, kInsets, kInsets); + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:center]; +} + +#pragma mark - Text Formatting + +- (NSDictionary *)textAttributes +{ + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:18.0], + NSForegroundColorAttributeName: [UIColor whiteColor], + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ViewController.m new file mode 100644 index 0000000000..687635ee68 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/ViewController.m @@ -0,0 +1,193 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import "AppDelegate.h" +#import +#import "SupplementaryNode.h" +#import "ItemNode.h" + +#define ASYNC_COLLECTION_LAYOUT 0 + +static CGSize const kItemSize = (CGSize){180, 90}; + +@interface ViewController () + +@property (nonatomic, strong) ASCollectionNode *collectionNode; +@property (nonatomic, strong) NSMutableArray *> *data; +@property (nonatomic, strong) UILongPressGestureRecognizer *moveRecognizer; + +@end + +@implementation ViewController + +#pragma mark - Lifecycle + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.moveRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress)]; + [self.view addGestureRecognizer:self.moveRecognizer]; + +#if ASYNC_COLLECTION_LAYOUT + ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections]; + layoutDelegate.propertiesProvider = self; + self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; +#else + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.headerReferenceSize = CGSizeMake(50.0, 50.0); + layout.footerReferenceSize = CGSizeMake(50.0, 50.0); + layout.itemSize = kItemSize; + self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; + [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; +#endif + + self.collectionNode.dataSource = self; + self.collectionNode.delegate = self; + + self.collectionNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.collectionNode.backgroundColor = [UIColor whiteColor]; + + [self.view addSubnode:self.collectionNode]; + self.collectionNode.frame = self.view.bounds; + +#if !SIMULATE_WEB_RESPONSE + self.navigationItem.leftItemsSupplementBackButton = YES; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh + target:self + action:@selector(reloadTapped)]; + [self loadData]; +#else + __weak typeof(self) weakSelf = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [weakSelf handleSimulatedWebResponse]; + }); +#endif +} + +- (void)handleSimulatedWebResponse +{ + [self.collectionNode performBatchUpdates:^{ + [self loadData]; + [self.collectionNode insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.data.count)]]; + } completion:nil]; +} + +- (void)loadData +{ + // Form our data array + typeof(self.data) data = [NSMutableArray array]; + for (NSInteger s = 0; s < 100; s++) { + NSMutableArray *items = [NSMutableArray array]; + for (NSInteger i = 0; i < 10; i++) { + items[i] = [NSString stringWithFormat:@"[%zd.%zd] says hi", s, i]; + } + data[s] = items; + } + self.data = data; +} + +#pragma mark - Button Actions + +- (void)reloadTapped +{ + // This method is deprecated because we reccommend using ASCollectionNode instead of ASCollectionView. + // This functionality & example project remains for users who insist on using ASCollectionView. + [self.collectionNode reloadData]; +} + +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding + +- (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + return kItemSize; +} + +- (void)handleLongPress +{ + UICollectionView *collectionView = self.collectionNode.view; + CGPoint location = [self.moveRecognizer locationInView:collectionView]; + switch (self.moveRecognizer.state) { + case UIGestureRecognizerStateBegan: { + NSIndexPath *indexPath = [collectionView indexPathForItemAtPoint:location]; + if (indexPath) { + [collectionView beginInteractiveMovementForItemAtIndexPath:indexPath]; + } + break; + } + case UIGestureRecognizerStateChanged: + [collectionView updateInteractiveMovementTargetPosition:location]; + break; + case UIGestureRecognizerStateEnded: + [collectionView endInteractiveMovement]; + break; + case UIGestureRecognizerStateFailed: + case UIGestureRecognizerStateCancelled: + [collectionView cancelInteractiveMovement]; + break; + case UIGestureRecognizerStatePossible: + // nop + break; + } +} + +#pragma mark - ASCollectionDataSource + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath; +{ + NSString *text = self.data[indexPath.section][indexPath.item]; + return ^{ + return [[ItemNode alloc] initWithString:text]; + }; +} + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSString *text = [kind isEqualToString:UICollectionElementKindSectionHeader] ? @"Header" : @"Footer"; + SupplementaryNode *node = [[SupplementaryNode alloc] initWithText:text]; + BOOL isHeaderSection = [kind isEqualToString:UICollectionElementKindSectionHeader]; + node.backgroundColor = isHeaderSection ? [UIColor blueColor] : [UIColor redColor]; + return node; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return self.data[section].count; +} + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ + return self.data.count; +} + +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode canMoveItemWithNode:(ASCellNode *)node +{ + return YES; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath +{ + __auto_type sectionArray = self.data[sourceIndexPath.section]; + __auto_type object = sectionArray[sourceIndexPath.item]; + [sectionArray removeObjectAtIndex:sourceIndexPath.item]; + [self.data[destinationIndexPath.section] insertObject:object atIndex:destinationIndexPath.item]; +} + +#pragma mark - ASCollectionDelegate + +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + NSLog(@"fetch additional content"); + [context completeBatchFetching:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/main.m b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/main.m new file mode 100644 index 0000000000..65850400e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASCollectionView/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Podfile b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Podfile new file mode 100644 index 0000000000..0fbf4c9524 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' + pod 'Texture/Yoga', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..17537d093d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,371 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 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 */; }; + DFE855DDBC731242D3515B58 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C284F7E957985CA251284B05 /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 1C47DEC3F9D2BD9AD5F5CD67 /* 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 = ""; }; + 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; }; + 79ED4D85CC60068C341CFD77 /* 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 = ""; }; + C284F7E957985CA251284B05 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DFE855DDBC731242D3515B58 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + C284F7E957985CA251284B05 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */, + 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0710; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..0b71c455d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/AppDelegate.m new file mode 100644 index 0000000000..d0fd66f775 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/ViewController.m new file mode 100644 index 0000000000..e5d019b1d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/ViewController.m @@ -0,0 +1,187 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import + +#pragma mark - TransitionNode + +#define USE_CUSTOM_LAYOUT_TRANSITION 0 + +@interface TransitionNode : ASDisplayNode +@property (nonatomic, assign) BOOL enabled; +@property (nonatomic, strong) ASButtonNode *buttonNode; +@property (nonatomic, strong) ASTextNode *textNodeOne; +@property (nonatomic, strong) ASTextNode *textNodeTwo; +@end + +@implementation TransitionNode + + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + self.automaticallyManagesSubnodes = YES; + + // Define the layout transition duration for the default transition + self.defaultLayoutTransitionDuration = 1.0; + + _enabled = NO; + + // Setup text nodes + _textNodeOne = [[ASTextNode alloc] init]; + _textNodeOne.attributedText = [[NSAttributedString alloc] initWithString:@"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled"]; + + _textNodeTwo = [[ASTextNode alloc] init]; + _textNodeTwo.attributedText = [[NSAttributedString alloc] initWithString:@"It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English."]; + ASSetDebugNames(_textNodeOne, _textNodeTwo); + + // Setup button + NSString *buttonTitle = @"Start Layout Transition"; + UIFont *buttonFont = [UIFont systemFontOfSize:16.0]; + UIColor *buttonColor = [UIColor blueColor]; + + _buttonNode = [[ASButtonNode alloc] init]; + [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:UIControlStateNormal]; + [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:[buttonColor colorWithAlphaComponent:0.5] forState:UIControlStateHighlighted]; + + + // Some debug colors + _textNodeOne.backgroundColor = [UIColor orangeColor]; + _textNodeTwo.backgroundColor = [UIColor greenColor]; + + + return self; +} + +- (void)didLoad +{ + [super didLoad]; + + [self.buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchUpInside]; +} + +#pragma mark - Actions + +- (void)buttonPressed:(id)sender +{ + self.enabled = !self.enabled; + [self transitionLayoutWithAnimation:YES shouldMeasureAsync:NO measurementCompletion:nil]; +} + + +#pragma mark - Layout + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASTextNode *nextTextNode = self.enabled ? self.textNodeTwo : self.textNodeOne; + nextTextNode.style.flexGrow = 1.0; + nextTextNode.style.flexShrink = 1.0; + + ASStackLayoutSpec *horizontalStackLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; + horizontalStackLayout.children = @[nextTextNode]; + + self.buttonNode.style.alignSelf = ASStackLayoutAlignSelfCenter; + + ASStackLayoutSpec *verticalStackLayout = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackLayout.spacing = 10.0; + verticalStackLayout.children = @[horizontalStackLayout, self.buttonNode]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(15.0, 15.0, 15.0, 15.0) child:verticalStackLayout]; +} + + +#pragma mark - Transition + +#if USE_CUSTOM_LAYOUT_TRANSITION + +- (void)animateLayoutTransition:(id)context +{ + ASDisplayNode *fromNode = [[context removedSubnodes] objectAtIndex:0]; + ASDisplayNode *toNode = [[context insertedSubnodes] objectAtIndex:0]; + + ASButtonNode *buttonNode = nil; + for (ASDisplayNode *node in [context subnodesForKey:ASTransitionContextToLayoutKey]) { + if ([node isKindOfClass:[ASButtonNode class]]) { + buttonNode = (ASButtonNode *)node; + break; + } + } + + CGRect toNodeFrame = [context finalFrameForNode:toNode]; + toNodeFrame.origin.x += (self.enabled ? toNodeFrame.size.width : -toNodeFrame.size.width); + toNode.frame = toNodeFrame; + toNode.alpha = 0.0; + + CGRect fromNodeFrame = fromNode.frame; + fromNodeFrame.origin.x += (self.enabled ? -fromNodeFrame.size.width : fromNodeFrame.size.width); + + // We will use the same transition duration as the default transition + [UIView animateWithDuration:self.defaultLayoutTransitionDuration animations:^{ + toNode.frame = [context finalFrameForNode:toNode]; + toNode.alpha = 1.0; + + fromNode.frame = fromNodeFrame; + fromNode.alpha = 0.0; + + // Update frame of self + CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; + CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; + BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); + if (isResized == YES) { + CGPoint position = self.frame.origin; + self.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); + } + + buttonNode.frame = [context finalFrameForNode:buttonNode]; + } completion:^(BOOL finished) { + [context completeTransition:finished]; + }]; +} + +#endif + +@end + + +#pragma mark - ViewController + +@interface ViewController () +@property (nonatomic, strong) TransitionNode *transitionNode; +@end + +@implementation ViewController + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _transitionNode = [TransitionNode new]; + [self.view addSubnode:_transitionNode]; + + // Some debug colors + _transitionNode.backgroundColor = [UIColor grayColor]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + CGSize size = [self.transitionNode layoutThatFits:ASSizeRangeMake(CGSizeZero, self.view.frame.size)].size; + self.transitionNode.frame = CGRectMake(0, 20, size.width, size.height); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/main.m b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKLayoutTransition/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Podfile b/submodules/AsyncDisplayKit/examples/ASDKTube/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1b8c982bed --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,452 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 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 */; }; + 8B9075851CF386A400F924C1 /* ico-unmute.png in Resources */ = {isa = PBXBuildFile; fileRef = 8B90757F1CF386A400F924C1 /* ico-unmute.png */; }; + 8B9075861CF386A400F924C1 /* ico-unmute@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8B9075801CF386A400F924C1 /* ico-unmute@2x.png */; }; + 8B9075871CF386A400F924C1 /* ico-unmute@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8B9075811CF386A400F924C1 /* ico-unmute@3x.png */; }; + 8B9075881CF386A400F924C1 /* ico-mute.png in Resources */ = {isa = PBXBuildFile; fileRef = 8B9075821CF386A400F924C1 /* ico-mute.png */; }; + 8B9075891CF386A400F924C1 /* ico-mute@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8B9075831CF386A400F924C1 /* ico-mute@2x.png */; }; + 8B90758A1CF386A400F924C1 /* ico-mute@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8B9075841CF386A400F924C1 /* ico-mute@3x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 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 = ""; }; + 8B90757F1CF386A400F924C1 /* ico-unmute.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ico-unmute.png"; sourceTree = ""; }; + 8B9075801CF386A400F924C1 /* ico-unmute@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ico-unmute@2x.png"; sourceTree = ""; }; + 8B9075811CF386A400F924C1 /* ico-unmute@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ico-unmute@3x.png"; sourceTree = ""; }; + 8B9075821CF386A400F924C1 /* ico-mute.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ico-mute.png"; sourceTree = ""; }; + 8B9075831CF386A400F924C1 /* ico-mute@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ico-mute@2x.png"; sourceTree = ""; }; + 8B9075841CF386A400F924C1 /* ico-mute@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ico-mute@3x.png"; 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5791C5525B690FA54F26ACE8 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 8B90757E1CF3869100F924C1 /* Icons */, + 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 = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + A2092CAF5607B3863A3700A2 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + CFD6AA1D30516C27DEE5602B /* Pods-Sample.debug.xcconfig */, + E51646FF8D3676A1D826A5AE /* Pods-Sample.release.xcconfig */, + ); + 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 = ""; + }; + 8B90757E1CF3869100F924C1 /* Icons */ = { + isa = PBXGroup; + children = ( + 8B90757F1CF386A400F924C1 /* ico-unmute.png */, + 8B9075801CF386A400F924C1 /* ico-unmute@2x.png */, + 8B9075811CF386A400F924C1 /* ico-unmute@3x.png */, + 8B9075821CF386A400F924C1 /* ico-mute.png */, + 8B9075831CF386A400F924C1 /* ico-mute@2x.png */, + 8B9075841CF386A400F924C1 /* ico-mute@3x.png */, + ); + path = Icons; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 93B7780A33739EF25F20366B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8B9075861CF386A400F924C1 /* ico-unmute@2x.png in Resources */, + 8B9075881CF386A400F924C1 /* ico-mute.png in Resources */, + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + 8B90758A1CF386A400F924C1 /* ico-mute@3x.png in Resources */, + 8B9075851CF386A400F924C1 /* ico-unmute.png in Resources */, + 8B9075871CF386A400F924C1 /* ico-unmute@3x.png in Resources */, + 8B9075891CF386A400F924C1 /* ico-mute@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 93B7780A33739EF25F20366B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + 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; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CFD6AA1D30516C27DEE5602B /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E51646FF8D3676A1D826A5AE /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/AppDelegate.m new file mode 100644 index 0000000000..9ceabb213e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/AppDelegate.m @@ -0,0 +1,43 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "WindowWithStatusBarUnderlay.h" +#import "Utilities.h" +#import "VideoFeedNodeController.h" + + +@implementation AppDelegate + +- (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/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h new file mode 100644 index 0000000000..131bb38ef7 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h @@ -0,0 +1,14 @@ +// +// VideoFeedNodeController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface VideoFeedNodeController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m new file mode 100644 index 0000000000..b6e53ba7a5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m @@ -0,0 +1,77 @@ +// +// VideoFeedNodeController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "VideoFeedNodeController.h" +#import +#import "VideoModel.h" +#import "VideoContentCell.h" + +@interface VideoFeedNodeController () + +@end + +@implementation VideoFeedNodeController +{ + ASTableNode *_tableNode; + NSMutableArray *_videoFeedData; +} + +- (instancetype)init +{ + _tableNode = [[ASTableNode alloc] init]; + _tableNode.delegate = self; + _tableNode.dataSource = self; + + if (!(self = [super initWithNode:_tableNode])) { + return nil; + } + + [self generateFeedData]; + self.navigationItem.title = @"Home"; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [_tableNode reloadData]; +} + +- (void)generateFeedData +{ + _videoFeedData = [[NSMutableArray alloc] init]; + + for (int i = 0; i < 30; i++) { + [_videoFeedData addObject:[[VideoModel alloc] init]]; + } +} + +#pragma mark - ASCollectionDelegate - ASCollectionDataSource + +- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode +{ + return 1; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return _videoFeedData.count; +} + +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + VideoModel *videoObject = [_videoFeedData objectAtIndex:indexPath.row]; + VideoContentCell *cellNode = [[VideoContentCell alloc] initWithVideoObject:videoObject]; + + return cellNode; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute.png new file mode 100644 index 0000000000..1f700e3a5d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute@2x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute@2x.png new file mode 100644 index 0000000000..025f1386a4 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute@3x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute@3x.png new file mode 100644 index 0000000000..a4815608b9 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-mute@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute.png new file mode 100644 index 0000000000..52d15943f7 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute@2x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute@2x.png new file mode 100644 index 0000000000..e671374d47 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute@3x.png b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute@3x.png new file mode 100644 index 0000000000..2fa2462d98 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Icons/ico-unmute@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Info.plist new file mode 100644 index 0000000000..3b4c6c7839 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Info.plist @@ -0,0 +1,38 @@ + + + + + UIViewControllerBasedStatusBarAppearance + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/Utilities.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/Utilities.h new file mode 100644 index 0000000000..4fd689f18b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/Utilities.h @@ -0,0 +1,41 @@ +// +// Utilities.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#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 diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/Utilities.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/Utilities.m new file mode 100644 index 0000000000..c9998154c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/Utilities.m @@ -0,0 +1,231 @@ +// +// Utilities.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#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/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/VideoModel.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/VideoModel.h new file mode 100644 index 0000000000..c912ab45fe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/VideoModel.h @@ -0,0 +1,17 @@ +// +// VideoModel.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#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/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/VideoModel.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/VideoModel.m new file mode 100644 index 0000000000..46cd7f3da1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Models/VideoModel.m @@ -0,0 +1,28 @@ +// +// VideoModel.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "VideoModel.h" + +@implementation VideoModel +- (instancetype)init +{ + self = [super init]; + if (self) { + NSString *videoUrlString = @"https://www.w3schools.com/html/mov_bbb.mp4"; + 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/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Nodes/VideoContentCell.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Nodes/VideoContentCell.h new file mode 100644 index 0000000000..2a386f9241 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Nodes/VideoContentCell.h @@ -0,0 +1,15 @@ +// +// VideoContentCell.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "VideoModel.h" + +@interface VideoContentCell : ASCellNode +- (instancetype)initWithVideoObject:(VideoModel *)video; +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Nodes/VideoContentCell.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Nodes/VideoContentCell.m new file mode 100644 index 0000000000..fac4e389db --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/Nodes/VideoContentCell.m @@ -0,0 +1,229 @@ +// +// VideoContentCell.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "VideoContentCell.h" + +#import + +#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; + ASButtonNode *_muteButtonNode; +} + +- (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.style.flexGrow = 1.0; + [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]; + + _muteButtonNode = [[ASButtonNode alloc] init]; + _muteButtonNode.style.width = ASDimensionMakeWithPoints(16.0); + _muteButtonNode.style.height = ASDimensionMakeWithPoints(22.0); + [_muteButtonNode addTarget:self action:@selector(didTapMuteButton) forControlEvents:ASControlNodeEventTouchUpInside]; + + _videoPlayerNode = [[ASVideoPlayerNode alloc] initWithURL:_videoModel.url]; + _videoPlayerNode.delegate = self; + _videoPlayerNode.backgroundColor = [UIColor blackColor]; + [self addSubnode:_videoPlayerNode]; + + [self setMuteButtonIcon]; + } + 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.style.width = ASDimensionMakeWithPoints(fullWidth); + _videoPlayerNode.style.height = ASDimensionMakeWithPoints(fullWidth * 9 / 16); + + _avatarNode.style.width = ASDimensionMakeWithPoints(AVATAR_IMAGE_HEIGHT); + _avatarNode.style.height = ASDimensionMakeWithPoints(AVATAR_IMAGE_HEIGHT); + + _likeButtonNode.style.width = ASDimensionMakeWithPoints(50.0); + _likeButtonNode.style.height = ASDimensionMakeWithPoints(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.children = @[_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.children = @[headerInset, _videoPlayerNode, bottomControlsInset]; + return verticalStack; +} + +- (void)setMuteButtonIcon +{ + if (_videoPlayerNode.muted) { + [_muteButtonNode setImage:[UIImage imageNamed:@"ico-mute"] forState:UIControlStateNormal]; + } else { + [_muteButtonNode setImage:[UIImage imageNamed:@"ico-unmute"] forState:UIControlStateNormal]; + } +} + +- (void)didTapMuteButton +{ + _videoPlayerNode.muted = !_videoPlayerNode.muted; + [self setMuteButtonIcon]; +} + +#pragma mark - ASVideoPlayerNodeDelegate +- (void)didTapVideoPlayerNode:(ASVideoPlayerNode *)videoPlayer +{ + if (_videoPlayerNode.playerState == ASVideoNodePlayerStatePlaying) { + _videoPlayerNode.controlsDisabled = !_videoPlayerNode.controlsDisabled; + [_videoPlayerNode pause]; + } else { + [_videoPlayerNode play]; + } +} + +- (NSDictionary *)videoPlayerNodeCustomControls:(ASVideoPlayerNode *)videoPlayer +{ + return @{ + @"muteControl" : _muteButtonNode + }; +} + +- (NSArray *)controlsForControlBar:(NSDictionary *)availableControls +{ + NSMutableArray *controls = [[NSMutableArray alloc] init]; + + if (availableControls[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]) { + [controls addObject:availableControls[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]]; + } + + if (availableControls[ @(ASVideoPlayerNodeControlTypeElapsedText) ]) { + [controls addObject:availableControls[ @(ASVideoPlayerNodeControlTypeElapsedText) ]]; + } + + if (availableControls[ @(ASVideoPlayerNodeControlTypeScrubber) ]) { + [controls addObject:availableControls[ @(ASVideoPlayerNodeControlTypeScrubber) ]]; + } + + if (availableControls[ @(ASVideoPlayerNodeControlTypeDurationText) ]) { + [controls addObject:availableControls[ @(ASVideoPlayerNodeControlTypeDurationText) ]]; + } + + return controls; +} + +#pragma mark - Layout +- (ASLayoutSpec*)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer forControls:(NSDictionary *)controls forMaximumSize:(CGSize)maxSize +{ + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.style.flexGrow = 1.0; + + UIEdgeInsets insets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); + + if (controls[ @(ASVideoPlayerNodeControlTypeScrubber) ]) { + ASDisplayNode *scrubber = controls[ @(ASVideoPlayerNodeControlTypeScrubber) ]; + scrubber.style.height = ASDimensionMakeWithPoints(44.0); + scrubber.style.minWidth = ASDimensionMakeWithPoints(0.0); + scrubber.style.maxWidth = ASDimensionMakeWithPoints(maxSize.width); + scrubber.style.flexGrow = 1.0; + } + + NSArray *controlBarControls = [self controlsForControlBar:controls]; + NSMutableArray *topBarControls = [[NSMutableArray alloc] init]; + + //Our custom control + if (controls[@"muteControl"]) { + [topBarControls addObject:controls[@"muteControl"]]; + } + + + ASStackLayoutSpec *topBarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:topBarControls]; + + ASInsetLayoutSpec *topBarInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:topBarSpec]; + + ASStackLayoutSpec *controlbarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children: controlBarControls ]; + controlbarSpec.style.alignSelf = ASStackLayoutAlignSelfStretch; + + + + ASInsetLayoutSpec *controlbarInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:controlbarSpec]; + + controlbarInsetSpec.style.alignSelf = ASStackLayoutAlignSelfStretch; + + ASStackLayoutSpec *mainVerticalStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[topBarInsetSpec, spacer, controlbarInsetSpec]]; + + return mainVerticalStack; + +} +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/ViewController.h new file mode 100644 index 0000000000..560b6a2d03 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/ViewController.m new file mode 100644 index 0000000000..0fe3274501 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/ViewController.m @@ -0,0 +1,209 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import +#import +#import "VideoModel.h" +#import "VideoContentCell.h" + +@interface ViewController() +@property (nonatomic, strong) ASVideoPlayerNode *videoPlayerNode; +@end + +@implementation ViewController +{ + ASTableNode *_tableNode; + NSMutableArray *_videoFeedData; +} + +- (instancetype)init +{ + _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 reloadData]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + //[self.view addSubnode:self.videoPlayerNode]; + + //[self.videoPlayerNode setNeedsLayout]; +} + +#pragma mark - ASCollectionDelegate - ASCollectionDataSource + +- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode +{ + return 1; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return _videoFeedData.count; +} + +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + VideoModel *videoObject = [_videoFeedData objectAtIndex:indexPath.row]; + VideoContentCell *cellNode = [[VideoContentCell alloc] initWithVideoObject:videoObject]; + return cellNode; +} + +- (ASVideoPlayerNode *)videoPlayerNode; +{ + if (_videoPlayerNode) { + return _videoPlayerNode; + } + + NSURL *fileUrl = [NSURL URLWithString:@"https://www.w3schools.com/html/mov_bbb.mp4"]; + + _videoPlayerNode = [[ASVideoPlayerNode alloc] initWithURL:fileUrl]; + _videoPlayerNode.delegate = self; +// _videoPlayerNode.disableControls = YES; +// +// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ +// _videoPlayerNode.disableControls = NO; +// }); + + _videoPlayerNode.backgroundColor = [UIColor blackColor]; + + return _videoPlayerNode; +} + +#pragma mark - ASVideoPlayerNodeDelegate +//- (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; +//} + +/*- (ASLayoutSpec *)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer + forControls:(NSDictionary *)controls + forConstrainedSize:(ASSizeRange)constrainedSize +{ + + NSMutableArray *bottomControls = [[NSMutableArray alloc] init]; + NSMutableArray *topControls = [[NSMutableArray alloc] init]; + + ASDisplayNode *scrubberNode = controls[@(ASVideoPlayerNodeControlTypeScrubber)]; + ASDisplayNode *playbackButtonNode = controls[@(ASVideoPlayerNodeControlTypePlaybackButton)]; + ASTextNode *elapsedTexNode = controls[@(ASVideoPlayerNodeControlTypeElapsedText)]; + ASTextNode *durationTexNode = controls[@(ASVideoPlayerNodeControlTypeDurationText)]; + + if (playbackButtonNode) { + [bottomControls addObject:playbackButtonNode]; + } + + if (scrubberNode) { + scrubberNode.preferredFrameSize = CGSizeMake(constrainedSize.max.width, 44.0); + [bottomControls addObject:scrubberNode]; + } + + if (elapsedTexNode) { + [topControls addObject:elapsedTexNode]; + } + + if (durationTexNode) { + [topControls addObject:durationTexNode]; + } + + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.flexGrow = 1.0; + + ASStackLayoutSpec *topBarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10.0 + justifyContent:ASStackLayoutJustifyContentCenter + alignItems:ASStackLayoutAlignItemsCenter + children:topControls]; + + + + UIEdgeInsets topBarSpecInsets = UIEdgeInsetsMake(20.0, 10.0, 0.0, 10.0); + + ASInsetLayoutSpec *topBarSpecInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:topBarSpecInsets child:topBarSpec]; + topBarSpecInsetSpec.alignSelf = ASStackLayoutAlignSelfStretch; + + 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:@[ topBarSpecInsetSpec, spacer, controlbarInsetSpec ]]; + + + return mainVerticalStack; +}*/ + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h new file mode 100644 index 0000000000..025f3ba4fe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h @@ -0,0 +1,14 @@ +// +// WindowWithStatusBarUnderlay.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface WindowWithStatusBarUnderlay : UIWindow + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m new file mode 100644 index 0000000000..944e5307b5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m @@ -0,0 +1,40 @@ +// +// WindowWithStatusBarUnderlay.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#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/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/main.m b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKTube/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Podfile b/submodules/AsyncDisplayKit/examples/ASDKgram/Podfile new file mode 100644 index 0000000000..8611fa11f2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Podfile @@ -0,0 +1,9 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture/IGListKit', :path => '../..' + pod 'Texture/PINRemoteImage', :path => '../..' + pod 'Texture/Yoga', :path => '../..' + pod 'Texture/Video', :path => '../..' + pod 'Weaver', :git => 'git@github.com:TextureGroup/Weaver.git', :branch => 'master' +end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a76797d57b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,535 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 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 */; }; + 76229A781CBB79E000B62CEF /* WindowWithStatusBarUnderlay.m in Sources */ = {isa = PBXBuildFile; fileRef = 76229A771CBB79E000B62CEF /* WindowWithStatusBarUnderlay.m */; }; + 767A5F111CAA3BFE004CDA8D /* tabBarIcons in Resources */ = {isa = PBXBuildFile; fileRef = 767A5F101CAA3BFE004CDA8D /* tabBarIcons */; }; + 767A5F131CAA3C66004CDA8D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 767A5F121CAA3C66004CDA8D /* Assets.xcassets */; }; + 768843801CAA37EF00D8629E /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843681CAA37EF00D8629E /* AppDelegate.m */; }; + 768843851CAA37EF00D8629E /* ImageURLModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7688436D1CAA37EF00D8629E /* ImageURLModel.m */; }; + 768843891CAA37EF00D8629E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843711CAA37EF00D8629E /* main.m */; }; + 7688438B1CAA37EF00D8629E /* PhotoCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843731CAA37EF00D8629E /* PhotoCellNode.m */; }; + 7688438C1CAA37EF00D8629E /* PhotoCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843741CAA37EF00D8629E /* PhotoCollectionViewCell.m */; }; + 7688438D1CAA37EF00D8629E /* PhotoFeedModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843751CAA37EF00D8629E /* PhotoFeedModel.m */; }; + 7688438E1CAA37EF00D8629E /* PhotoFeedNodeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843761CAA37EF00D8629E /* PhotoFeedNodeController.m */; }; + 768843901CAA37EF00D8629E /* PhotoModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843781CAA37EF00D8629E /* PhotoModel.m */; }; + 768843911CAA37EF00D8629E /* PhotoTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 768843791CAA37EF00D8629E /* PhotoTableViewCell.m */; }; + 768843921CAA37EF00D8629E /* PhotoFeedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7688437A1CAA37EF00D8629E /* PhotoFeedViewController.m */; }; + 768843931CAA37EF00D8629E /* UserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7688437B1CAA37EF00D8629E /* UserModel.m */; }; + 768843961CAA37EF00D8629E /* Utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7688437E1CAA37EF00D8629E /* Utilities.m */; }; + B13424EE6D36C2EC5D1030B6 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AD5DDA0A29B0F32AA5CC47BA /* libPods-Sample.a */; }; + CC00D1571E15912F004E5502 /* PhotoFeedListKitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00D1561E15912F004E5502 /* PhotoFeedListKitViewController.m */; }; + CC5369AC1E15925200FAD348 /* PhotoFeedSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5369AB1E15925200FAD348 /* PhotoFeedSectionController.m */; }; + CC5532171E15CC1E0011C01F /* ASCollectionSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */; }; + CC6350BB1E1C482D002BC613 /* TailLoadingNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */; }; + CC85250F1E36B392008EABE6 /* FeedHeaderNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */; }; + CCEDDDD7200C4C0E00FFCD0A /* TextureConfigDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD6200C4C0E00FFCD0A /* TextureConfigDelegate.m */; }; + E5F128F01E09625400B4335F /* PhotoFeedBaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = E5F128EF1E09625400B4335F /* PhotoFeedBaseController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 69CE83D91E515036004AA230 /* PhotoFeedControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedControllerProtocol.h; 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; }; + 76229A761CBB79E000B62CEF /* WindowWithStatusBarUnderlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowWithStatusBarUnderlay.h; sourceTree = ""; }; + 76229A771CBB79E000B62CEF /* WindowWithStatusBarUnderlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WindowWithStatusBarUnderlay.m; sourceTree = ""; }; + 767A5F101CAA3BFE004CDA8D /* tabBarIcons */ = {isa = PBXFileReference; lastKnownFileType = folder; path = tabBarIcons; sourceTree = ""; }; + 767A5F121CAA3C66004CDA8D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 768843511CAA37EF00D8629E /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AppDelegate.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843561CAA37EF00D8629E /* ImageURLModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ImageURLModel.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 7688435B1CAA37EF00D8629E /* PhotoCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoCellNode.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 7688435C1CAA37EF00D8629E /* PhotoCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoCollectionViewCell.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 7688435D1CAA37EF00D8629E /* PhotoFeedModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoFeedModel.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 7688435E1CAA37EF00D8629E /* PhotoFeedNodeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoFeedNodeController.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843601CAA37EF00D8629E /* PhotoModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoModel.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843611CAA37EF00D8629E /* PhotoTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoTableViewCell.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843621CAA37EF00D8629E /* PhotoFeedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PhotoFeedViewController.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843631CAA37EF00D8629E /* UserModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = UserModel.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843661CAA37EF00D8629E /* Utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = Utilities.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 768843671CAA37EF00D8629E /* Sample.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = Sample.pch; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843681CAA37EF00D8629E /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AppDelegate.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 7688436D1CAA37EF00D8629E /* ImageURLModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ImageURLModel.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843711CAA37EF00D8629E /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = main.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843731CAA37EF00D8629E /* PhotoCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoCellNode.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843741CAA37EF00D8629E /* PhotoCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoCollectionViewCell.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843751CAA37EF00D8629E /* PhotoFeedModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoFeedModel.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843761CAA37EF00D8629E /* PhotoFeedNodeController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoFeedNodeController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843781CAA37EF00D8629E /* PhotoModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoModel.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 768843791CAA37EF00D8629E /* PhotoTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoTableViewCell.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 7688437A1CAA37EF00D8629E /* PhotoFeedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PhotoFeedViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 7688437B1CAA37EF00D8629E /* UserModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = UserModel.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 7688437E1CAA37EF00D8629E /* Utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = Utilities.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 7688437F1CAA37EF00D8629E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 97A9B1BAF4265967672F9EA3 /* 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 = ""; }; + 9DD0C21A21E8AA2B00CB0F51 /* Availability.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Availability.h; sourceTree = ""; }; + AD5DDA0A29B0F32AA5CC47BA /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + CC00D1551E15912F004E5502 /* PhotoFeedListKitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedListKitViewController.h; sourceTree = ""; }; + CC00D1561E15912F004E5502 /* PhotoFeedListKitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoFeedListKitViewController.m; sourceTree = ""; }; + CC5369AA1E15925200FAD348 /* PhotoFeedSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedSectionController.h; sourceTree = ""; }; + CC5369AB1E15925200FAD348 /* PhotoFeedSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoFeedSectionController.m; sourceTree = ""; }; + CC5532111E159D770011C01F /* RefreshingSectionControllerType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RefreshingSectionControllerType.h; sourceTree = ""; }; + CC5532151E15CC1E0011C01F /* ASCollectionSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionSectionController.h; sourceTree = ""; }; + CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionSectionController.m; sourceTree = ""; }; + CC6350B91E1C482D002BC613 /* TailLoadingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TailLoadingNode.h; sourceTree = ""; }; + CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TailLoadingNode.m; sourceTree = ""; }; + CC85250D1E36B392008EABE6 /* FeedHeaderNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedHeaderNode.h; sourceTree = ""; }; + CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedHeaderNode.m; sourceTree = ""; }; + CCEDDDD6200C4C0E00FFCD0A /* TextureConfigDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TextureConfigDelegate.m; sourceTree = ""; }; + D09B5DF0BFB37583DE8F3142 /* 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 = ""; }; + E5F128EE1E09612700B4335F /* PhotoFeedBaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedBaseController.h; sourceTree = ""; }; + E5F128EF1E09625400B4335F /* PhotoFeedBaseController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoFeedBaseController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B13424EE6D36C2EC5D1030B6 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 9DD0C21A21E8AA2B00CB0F51 /* Availability.h */, + 768843511CAA37EF00D8629E /* AppDelegate.h */, + 768843681CAA37EF00D8629E /* AppDelegate.m */, + CCEDDDD6200C4C0E00FFCD0A /* TextureConfigDelegate.m */, + 76229A761CBB79E000B62CEF /* WindowWithStatusBarUnderlay.h */, + 76229A771CBB79E000B62CEF /* WindowWithStatusBarUnderlay.m */, + 767A5F141CAA3D8A004CDA8D /* Controller */, + 767A5F181CAA3DB0004CDA8D /* View */, + 767A5F171CAA3DA1004CDA8D /* Model */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 768843671CAA37EF00D8629E /* Sample.pch */, + 7688437F1CAA37EF00D8629E /* Info.plist */, + 768843711CAA37EF00D8629E /* main.m */, + 767A5F121CAA3C66004CDA8D /* Assets.xcassets */, + 767A5F101CAA3BFE004CDA8D /* tabBarIcons */, + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */, + AD5DDA0A29B0F32AA5CC47BA /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 97A9B1BAF4265967672F9EA3 /* Pods-Sample.debug.xcconfig */, + D09B5DF0BFB37583DE8F3142 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 767A5F141CAA3D8A004CDA8D /* Controller */ = { + isa = PBXGroup; + children = ( + 69CE83D91E515036004AA230 /* PhotoFeedControllerProtocol.h */, + E5F128EE1E09612700B4335F /* PhotoFeedBaseController.h */, + E5F128EF1E09625400B4335F /* PhotoFeedBaseController.m */, + 767A5F161CAA3D96004CDA8D /* UIKit */, + 767A5F151CAA3D90004CDA8D /* ASDK */, + CC00D1581E159132004E5502 /* ASDK-ListKit */, + ); + name = Controller; + sourceTree = ""; + }; + 767A5F151CAA3D90004CDA8D /* ASDK */ = { + isa = PBXGroup; + children = ( + 7688435E1CAA37EF00D8629E /* PhotoFeedNodeController.h */, + 768843761CAA37EF00D8629E /* PhotoFeedNodeController.m */, + ); + name = ASDK; + sourceTree = ""; + }; + 767A5F161CAA3D96004CDA8D /* UIKit */ = { + isa = PBXGroup; + children = ( + 768843621CAA37EF00D8629E /* PhotoFeedViewController.h */, + 7688437A1CAA37EF00D8629E /* PhotoFeedViewController.m */, + ); + name = UIKit; + sourceTree = ""; + }; + 767A5F171CAA3DA1004CDA8D /* Model */ = { + isa = PBXGroup; + children = ( + 7688435D1CAA37EF00D8629E /* PhotoFeedModel.h */, + 768843751CAA37EF00D8629E /* PhotoFeedModel.m */, + 768843601CAA37EF00D8629E /* PhotoModel.h */, + 768843781CAA37EF00D8629E /* PhotoModel.m */, + 768843561CAA37EF00D8629E /* ImageURLModel.h */, + 7688436D1CAA37EF00D8629E /* ImageURLModel.m */, + 768843631CAA37EF00D8629E /* UserModel.h */, + 7688437B1CAA37EF00D8629E /* UserModel.m */, + 768843661CAA37EF00D8629E /* Utilities.h */, + 7688437E1CAA37EF00D8629E /* Utilities.m */, + ); + name = Model; + sourceTree = ""; + }; + 767A5F181CAA3DB0004CDA8D /* View */ = { + isa = PBXGroup; + children = ( + 767A5F191CAA3DB9004CDA8D /* UIKit */, + 767A5F1A1CAA3DBF004CDA8D /* ASDK */, + ); + name = View; + sourceTree = ""; + }; + 767A5F191CAA3DB9004CDA8D /* UIKit */ = { + isa = PBXGroup; + children = ( + 768843611CAA37EF00D8629E /* PhotoTableViewCell.h */, + 768843791CAA37EF00D8629E /* PhotoTableViewCell.m */, + 7688435C1CAA37EF00D8629E /* PhotoCollectionViewCell.h */, + 768843741CAA37EF00D8629E /* PhotoCollectionViewCell.m */, + ); + name = UIKit; + sourceTree = ""; + }; + 767A5F1A1CAA3DBF004CDA8D /* ASDK */ = { + isa = PBXGroup; + children = ( + CC85250D1E36B392008EABE6 /* FeedHeaderNode.h */, + CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */, + CC6350B91E1C482D002BC613 /* TailLoadingNode.h */, + CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */, + 7688435B1CAA37EF00D8629E /* PhotoCellNode.h */, + 768843731CAA37EF00D8629E /* PhotoCellNode.m */, + ); + name = ASDK; + sourceTree = ""; + }; + CC00D1581E159132004E5502 /* ASDK-ListKit */ = { + isa = PBXGroup; + children = ( + CC5532111E159D770011C01F /* RefreshingSectionControllerType.h */, + CC00D1551E15912F004E5502 /* PhotoFeedListKitViewController.h */, + CC00D1561E15912F004E5502 /* PhotoFeedListKitViewController.m */, + CC5369AA1E15925200FAD348 /* PhotoFeedSectionController.h */, + CC5369AB1E15925200FAD348 /* PhotoFeedSectionController.m */, + CC5532151E15CC1E0011C01F /* ASCollectionSectionController.h */, + CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */, + ); + name = "ASDK-ListKit"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + 767A5F111CAA3BFE004CDA8D /* tabBarIcons in Resources */, + 767A5F131CAA3C66004CDA8D /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 768843891CAA37EF00D8629E /* main.m in Sources */, + 7688438C1CAA37EF00D8629E /* PhotoCollectionViewCell.m in Sources */, + 768843921CAA37EF00D8629E /* PhotoFeedViewController.m in Sources */, + 76229A781CBB79E000B62CEF /* WindowWithStatusBarUnderlay.m in Sources */, + 768843961CAA37EF00D8629E /* Utilities.m in Sources */, + E5F128F01E09625400B4335F /* PhotoFeedBaseController.m in Sources */, + 768843931CAA37EF00D8629E /* UserModel.m in Sources */, + CC5532171E15CC1E0011C01F /* ASCollectionSectionController.m in Sources */, + 768843801CAA37EF00D8629E /* AppDelegate.m in Sources */, + CCEDDDD7200C4C0E00FFCD0A /* TextureConfigDelegate.m in Sources */, + 7688438E1CAA37EF00D8629E /* PhotoFeedNodeController.m in Sources */, + CC6350BB1E1C482D002BC613 /* TailLoadingNode.m in Sources */, + CC85250F1E36B392008EABE6 /* FeedHeaderNode.m in Sources */, + 768843901CAA37EF00D8629E /* PhotoModel.m in Sources */, + 768843911CAA37EF00D8629E /* PhotoTableViewCell.m in Sources */, + CC00D1571E15912F004E5502 /* PhotoFeedListKitViewController.m in Sources */, + 7688438B1CAA37EF00D8629E /* PhotoCellNode.m in Sources */, + 7688438D1CAA37EF00D8629E /* PhotoFeedModel.m in Sources */, + CC5369AC1E15925200FAD348 /* PhotoFeedSectionController.m in Sources */, + 768843851CAA37EF00D8629E /* ImageURLModel.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 97A9B1BAF4265967672F9EA3 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Sample/Sample.pch; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D09B5DF0BFB37583DE8F3142 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Sample/Sample.pch; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..d41d58c5d8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ASCollectionSectionController.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ASCollectionSectionController.h new file mode 100644 index 0000000000..1a3a8443c9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ASCollectionSectionController.h @@ -0,0 +1,28 @@ +// +// ASCollectionSectionController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASCollectionSectionController : IGListSectionController + +/** + * The items managed by this section controller. + */ +@property (nonatomic, strong, readonly) NSArray> *items; + +- (void)setItems:(NSArray> *)newItems + animated:(BOOL)animated + completion:(nullable void(^)())completion; + +- (NSInteger)numberOfItems; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ASCollectionSectionController.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ASCollectionSectionController.m new file mode 100644 index 0000000000..c1ddb42d41 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ASCollectionSectionController.m @@ -0,0 +1,73 @@ +// +// ASCollectionSectionController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASCollectionSectionController.h" +#import + +@interface ASCollectionSectionController () +@property (nonatomic, strong, readonly) dispatch_queue_t diffingQueue; + +/// The items that have been diffed and are waiting to be submitted to the collection view. +/// Should always be accessed on the diffing queue, and should never be accessed +/// before the initial items are read (in -numberOfItems). +@property (nonatomic, copy) NSArray *pendingItems; + +@property (nonatomic) BOOL initialItemsRead; +@end + +@implementation ASCollectionSectionController +@synthesize diffingQueue = _diffingQueue; + +- (NSInteger)numberOfItems +{ + if (_initialItemsRead == NO) { + _pendingItems = self.items; + _initialItemsRead = YES; + } + return self.items.count; +} + +- (dispatch_queue_t)diffingQueue +{ + if (_diffingQueue == nil) { + _diffingQueue = dispatch_queue_create("ASCollectionSectionController.diffingQueue", DISPATCH_QUEUE_SERIAL); + } + return _diffingQueue; +} + +- (void)setItems:(NSArray *)newItems animated:(BOOL)animated completion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + newItems = [newItems copy]; + if (!self.initialItemsRead) { + _items = newItems; + if (completion) { + completion(); + } + return; + } + + dispatch_async(self.diffingQueue, ^{ + IGListIndexSetResult *result = IGListDiff(self.pendingItems, newItems, IGListDiffPointerPersonality); + self.pendingItems = newItems; + dispatch_async(dispatch_get_main_queue(), ^{ + id ctx = self.collectionContext; + [ctx performBatchAnimated:animated updates:^(id _Nonnull batchContext) { + [batchContext insertInSectionController:(id)self atIndexes:result.inserts]; + [batchContext deleteInSectionController:(id)self atIndexes:result.deletes]; + _items = newItems; + } completion:^(BOOL finished) { + if (completion) { + completion(); + } + }]; + }); + }); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/AppDelegate.h new file mode 100644 index 0000000000..169cc651b5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/AppDelegate.h @@ -0,0 +1,13 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +@interface AppDelegate : UIResponder + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/AppDelegate.m new file mode 100644 index 0000000000..9058c8f100 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/AppDelegate.m @@ -0,0 +1,101 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "PhotoFeedViewController.h" +#import "PhotoFeedNodeController.h" +#import "PhotoFeedListKitViewController.h" +#import "WindowWithStatusBarUnderlay.h" +#import "Utilities.h" + +#import + +#define WEAVER 0 + +#if WEAVER +#import +#endif + +@interface AppDelegate () +@end + +@implementation AppDelegate +{ + WindowWithStatusBarUnderlay *_window; +} + +- (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]; + + // ASDK Home Feed viewController & navController + PhotoFeedNodeController *asdkHomeFeedVC = [[PhotoFeedNodeController alloc] init]; + UINavigationController *asdkHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:asdkHomeFeedVC]; + asdkHomeFeedNavCtrl.navigationBar.barStyle = UIBarStyleBlack; + asdkHomeFeedNavCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"ASDK" image:[UIImage imageNamed:@"home"] tag:0]; + asdkHomeFeedNavCtrl.hidesBarsOnSwipe = YES; + + // ListKit Home Feed viewController & navController + PhotoFeedListKitViewController *listKitHomeFeedVC = [[PhotoFeedListKitViewController alloc] init]; + UINavigationController *listKitHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:listKitHomeFeedVC]; + listKitHomeFeedNavCtrl.navigationBar.barStyle = UIBarStyleBlack; + listKitHomeFeedNavCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"ListKit" image:[UIImage imageNamed:@"home"] tag:0]; + listKitHomeFeedNavCtrl.hidesBarsOnSwipe = YES; + + + + // UIKit Home Feed viewController & navController + PhotoFeedViewController *uikitHomeFeedVC = [[PhotoFeedViewController alloc] init]; + UINavigationController *uikitHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:uikitHomeFeedVC]; + uikitHomeFeedNavCtrl.navigationBar.barStyle = UIBarStyleBlack; + uikitHomeFeedNavCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"UIKit" image:[UIImage imageNamed:@"home"] tag:0]; + uikitHomeFeedNavCtrl.hidesBarsOnSwipe = YES; + + // UITabBarController + UITabBarController *tabBarController = [[UITabBarController alloc] init]; + tabBarController.viewControllers = @[uikitHomeFeedNavCtrl, asdkHomeFeedNavCtrl, listKitHomeFeedNavCtrl]; + tabBarController.selectedViewController = asdkHomeFeedNavCtrl; + tabBarController.delegate = self; + [[UITabBar appearance] setTintColor:[UIColor darkBlueColor]]; + + _window.rootViewController = tabBarController; + [_window makeKeyAndVisible]; + + // Nav Bar appearance + NSDictionary *attributes = @{NSForegroundColorAttributeName:[UIColor whiteColor]}; + [[UINavigationBar appearance] setTitleTextAttributes:attributes]; + [[UINavigationBar appearance] setBarTintColor:[UIColor darkBlueColor]]; + [[UINavigationBar appearance] setTranslucent:NO]; + +#if WEAVER + WVDebugger *debugger = [WVDebugger defaultInstance]; + [debugger enableLayoutElementDebuggingWithApplication:application]; + [debugger autoConnect]; +#endif + + return YES; +} + +#pragma mark - UITabBarControllerDelegate + +- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController +{ + if ([viewController isKindOfClass:[UINavigationController class]]) { + NSArray *viewControllers = [(UINavigationController *)viewController viewControllers]; + UIViewController *rootViewController = viewControllers[0]; + if ([rootViewController conformsToProtocol:@protocol(PhotoFeedControllerProtocol)]) { + // FIXME: the dataModel does not currently handle clearing data during loading properly +// [(id )rootViewController resetAllData]; + } + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..eeea76c2db --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/Contents.json new file mode 100644 index 0000000000..07252697c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "camera.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "camera@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/camera.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/camera.png new file mode 100644 index 0000000000..2eeecba825 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/camera.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/camera@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/camera@2x.png new file mode 100644 index 0000000000..c1ea4ab857 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/camera.imageset/camera@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/crosshairs.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/crosshairs.imageset/Contents.json new file mode 100644 index 0000000000..66e65dc03e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/crosshairs.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "crosshair.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/crosshairs.imageset/crosshair.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/crosshairs.imageset/crosshair.png new file mode 100644 index 0000000000..ea3c5e27ba Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/crosshairs.imageset/crosshair.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/Contents.json new file mode 100644 index 0000000000..37e4afe0e2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "earth.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "earth@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/earth.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/earth.png new file mode 100644 index 0000000000..c182ea5565 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/earth.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/earth@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/earth@2x.png new file mode 100644 index 0000000000..b8049a5004 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/earth.imageset/earth@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/Contents.json new file mode 100644 index 0000000000..ce48b1b641 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "home.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "home@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/home.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/home.png new file mode 100644 index 0000000000..b88cd66a4b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/home.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/home@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/home@2x.png new file mode 100644 index 0000000000..838e660097 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/home.imageset/home@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/Contents.json new file mode 100644 index 0000000000..ecb5bbebcf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "profile.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "profile@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/profile.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/profile.png new file mode 100644 index 0000000000..d885b3aedf Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/profile.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/profile@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/profile@2x.png new file mode 100644 index 0000000000..81352fe0cb Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Assets.xcassets/Tab Bar Icons/profile.imageset/profile@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Availability.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Availability.h new file mode 100644 index 0000000000..27c8f82ceb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Availability.h @@ -0,0 +1,18 @@ +// +// Availability.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +/** + * Enable Yoga layout engine in Texture cells + */ +#define YOGA_LAYOUT 0 + +/** + * There are many ways to format ASLayoutSpec code. In this example, we offer two different formats: + * A flatter, more ordinary Objective-C style; or a more structured, "visually" declarative style. + */ +#define FLAT_LAYOUT 0 diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..8326657f7a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/FeedHeaderNode.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/FeedHeaderNode.h new file mode 100644 index 0000000000..13e6b16d6f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/FeedHeaderNode.h @@ -0,0 +1,13 @@ +// +// FeedHeaderNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface FeedHeaderNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/FeedHeaderNode.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/FeedHeaderNode.m new file mode 100644 index 0000000000..f6d922c27c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/FeedHeaderNode.m @@ -0,0 +1,55 @@ +// +// FeedHeaderNode.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "FeedHeaderNode.h" + +#import "Availability.h" +#import "Utilities.h" + +#define YOGA_LAYOUT 0 + +static UIEdgeInsets kFeedHeaderInset = { .top = 20, .bottom = 20, .left = 10, .right = 10 }; + +@interface FeedHeaderNode () +@property (nonatomic, strong, readonly) ASTextNode *textNode; +@end + +@implementation FeedHeaderNode + +- (instancetype)init +{ + if (self = [super init]) { + self.automaticallyManagesSubnodes = YES; + + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [NSAttributedString attributedStringWithString:@"Latest Posts" fontSize:18 color:[UIColor darkGrayColor] firstWordColor:nil]; + + [self setupYogaLayoutIfNeeded]; + } + return self; +} + +#if !YOGA_LAYOUT +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:kFeedHeaderInset child:_textNode]; +} +#endif + +- (void)setupYogaLayoutIfNeeded +{ +#if YOGA_LAYOUT + [self.style yogaNodeCreateIfNeeded]; + [self.textNode.style yogaNodeCreateIfNeeded]; + [self addYogaChild:self.textNode]; + + self.style.padding = ASEdgeInsetsMake(kFeedHeaderInset); +#endif +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ImageURLModel.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ImageURLModel.h new file mode 100644 index 0000000000..16828a2dac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ImageURLModel.h @@ -0,0 +1,14 @@ +// +// ImageURLModel.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +@interface ImageURLModel : NSObject + ++ (NSString *)imageParameterForClosestImageSize:(CGSize)size; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ImageURLModel.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ImageURLModel.m new file mode 100644 index 0000000000..671027db5e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/ImageURLModel.m @@ -0,0 +1,50 @@ +// +// ImageURLModel.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ImageURLModel.h" + +@implementation ImageURLModel + ++ (NSString *)imageParameterForClosestImageSize:(CGSize)size +{ + BOOL squareImageRequested = (size.width == size.height) ? YES : NO; + + if (squareImageRequested) { + NSUInteger imageParameterID = [self imageParameterForSquareCroppedSize:size]; + return [NSString stringWithFormat:@"&image_size=%lu", (long)imageParameterID]; + } else { + return @""; + } +} + +// 500px standard cropped image sizes ++ (NSUInteger)imageParameterForSquareCroppedSize:(CGSize)size +{ + NSUInteger imageParameterID; + + if (size.height <= 70) { + imageParameterID = 1; + } else if (size.height <= 100) { + imageParameterID = 100; + } else if (size.height <= 140) { + imageParameterID = 2; + } else if (size.height <= 200) { + imageParameterID = 200; + } else if (size.height <= 280) { + imageParameterID = 3; + } else if (size.height <= 400) { + imageParameterID = 400; + } else { + imageParameterID = 600; + } + + return imageParameterID; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Info.plist new file mode 100644 index 0000000000..7641b2f1a3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCellNode.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCellNode.h new file mode 100644 index 0000000000..7724970252 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCellNode.h @@ -0,0 +1,19 @@ +// +// PhotoCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoModel.h" +#import +#import "PhotoTableViewCell.h" // PhotoTableViewCellProtocol + + +@interface PhotoCellNode : ASCellNode + +- (instancetype)initWithPhotoObject:(PhotoModel *)photo; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCellNode.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCellNode.m new file mode 100644 index 0000000000..eef2bb06be --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCellNode.m @@ -0,0 +1,341 @@ +// +// PhotoCellNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoCellNode.h" + +#import +#import + +#import "Availability.h" +#import "PINImageView+PINRemoteImage.h" +#import "PINButton+PINRemoteImage.h" +#import "Utilities.h" + +#define DEBUG_PHOTOCELL_LAYOUT 0 + +#define HEADER_HEIGHT 50 +#define USER_IMAGE_HEIGHT 30 +#define HORIZONTAL_BUFFER 10 +#define VERTICAL_BUFFER 5 +#define FONT_SIZE 14 + +#define InsetForAvatar UIEdgeInsetsMake(HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER) +#define InsetForHeader UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER) +#define InsetForFooter UIEdgeInsetsMake(VERTICAL_BUFFER, HORIZONTAL_BUFFER, VERTICAL_BUFFER, HORIZONTAL_BUFFER) + +@interface PhotoCellNode () +@end + +@implementation PhotoCellNode +{ + PhotoModel *_photoModel; + ASNetworkImageNode *_userAvatarImageNode; + ASNetworkImageNode *_photoImageNode; + ASTextNode *_userNameLabel; + ASTextNode *_photoLocationLabel; + ASTextNode *_photoTimeIntervalSincePostLabel; + ASTextNode *_photoLikesLabel; + ASTextNode *_photoDescriptionLabel; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithPhotoObject:(PhotoModel *)photo; +{ + self = [super init]; + + if (self) { + + _photoModel = photo; + + _userAvatarImageNode = [[ASNetworkImageNode alloc] init]; + _userAvatarImageNode.URL = photo.ownerUserProfile.userPicURL; // FIXME: make round + + // FIXME: autocomplete for this line seems broken + [_userAvatarImageNode setImageModificationBlock:^UIImage *(UIImage *image) { + CGSize profileImageSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + return [image makeCircularImageWithSize:profileImageSize]; + }]; + + _photoImageNode = [[ASNetworkImageNode alloc] init]; + _photoImageNode.delegate = self; + _photoImageNode.URL = photo.URL; + _photoImageNode.layerBacked = YES; + + _userNameLabel = [[ASTextNode alloc] init]; + _userNameLabel.attributedText = [photo.ownerUserProfile usernameAttributedStringWithFontSize:FONT_SIZE]; + + _photoLocationLabel = [[ASTextNode alloc] init]; + _photoLocationLabel.maximumNumberOfLines = 1; + _photoLocationLabel.attributedText = [photo locationAttributedStringWithFontSize:FONT_SIZE]; + + _photoTimeIntervalSincePostLabel = [self createLayerBackedTextNodeWithString:[photo uploadDateAttributedStringWithFontSize:FONT_SIZE]]; + _photoLikesLabel = [self createLayerBackedTextNodeWithString:[photo likesAttributedStringWithFontSize:FONT_SIZE]]; + _photoDescriptionLabel = [self createLayerBackedTextNodeWithString:[photo descriptionAttributedStringWithFontSize:FONT_SIZE]]; + _photoDescriptionLabel.maximumNumberOfLines = 3; + + // instead of adding everything addSubnode: + self.automaticallyManagesSubnodes = YES; + + [self setupYogaLayoutIfNeeded]; + +#if DEBUG_PHOTOCELL_LAYOUT + _userAvatarImageNode.backgroundColor = [UIColor greenColor]; + _userNameLabel.backgroundColor = [UIColor greenColor]; + _photoLocationLabel.backgroundColor = [UIColor greenColor]; + _photoTimeIntervalSincePostLabel.backgroundColor = [UIColor greenColor]; + _photoCommentsNode.backgroundColor = [UIColor greenColor]; + _photoDescriptionLabel.backgroundColor = [UIColor greenColor]; + _photoLikesLabel.backgroundColor = [UIColor greenColor]; +#endif + } + + return self; +} + +#if !YOGA_LAYOUT +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // There are many ways to format ASLayoutSpec code. In this example, we offer two different formats: + // A flatter, more ordinary Objective-C style; or a more structured, "visually" declarative style. + if (FLAT_LAYOUT) { + // This layout has a horizontal stack of header items at the top, set within a vertical stack of items. + NSMutableArray *headerChildren = [NSMutableArray array]; + NSMutableArray *verticalChildren = [NSMutableArray array]; + + // Header stack + ASStackLayoutSpec *headerStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; + headerStack.alignItems = ASStackLayoutAlignItemsCenter; + + // Avatar Image, with inset - first thing in the header stack. + _userAvatarImageNode.style.preferredSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + [headerChildren addObject:[ASInsetLayoutSpec insetLayoutSpecWithInsets:InsetForAvatar child:_userAvatarImageNode]]; + + // User Name and Photo Location stack is next + ASStackLayoutSpec *userPhotoLocationStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + userPhotoLocationStack.style.flexShrink = 1.0; + [headerChildren addObject:userPhotoLocationStack]; + + // Setup the inside of the User Name and Photo Location stack. + _userNameLabel.style.flexShrink = 1.0; + [userPhotoLocationStack setChildren:@[_userNameLabel]]; + + if (_photoLocationLabel.attributedText) { + _photoLocationLabel.style.flexShrink = 1.0; + [userPhotoLocationStack setChildren:[userPhotoLocationStack.children arrayByAddingObject:_photoLocationLabel]]; + } + + // Add a spacer to allow a flexible space between the User Name / Location stack, and the Timestamp. + ASLayoutSpec *spacer = [ASLayoutSpec new]; + spacer.style.flexGrow = 1.0; + [headerChildren addObject:spacer]; + + // Photo Timestamp Label. + _photoTimeIntervalSincePostLabel.style.spacingBefore = HORIZONTAL_BUFFER; + [headerChildren addObject:_photoTimeIntervalSincePostLabel]; + + // Add all of the above items to the horizontal header stack + headerStack.children = headerChildren; + + // Create the last stack before assembling everything: the Footer Stack contains the description and comments. + ASStackLayoutSpec *footerStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + footerStack.spacing = VERTICAL_BUFFER; + footerStack.children = @[_photoLikesLabel, _photoDescriptionLabel]; + + // Main Vertical Stack: contains header, large main photo with fixed aspect ratio, and footer. + ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + + [verticalChildren addObject:[ASInsetLayoutSpec insetLayoutSpecWithInsets:InsetForHeader child:headerStack]]; + [verticalChildren addObject:[ASRatioLayoutSpec ratioLayoutSpecWithRatio :1.0 child:_photoImageNode]]; + [verticalChildren addObject:[ASInsetLayoutSpec insetLayoutSpecWithInsets:InsetForFooter child:footerStack]]; + + verticalStack.children = verticalChildren; + + return verticalStack; + + } else { // The style below is the more structured, visual, and declarative style. It is functionally identical. + + return + // Main stack + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[ + + // Header stack with inset + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:InsetForHeader + child: + // Header stack + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[ + // Avatar image with inset + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:InsetForAvatar + child: + [_userAvatarImageNode styledWithBlock:^(ASLayoutElementStyle *style) { + style.preferredSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + }] + ], + // User and photo location stack + [[ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:_photoLocationLabel.attributedText ? @[ + [_userNameLabel styledWithBlock:^(ASLayoutElementStyle *style) { + style.flexShrink = 1.0; + }], + [_photoLocationLabel styledWithBlock:^(ASLayoutElementStyle *style) { + style.flexShrink = 1.0; + }] + ] : + @[ + [_userNameLabel styledWithBlock:^(ASLayoutElementStyle *style) { + style.flexShrink = 1.0; + }] + ]] + styledWithBlock:^(ASLayoutElementStyle *style) { + style.flexShrink = 1.0; + }], + // Spacer between user / photo location and photo time inverval + [[ASLayoutSpec new] styledWithBlock:^(ASLayoutElementStyle *style) { + style.flexGrow = 1.0; + }], + // Photo and time interval node + [_photoTimeIntervalSincePostLabel styledWithBlock:^(ASLayoutElementStyle *style) { + // to remove double spaces around spacer + style.spacingBefore = HORIZONTAL_BUFFER; + }] + ]] + ], + + // Center photo with ratio + [ASRatioLayoutSpec + ratioLayoutSpecWithRatio:1.0 + child:_photoImageNode], + + // Footer stack with inset + [ASInsetLayoutSpec + insetLayoutSpecWithInsets:InsetForFooter + child: + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:VERTICAL_BUFFER + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[ + _photoLikesLabel, + _photoDescriptionLabel + ]] + ] + ]]; + } +} +#endif + +#pragma mark - Instance Methods + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; +} + +#pragma mark - Network Image Delegate + +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageLoadInfo *)info +{ + // Docs say method is called from bg but right now it's called from main. + // Save main thread time by shunting this. + if (info.sourceType == ASNetworkImageSourceDownload) { + ASPerformBlockOnBackgroundThread(^{ + NSLog(@"Received image %@ from %@ with userInfo %@", image, info.url.path, ASObjectDescriptionMakeTiny(info.userInfo)); + }); + } +} + +#pragma mark - Helper Methods + +- (ASTextNode *)createLayerBackedTextNodeWithString:(NSAttributedString *)attributedString +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.layerBacked = YES; + textNode.attributedText = attributedString; + return textNode; +} + +- (void)setupYogaLayoutIfNeeded +{ +#if YOGA_LAYOUT + [self.style yogaNodeCreateIfNeeded]; + [_userAvatarImageNode.style yogaNodeCreateIfNeeded]; + [_userNameLabel.style yogaNodeCreateIfNeeded]; + [_photoImageNode.style yogaNodeCreateIfNeeded]; + [_photoLikesLabel.style yogaNodeCreateIfNeeded]; + [_photoDescriptionLabel.style yogaNodeCreateIfNeeded]; + [_photoLocationLabel.style yogaNodeCreateIfNeeded]; + [_photoTimeIntervalSincePostLabel.style yogaNodeCreateIfNeeded]; + + ASDisplayNode *headerStack = [ASDisplayNode yogaHorizontalStack]; + headerStack.style.margin = ASEdgeInsetsMake(InsetForHeader); + headerStack.style.alignItems = ASStackLayoutAlignItemsCenter; + headerStack.style.flexGrow = 1.0; + + // Avatar Image, with inset - first thing in the header stack. + _userAvatarImageNode.style.preferredSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + _userAvatarImageNode.style.margin = ASEdgeInsetsMake(InsetForAvatar); + [headerStack addYogaChild:_userAvatarImageNode]; + + // User Name and Photo Location stack is next + ASDisplayNode *userPhotoLocationStack = [ASDisplayNode yogaVerticalStack]; + userPhotoLocationStack.style.flexShrink = 1.0; + [headerStack addYogaChild:userPhotoLocationStack]; + + // Setup the inside of the User Name and Photo Location stack. + _userNameLabel.style.flexShrink = 1.0; + [userPhotoLocationStack addYogaChild:_userNameLabel]; + + if (_photoLocationLabel.attributedText) { + _photoLocationLabel.style.flexShrink = 1.0; + [userPhotoLocationStack addYogaChild:_photoLocationLabel]; + } + + // Add a spacer to allow a flexible space between the User Name / Location stack, and the Timestamp. + [headerStack addYogaChild:[ASDisplayNode yogaSpacerNode]]; + + // Photo Timestamp Label. + _photoTimeIntervalSincePostLabel.style.spacingBefore = HORIZONTAL_BUFFER; + [headerStack addYogaChild:_photoTimeIntervalSincePostLabel]; + + // Create the last stack before assembling everything: the Footer Stack contains the description and comments. + ASDisplayNode *footerStack = [ASDisplayNode yogaVerticalStack]; + footerStack.style.margin = ASEdgeInsetsMake(InsetForFooter); + footerStack.style.padding = ASEdgeInsetsMake(UIEdgeInsetsMake(0.0, 0.0, VERTICAL_BUFFER, 0.0)); + footerStack.yogaChildren = @[_photoLikesLabel, _photoDescriptionLabel]; + + // Main Vertical Stack: contains header, large main photo with fixed aspect ratio, and footer. + _photoImageNode.style.aspectRatio = 1.0; + + ASDisplayNode *verticalStack = self; + self.style.flexDirection = ASStackLayoutDirectionVertical; + + [verticalStack addYogaChild:headerStack]; + [verticalStack addYogaChild:_photoImageNode]; + [verticalStack addYogaChild:footerStack]; +#endif +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCollectionViewCell.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCollectionViewCell.h new file mode 100644 index 0000000000..d277b5a571 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCollectionViewCell.h @@ -0,0 +1,16 @@ +// +// PhotoCollectionViewCell.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoModel.h" + +@interface PhotoCollectionViewCell : UICollectionViewCell + +- (void)updateCellWithPhotoObject:(PhotoModel *)photo; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCollectionViewCell.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCollectionViewCell.m new file mode 100644 index 0000000000..d04fe1ca5d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoCollectionViewCell.m @@ -0,0 +1,58 @@ +// +// PhotoCollectionViewCell.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoCollectionViewCell.h" +#import "PINImageView+PINRemoteImage.h" +#import "PINButton+PINRemoteImage.h" + +@implementation PhotoCollectionViewCell +{ + UIImageView *_photoImageView; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + + _photoImageView = [[UIImageView alloc] init]; + [_photoImageView setPin_updateWithProgress:YES]; + [self.contentView addSubview:_photoImageView]; + } + + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + _photoImageView.frame = self.bounds; +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + + // remove images so that the old content doesn't appear before the new content is loaded + _photoImageView.image = nil; +} + +#pragma mark - Instance Methods + +- (void)updateCellWithPhotoObject:(PhotoModel *)photo +{ + // async download of photo using PINRemoteImage + [_photoImageView pin_setImageFromURL:photo.URL]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedBaseController.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedBaseController.h new file mode 100644 index 0000000000..b40a5946c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedBaseController.h @@ -0,0 +1,28 @@ +// +// PhotoFeedBaseController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "PhotoFeedControllerProtocol.h" + +@protocol PhotoFeedControllerProtocol; +@class PhotoFeedModel; + +@interface PhotoFeedBaseController : ASViewController + +@property (nonatomic, strong, readonly) PhotoFeedModel *photoFeed; +@property (nonatomic, strong, readonly) UITableView *tableView; + +- (void)refreshFeed; +- (void)insertNewRows:(NSArray *)newPhotos; + +#pragma mark - Subclasses must override these methods + +- (void)loadPage; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedBaseController.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedBaseController.m new file mode 100644 index 0000000000..d94a34e922 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedBaseController.m @@ -0,0 +1,112 @@ +// +// PhotoFeedBaseController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedBaseController.h" +#import "PhotoFeedModel.h" + +@implementation PhotoFeedBaseController +{ + UIActivityIndicatorView *_activityIndicatorView; +} + +// -loadView is guaranteed to be called on the main thread and is the appropriate place to +// set up an UIKit objects you may be using. +- (void)loadView +{ + [super loadView]; + + _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + + _photoFeed = [[PhotoFeedModel alloc] initWithPhotoFeedModelType:PhotoFeedModelTypePopular imageSize:[self imageSizeForScreenWidth]]; + [self refreshFeed]; + + CGSize boundSize = self.view.bounds.size; + [_activityIndicatorView sizeToFit]; + CGRect refreshRect = _activityIndicatorView.frame; + refreshRect.origin = CGPointMake((boundSize.width - _activityIndicatorView.frame.size.width) / 2.0, + (boundSize.height - _activityIndicatorView.frame.size.height) / 2.0); + _activityIndicatorView.frame = refreshRect; + [self.view addSubview:_activityIndicatorView]; + + self.tableView.allowsSelection = NO; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + self.view.backgroundColor = [UIColor whiteColor]; +} + +- (void)refreshFeed +{ + [_activityIndicatorView startAnimating]; + // small first batch + [_photoFeed refreshFeedWithCompletionBlock:^(NSArray *newPhotos){ + + [_activityIndicatorView stopAnimating]; + + [self.tableView reloadData]; + + // immediately start second larger fetch + [self loadPage]; + + } numResultsToReturn:4]; +} + +- (void)insertNewRows:(NSArray *)newPhotos +{ + NSInteger section = 0; + NSMutableArray *indexPaths = [NSMutableArray array]; + + NSInteger newTotalNumberOfPhotos = [_photoFeed numberOfItemsInFeed]; + NSInteger existingNumberOfPhotos = newTotalNumberOfPhotos - newPhotos.count; + for (NSInteger row = existingNumberOfPhotos; row < newTotalNumberOfPhotos; row++) { + NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:section]; + [indexPaths addObject:path]; + } + [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleLightContent; +} + +- (BOOL)prefersStatusBarHidden +{ + return NO; +} + +- (CGSize)imageSizeForScreenWidth +{ + CGRect screenRect = [[UIScreen mainScreen] bounds]; + CGFloat screenScale = [[UIScreen mainScreen] scale]; + return CGSizeMake(screenRect.size.width * screenScale, screenRect.size.width * screenScale); +} + +#pragma mark - PhotoFeedViewControllerProtocol + +- (void)resetAllData +{ + [_photoFeed clearFeed]; + [self.tableView reloadData]; + [self refreshFeed]; +} + +#pragma mark - Subclassing + +- (UITableView *)tableView +{ + NSAssert(NO, @"Subclasses must override this method"); + return nil; +} + +- (void)loadPage +{ + NSAssert(NO, @"Subclasses must override this method"); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedControllerProtocol.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedControllerProtocol.h new file mode 100644 index 0000000000..df5832939b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedControllerProtocol.h @@ -0,0 +1,13 @@ +// +// PhotoFeedControllerProtocol.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@protocol PhotoFeedControllerProtocol +- (void)resetAllData; +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedListKitViewController.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedListKitViewController.h new file mode 100644 index 0000000000..a1e3e6a338 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedListKitViewController.h @@ -0,0 +1,14 @@ +// +// PhotoFeedListKitViewController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "PhotoFeedControllerProtocol.h" + +@interface PhotoFeedListKitViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedListKitViewController.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedListKitViewController.m new file mode 100644 index 0000000000..997e9451b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedListKitViewController.m @@ -0,0 +1,118 @@ +// +// PhotoFeedListKitViewController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedListKitViewController.h" +#import +#import "PhotoFeedModel.h" +#import "PhotoFeedSectionController.h" +#import "RefreshingSectionControllerType.h" + +@interface PhotoFeedListKitViewController () +@property (nonatomic, strong) IGListAdapter *listAdapter; +@property (nonatomic, strong) PhotoFeedModel *photoFeed; +@property (nonatomic, strong, readonly) ASCollectionNode *collectionNode; +@property (nonatomic, strong, readonly) UIActivityIndicatorView *spinner; +@property (nonatomic, strong, readonly) UIRefreshControl *refreshCtrl; +@end + +@implementation PhotoFeedListKitViewController +@synthesize spinner = _spinner; + +- (instancetype)init +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionNode *node = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + if (self = [super initWithNode:node]) { + self.navigationItem.title = @"ListKit"; + + CGRect screenRect = [[UIScreen mainScreen] bounds]; + CGFloat screenScale = [[UIScreen mainScreen] scale]; + CGSize screenWidthImageSize = CGSizeMake(screenRect.size.width * screenScale, screenRect.size.width * screenScale); + _photoFeed = [[PhotoFeedModel alloc] initWithPhotoFeedModelType:PhotoFeedModelTypePopular imageSize:screenWidthImageSize]; + + IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; + _listAdapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:self workingRangeSize:0]; + _listAdapter.dataSource = self; + [_listAdapter setASDKCollectionNode:self.collectionNode]; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.collectionNode.view.alwaysBounceVertical = YES; + _refreshCtrl = [[UIRefreshControl alloc] init]; + [_refreshCtrl addTarget:self action:@selector(refreshFeed) forControlEvents:UIControlEventValueChanged]; + [self.collectionNode.view addSubview:_refreshCtrl]; + _spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; +} + +- (ASCollectionNode *)collectionNode +{ + return self.node; +} + +- (void)resetAllData +{ + // nop, not used currently +} + +- (void)refreshFeed +{ + // Ask the first section controller to do the refreshing. + id secCtrl = [self.listAdapter sectionControllerForObject:self.photoFeed]; + if ([(NSObject*)secCtrl conformsToProtocol:@protocol(RefreshingSectionControllerType)]) { + [secCtrl refreshContentWithCompletion:^{ + [self.refreshCtrl endRefreshing]; + }]; + } +} + +- (UIActivityIndicatorView *)spinner +{ + if (_spinner == nil) { + _spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [_spinner startAnimating]; + } + return _spinner; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleLightContent; +} + +- (BOOL)prefersStatusBarHidden +{ + return NO; +} + +#pragma mark - IGListAdapterDataSource + +- (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter +{ + return @[ self.photoFeed ]; +} + +- (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter +{ + return self.spinner; +} + +- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object +{ + if ([object isKindOfClass:[PhotoFeedModel class]]) { + return [[PhotoFeedSectionController alloc] init]; + } else { + ASDisplayNodeFailAssert(@"Only supports objects of class PhotoFeedModel."); + return nil; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedModel.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedModel.h new file mode 100644 index 0000000000..a71b830b37 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedModel.h @@ -0,0 +1,37 @@ +// +// PhotoFeedModel.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoModel.h" +#import + +typedef NS_ENUM(NSInteger, PhotoFeedModelType) { + PhotoFeedModelTypePopular, + PhotoFeedModelTypeLocation, + PhotoFeedModelTypeUserPhotos +}; + +@interface PhotoFeedModel : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPhotoFeedModelType:(PhotoFeedModelType)type imageSize:(CGSize)size NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) NSArray *photos; + +- (NSUInteger)totalNumberOfPhotos; +- (NSUInteger)numberOfItemsInFeed; +- (PhotoModel *)objectAtIndex:(NSUInteger)index; +- (NSInteger)indexOfPhotoModel:(PhotoModel *)photoModel; + +- (void)updatePhotoFeedModelTypeUserId:(NSUInteger)userID; + +- (void)clearFeed; +- (void)requestPageWithCompletionBlock:(void (^)(NSArray *))block numResultsToReturn:(NSUInteger)numResults; +- (void)refreshFeedWithCompletionBlock:(void (^)(NSArray *))block numResultsToReturn:(NSUInteger)numResults; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedModel.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedModel.m new file mode 100644 index 0000000000..6548f9d373 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedModel.m @@ -0,0 +1,251 @@ +// +// PhotoFeedModel.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedModel.h" +#import "ImageURLModel.h" + +#define unsplash_ENDPOINT_HOST @"https://api.unsplash.com/" +#define unsplash_ENDPOINT_POPULAR @"photos?order_by=popular" +#define unsplash_ENDPOINT_SEARCH @"photos/search?geo=" //latitude,longitude,radius +#define unsplash_ENDPOINT_USER @"photos?user_id=" +#define unsplash_CONSUMER_KEY_PARAM @"&client_id=3b99a69cee09770a4a0bbb870b437dbda53efb22f6f6de63714b71c4df7c9642" // PLEASE REQUEST YOUR OWN UNSPLASH CONSUMER KEY +#define unsplash_IMAGES_PER_PAGE 30 + +@implementation PhotoFeedModel +{ + PhotoFeedModelType _feedType; + + NSMutableArray *_photos; // array of PhotoModel objects + NSMutableArray *_ids; + + CGSize _imageSize; + NSString *_urlString; + NSUInteger _currentPage; + NSUInteger _totalPages; + NSUInteger _totalItems; + BOOL _fetchPageInProgress; + BOOL _refreshFeedInProgress; + NSURLSessionDataTask *_task; + + NSUInteger _userID; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithPhotoFeedModelType:(PhotoFeedModelType)type imageSize:(CGSize)size +{ + self = [super init]; + + if (self) { + _feedType = type; + _imageSize = size; + _photos = [[NSMutableArray alloc] init]; + _ids = [[NSMutableArray alloc] init]; + _currentPage = 0; + + NSString *apiEndpointString; + switch (type) { + case (PhotoFeedModelTypePopular): + apiEndpointString = unsplash_ENDPOINT_POPULAR; + break; + + case (PhotoFeedModelTypeLocation): + apiEndpointString = unsplash_ENDPOINT_SEARCH; + break; + + case (PhotoFeedModelTypeUserPhotos): + apiEndpointString = unsplash_ENDPOINT_USER; + break; + + default: + break; + } + _urlString = [[unsplash_ENDPOINT_HOST stringByAppendingString:apiEndpointString] stringByAppendingString:unsplash_CONSUMER_KEY_PARAM]; + } + + return self; +} + +#pragma mark - Instance Methods + +- (NSArray *)photos +{ + return [_photos copy]; +} + +- (NSUInteger)totalNumberOfPhotos +{ + return _totalItems; +} + +- (NSUInteger)numberOfItemsInFeed +{ + return [_photos count]; +} + +- (PhotoModel *)objectAtIndex:(NSUInteger)index +{ + return [_photos objectAtIndex:index]; +} + +- (NSInteger)indexOfPhotoModel:(PhotoModel *)photoModel +{ + return [_photos indexOfObjectIdenticalTo:photoModel]; +} + +- (void)updatePhotoFeedModelTypeUserId:(NSUInteger)userID +{ + _userID = userID; + + NSString *userString = [NSString stringWithFormat:@"%lu", (long)userID]; + _urlString = [unsplash_ENDPOINT_HOST stringByAppendingString:unsplash_ENDPOINT_USER]; + _urlString = [[_urlString stringByAppendingString:userString] stringByAppendingString:@"&sort=created_at&image_size=3&include_store=store_download&include_states=voted"]; + _urlString = [_urlString stringByAppendingString:unsplash_CONSUMER_KEY_PARAM]; +} + +- (void)clearFeed +{ + _photos = [[NSMutableArray alloc] init]; + _ids = [[NSMutableArray alloc] init]; + _currentPage = 0; + _fetchPageInProgress = NO; + _refreshFeedInProgress = NO; + [_task cancel]; + _task = nil; +} + +- (void)requestPageWithCompletionBlock:(void (^)(NSArray *))block numResultsToReturn:(NSUInteger)numResults +{ + // only one fetch at a time + if (_fetchPageInProgress) { + return; + } else { + _fetchPageInProgress = YES; + [self fetchPageWithCompletionBlock:block numResultsToReturn:numResults]; + } +} + +- (void)refreshFeedWithCompletionBlock:(void (^)(NSArray *))block numResultsToReturn:(NSUInteger)numResults +{ + // only one fetch at a time + if (_refreshFeedInProgress) { + return; + + } else { + _refreshFeedInProgress = YES; + _currentPage = 0; + + // FIXME: blow away any other requests in progress + [self fetchPageWithCompletionBlock:^(NSArray *newPhotos) { + if (block) { + block(newPhotos); + } + _refreshFeedInProgress = NO; + } numResultsToReturn:numResults replaceData:YES]; + } +} + +#pragma mark - Helper Methods + +- (void)fetchPageWithCompletionBlock:(void (^)(NSArray *))block numResultsToReturn:(NSUInteger)numResults +{ + [self fetchPageWithCompletionBlock:block numResultsToReturn:numResults replaceData:NO]; +} + +- (void)fetchPageWithCompletionBlock:(void (^)(NSArray *))block numResultsToReturn:(NSUInteger)numResults replaceData:(BOOL)replaceData +{ + // early return if reached end of pages + if (_totalPages) { + if (_currentPage == _totalPages) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (block) { + block(@[]); + } + }); + return; + } + } + + NSUInteger numPhotos = (numResults < unsplash_IMAGES_PER_PAGE) ? numResults : unsplash_IMAGES_PER_PAGE; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSMutableArray *newPhotos = [NSMutableArray array]; + NSMutableArray *newIDs = [NSMutableArray array]; + + @synchronized(self) { + NSUInteger nextPage = _currentPage + 1; + NSString *imageSizeParam = [ImageURLModel imageParameterForClosestImageSize:_imageSize]; + NSString *urlAdditions = [NSString stringWithFormat:@"&page=%lu&per_page=%lu%@", (unsigned long)nextPage, (long)numPhotos, imageSizeParam]; + NSURL *url = [NSURL URLWithString:[_urlString stringByAppendingString:urlAdditions]]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + _task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + @synchronized(self) { + NSHTTPURLResponse *httpResponse = nil; + if (data && [response isKindOfClass:[NSHTTPURLResponse class]]) { + httpResponse = (NSHTTPURLResponse *)response; + NSArray *objects = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; + + if ([objects isKindOfClass:[NSArray class]]) { + _currentPage = nextPage; + _totalItems = [[httpResponse allHeaderFields][@"x-total"] integerValue]; + _totalPages = _totalItems / unsplash_IMAGES_PER_PAGE; // default per page is 10 + if (_totalItems % unsplash_IMAGES_PER_PAGE != 0) { + _totalPages += 1; + } + + NSArray *photos = objects; + for (NSDictionary *photoDictionary in photos) { + if ([photoDictionary isKindOfClass:[NSDictionary class]]) { + PhotoModel *photo = [[PhotoModel alloc] initWithUnsplashPhoto:photoDictionary]; + if (photo) { + if (replaceData || ![_ids containsObject:photo.photoID]) { + [newPhotos addObject:photo]; + [newIDs addObject:photo.photoID]; + } + } + } + } + } + } + } + + dispatch_async(dispatch_get_main_queue(), ^{ + @synchronized(self) { + if (replaceData) { + _photos = [newPhotos mutableCopy]; + _ids = [newIDs mutableCopy]; + } else { + [_photos addObjectsFromArray:newPhotos]; + [_ids addObjectsFromArray:newIDs]; + } + if (block) { + block(newPhotos); + } + _fetchPageInProgress = NO; + } + }); + }]; + [_task resume]; + } + }); +} + +#pragma mark - IGListDiffable + +- (id)diffIdentifier +{ + return self; +} + +- (BOOL)isEqualToDiffableObject:(id)object +{ + return self == object; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedNodeController.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedNodeController.h new file mode 100644 index 0000000000..e4b8553c08 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedNodeController.h @@ -0,0 +1,14 @@ +// +// PhotoFeedNodeController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedBaseController.h" + +@interface PhotoFeedNodeController : PhotoFeedBaseController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedNodeController.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedNodeController.m new file mode 100644 index 0000000000..8abc80bb53 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedNodeController.m @@ -0,0 +1,105 @@ +// +// PhotoFeedNodeController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedNodeController.h" +#import +#import "Utilities.h" +#import "PhotoModel.h" +#import "PhotoCellNode.h" +#import "PhotoFeedModel.h" + +#define AUTO_TAIL_LOADING_NUM_SCREENFULS 2.5 + +@interface PhotoFeedNodeController () +@property (nonatomic, strong) ASTableNode *tableNode; +@end + +@implementation PhotoFeedNodeController + +#pragma mark - Lifecycle + +// -init is often called off the main thread in ASDK. Therefore it is imperative that no UIKit objects are accessed. +// Examples of common errors include accessing the node’s view or creating a gesture recognizer. +- (instancetype)init +{ + _tableNode = [[ASTableNode alloc] init]; + self = [super initWithNode:_tableNode]; + + if (self) { + self.navigationItem.title = @"ASDK"; + [self.navigationController setNavigationBarHidden:YES]; + + _tableNode.dataSource = self; + _tableNode.delegate = self; + } + + return self; +} + +// -loadView is guaranteed to be called on the main thread and is the appropriate place to +// set up an UIKit objects you may be using. +- (void)loadView +{ + [super loadView]; + + self.tableNode.leadingScreensForBatching = AUTO_TAIL_LOADING_NUM_SCREENFULS; // overriding default of 2.0 +} + +- (void)loadPageWithContext:(ASBatchContext *)context +{ + [self.photoFeed requestPageWithCompletionBlock:^(NSArray *newPhotos){ + + [self insertNewRows:newPhotos]; + if (context) { + [context completeBatchFetching:YES]; + } + } numResultsToReturn:20]; +} + +#pragma mark - Subclassing + +- (UITableView *)tableView +{ + return _tableNode.view; +} + +- (void)loadPage +{ + [self loadPageWithContext:nil]; +} + +#pragma mark - ASTableDataSource methods + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return [self.photoFeed numberOfItemsInFeed]; +} + +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + PhotoModel *photoModel = [self.photoFeed objectAtIndex:indexPath.row]; + // this will be executed on a background thread - important to make sure it's thread safe + ASCellNode *(^ASCellNodeBlock)() = ^ASCellNode *() { + PhotoCellNode *cellNode = [[PhotoCellNode alloc] initWithPhotoObject:photoModel]; + return cellNode; + }; + + return ASCellNodeBlock; +} + +#pragma mark - ASTableDelegate methods + +// Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. +- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + [context beginBatchFetching]; + [self loadPageWithContext:context]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedSectionController.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedSectionController.h new file mode 100644 index 0000000000..f7d820d6e5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedSectionController.h @@ -0,0 +1,24 @@ +// +// PhotoFeedSectionController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import "RefreshingSectionControllerType.h" +#import "ASCollectionSectionController.h" + +@class PhotoFeedModel; + +NS_ASSUME_NONNULL_BEGIN + +@interface PhotoFeedSectionController : ASCollectionSectionController + +@property (nonatomic, strong, nullable) PhotoFeedModel *photoFeed; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedSectionController.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedSectionController.m new file mode 100644 index 0000000000..25e1a1b613 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedSectionController.m @@ -0,0 +1,144 @@ +// +// PhotoFeedSectionController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedSectionController.h" +#import "PhotoFeedModel.h" +#import "PhotoModel.h" +#import "PhotoCellNode.h" +#import "TailLoadingNode.h" +#import "FeedHeaderNode.h" + +@interface PhotoFeedSectionController () +@property (nonatomic, strong) NSString *paginatingSpinner; +@end + +@implementation PhotoFeedSectionController + +- (instancetype)init +{ + if (self = [super init]) { + _paginatingSpinner = @"Paginating Spinner"; + self.supplementaryViewSource = self; + } + return self; +} + +#pragma mark - IGListSectionType + +- (void)didUpdateToObject:(id)object +{ + _photoFeed = object; + [self setItems:_photoFeed.photos animated:NO completion:nil]; +} + +- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index +{ + return [ASIGListSectionControllerMethods cellForItemAtIndex:index sectionController:self]; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index +{ + return [ASIGListSectionControllerMethods sizeForItemAtIndex:index]; +} + +- (void)didSelectItemAtIndex:(NSInteger)index +{ + // nop +} + +#pragma mark - ASSectionController + +- (ASCellNodeBlock)nodeBlockForItemAtIndex:(NSInteger)index +{ + id object = self.items[index]; + // this will be executed on a background thread - important to make sure it's thread safe + ASCellNode *(^nodeBlock)() = nil; + if (object == _paginatingSpinner) { + nodeBlock = ^{ + return [[TailLoadingNode alloc] init]; + }; + } else if ([object isKindOfClass:[PhotoModel class]]) { + PhotoModel *photoModel = object; + nodeBlock = ^{ + PhotoCellNode *cellNode = [[PhotoCellNode alloc] initWithPhotoObject:photoModel]; + return cellNode; + }; + } + + return nodeBlock; +} + +- (void)beginBatchFetchWithContext:(ASBatchContext *)context +{ + dispatch_async(dispatch_get_main_queue(), ^{ + // Immediately add the loading spinner if needed. + if (self.items.count > 0) { + NSArray *newItems = [self.items arrayByAddingObject:_paginatingSpinner]; + [self setItems:newItems animated:NO completion:nil]; + } + + // Push to next runloop to give time to insert the spinner + dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // Start the fetch, then update the items (removing the spinner) when they are loaded. + [_photoFeed requestPageWithCompletionBlock:^(NSArray *newPhotos){ + [self setItems:_photoFeed.photos animated:NO completion:^{ + [context completeBatchFetching:YES]; + }]; + } numResultsToReturn:20]; + }); + }); + }); +} + +#pragma mark - RefreshingSectionControllerType + +- (void)refreshContentWithCompletion:(void(^)())completion +{ + [_photoFeed refreshFeedWithCompletionBlock:^(NSArray *addedItems) { + [self setItems:_photoFeed.photos animated:YES completion:completion]; + } numResultsToReturn:4]; +} + +#pragma mark - ASSupplementaryNodeSource + +- (ASCellNodeBlock)nodeBlockForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + ASDisplayNodeAssert([elementKind isEqualToString:UICollectionElementKindSectionHeader], nil); + return ^{ + return [[FeedHeaderNode alloc] init]; + }; +} + +- (ASSizeRange)sizeRangeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) { + return ASSizeRangeUnconstrained; + } else { + return ASSizeRangeZero; + } +} + +#pragma mark - IGListSupplementaryViewSource + +- (NSArray *)supportedElementKinds +{ + return @[ UICollectionElementKindSectionHeader ]; +} + +- (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + return [ASIGListSupplementaryViewSourceMethods viewForSupplementaryElementOfKind:elementKind atIndex:index sectionController:self]; +} + +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + return [ASIGListSupplementaryViewSourceMethods sizeForSupplementaryViewOfKind:elementKind atIndex:index]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedViewController.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedViewController.h new file mode 100644 index 0000000000..14295d0bbc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedViewController.h @@ -0,0 +1,14 @@ +// +// PhotoFeedViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedBaseController.h" + +@interface PhotoFeedViewController : PhotoFeedBaseController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedViewController.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedViewController.m new file mode 100644 index 0000000000..c3f9a4aefe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoFeedViewController.m @@ -0,0 +1,104 @@ +// +// PhotoFeedViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoFeedViewController.h" +#import "Utilities.h" +#import "PhotoTableViewCell.h" +#import "PhotoFeedModel.h" + +#define AUTO_TAIL_LOADING_NUM_SCREENFULS 2.5 + +@interface PhotoFeedViewController () +@end + +@implementation PhotoFeedViewController +{ + UITableView *_tableView; +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super initWithNibName:nil bundle:nil]; + + if (self) { + self.navigationItem.title = @"UIKit"; + [self.navigationController setNavigationBarHidden:YES]; + + _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + _tableView.delegate = self; + _tableView.dataSource = self; + } + + return self; +} + +// anything involving the view should go here, not init +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_tableView]; + _tableView.frame = self.view.bounds; + [_tableView registerClass:[PhotoTableViewCell class] forCellReuseIdentifier:@"photoCell"]; +} + +#pragma mark - Subclassing + +- (UITableView *)tableView +{ + return _tableView; +} + +- (void)loadPage +{ + [self.photoFeed requestPageWithCompletionBlock:^(NSArray *newPhotos){ + [self insertNewRows:newPhotos]; + } numResultsToReturn:20]; +} + +#pragma mark - UITableViewDataSource methods + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return [self.photoFeed numberOfItemsInFeed]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + PhotoTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"photoCell" forIndexPath:indexPath]; + [cell updateCellWithPhotoObject:[self.photoFeed objectAtIndex:indexPath.row]]; + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + PhotoModel *photo = [self.photoFeed objectAtIndex:indexPath.row]; + return [PhotoTableViewCell heightForPhotoModel:photo withWidth:self.view.bounds.size.width]; +} + +#pragma mark - UITableViewDelegate methods + +// table automatic tail loading +-(void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + CGFloat currentOffSetY = scrollView.contentOffset.y; + CGFloat contentHeight = scrollView.contentSize.height; + CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; + + CGFloat screenfullsBeforeBottom = (contentHeight - currentOffSetY) / screenHeight; + if (screenfullsBeforeBottom < AUTO_TAIL_LOADING_NUM_SCREENFULS) { + [self loadPage]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoModel.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoModel.h new file mode 100644 index 0000000000..95c1a8130c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoModel.h @@ -0,0 +1,33 @@ +// +// PhotoModel.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "UserModel.h" +#import + +@interface PhotoModel : NSObject + +@property (nonatomic, strong, readonly) NSURL *URL; +@property (nonatomic, strong, readonly) NSString *photoID; +@property (nonatomic, strong, readonly) NSString *uploadDateString; +@property (nonatomic, strong, readonly) NSString *descriptionText; +@property (nonatomic, assign, readonly) NSUInteger likesCount; +@property (nonatomic, strong, readonly) NSString *location; +@property (nonatomic, strong, readonly) UserModel *ownerUserProfile; +@property (nonatomic, assign, readonly) NSUInteger width; +@property (nonatomic, assign, readonly) NSUInteger height; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithUnsplashPhoto:(NSDictionary *)photoDictionary NS_DESIGNATED_INITIALIZER; + +- (NSAttributedString *)descriptionAttributedStringWithFontSize:(CGFloat)size; +- (NSAttributedString *)uploadDateAttributedStringWithFontSize:(CGFloat)size; +- (NSAttributedString *)likesAttributedStringWithFontSize:(CGFloat)size; +- (NSAttributedString *)locationAttributedStringWithFontSize:(CGFloat)size; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoModel.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoModel.m new file mode 100644 index 0000000000..0662e71ae5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoModel.m @@ -0,0 +1,90 @@ +// +// PhotoModel.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoModel.h" +#import "Utilities.h" + +@implementation PhotoModel +{ + NSDictionary *_dictionaryRepresentation; + NSString *_uploadDateRaw; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithUnsplashPhoto:(NSDictionary *)photoDictionary +{ + self = [super init]; + + if (self) { + _dictionaryRepresentation = photoDictionary; + _uploadDateRaw = [photoDictionary objectForKey:@"created_at"]; + _photoID = [photoDictionary objectForKey:@"id"]; + _descriptionText = [photoDictionary valueForKeyPath:@"description"]; + _likesCount = [[photoDictionary objectForKey:@"likes"] integerValue]; + _location = [photoDictionary objectForKey:@"location"]; + + NSString *urlString = [photoDictionary objectForKey:@"urls"][@"regular"]; + _URL = urlString ? [NSURL URLWithString:urlString] : nil; + + _ownerUserProfile = [[UserModel alloc] initWithUnsplashPhoto:photoDictionary]; + _uploadDateString = [NSString elapsedTimeStringSinceDate:_uploadDateRaw]; + + _height = [[photoDictionary objectForKey:@"height"] integerValue]; + _width = [[photoDictionary objectForKey:@"width"] integerValue]; + } + + return self; +} + +#pragma mark - Instance Methods + +- (NSAttributedString *)descriptionAttributedStringWithFontSize:(CGFloat)size +{ + NSString *string = [NSString stringWithFormat:@"%@ %@", self.ownerUserProfile.username, self.descriptionText]; + NSAttributedString *attrString = [NSAttributedString attributedStringWithString:string + fontSize:size + color:[UIColor darkGrayColor] + firstWordColor:[UIColor darkBlueColor]]; + return attrString; +} + +- (NSAttributedString *)uploadDateAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:self.uploadDateString fontSize:size color:[UIColor lightGrayColor] firstWordColor:nil]; +} + +- (NSAttributedString *)likesAttributedStringWithFontSize:(CGFloat)size +{ + NSString *likesString = [NSString stringWithFormat:@"♥︎ %lu likes", (unsigned long)_likesCount]; + + return [NSAttributedString attributedStringWithString:likesString fontSize:size color:[UIColor darkBlueColor] firstWordColor:nil]; +} + +- (NSAttributedString *)locationAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:self.location fontSize:size color:[UIColor lightBlueColor] firstWordColor:nil]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ - %@", _photoID, _descriptionText]; +} + +- (id)diffIdentifier +{ + return self.photoID; +} + +- (BOOL)isEqualToDiffableObject:(id)object +{ + return [self isEqual:object]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoTableViewCell.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoTableViewCell.h new file mode 100644 index 0000000000..47e8e913f7 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoTableViewCell.h @@ -0,0 +1,18 @@ +// +// PhotoTableViewCell.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoModel.h" + +@interface PhotoTableViewCell : UITableViewCell + ++ (CGFloat)heightForPhotoModel:(PhotoModel *)photo withWidth:(CGFloat)width; + +- (void)updateCellWithPhotoObject:(PhotoModel *)photo; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoTableViewCell.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoTableViewCell.m new file mode 100644 index 0000000000..9cc5fa785b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/PhotoTableViewCell.m @@ -0,0 +1,425 @@ +// +// PhotoTableViewCell.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PhotoTableViewCell.h" +#import "Utilities.h" +#import "PINImageView+PINRemoteImage.h" +#import "PINButton+PINRemoteImage.h" + +#define DEBUG_PHOTOCELL_LAYOUT 0 +#define USE_UIKIT_AUTOLAYOUT 1 +#define USE_UIKIT_MANUAL_LAYOUT !USE_UIKIT_AUTOLAYOUT + +#define HEADER_HEIGHT 50 +#define USER_IMAGE_HEIGHT 30 +#define HORIZONTAL_BUFFER 10 +#define VERTICAL_BUFFER 5 +#define FONT_SIZE 14 + +@implementation PhotoTableViewCell +{ + PhotoModel *_photoModel; + + UIImageView *_userAvatarImageView; + UIImageView *_photoImageView; + UILabel *_userNameLabel; + UILabel *_photoLocationLabel; + UILabel *_photoTimeIntervalSincePostLabel; + UILabel *_photoLikesLabel; + UILabel *_photoDescriptionLabel; + + NSLayoutConstraint *_userNameYPositionWithPhotoLocation; + NSLayoutConstraint *_userNameYPositionWithoutPhotoLocation; + NSLayoutConstraint *_photoLocationYPosition; +} + +#pragma mark - Class Methods + ++ (CGFloat)heightForPhotoModel:(PhotoModel *)photo withWidth:(CGFloat)width; +{ + CGFloat photoHeight = width; + + UIFont *font = [UIFont systemFontOfSize:FONT_SIZE]; + CGFloat likesHeight = roundf([font lineHeight]); + + NSAttributedString *descriptionAttrString = [photo descriptionAttributedStringWithFontSize:FONT_SIZE]; + CGFloat availableWidth = (width - HORIZONTAL_BUFFER * 2); + CGFloat descriptionHeight = [descriptionAttrString boundingRectWithSize:CGSizeMake(availableWidth, CGFLOAT_MAX) + options:NSStringDrawingUsesLineFragmentOrigin + context:nil].size.height; + + return HEADER_HEIGHT + photoHeight + likesHeight + descriptionHeight + (4 * VERTICAL_BUFFER); +} + +#pragma mark - Lifecycle + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + + if (self) { + _userAvatarImageView = [[UIImageView alloc] init]; + _photoImageView = [[UIImageView alloc] init]; + _photoImageView.contentMode = UIViewContentModeScaleAspectFill; + _userNameLabel = [[UILabel alloc] init]; + _photoLocationLabel = [[UILabel alloc] init]; + _photoTimeIntervalSincePostLabel = [[UILabel alloc] init]; + _photoLikesLabel = [[UILabel alloc] init]; + _photoDescriptionLabel = [[UILabel alloc] init]; + _photoDescriptionLabel.numberOfLines = 3; + + [self addSubview:_userAvatarImageView]; + [self addSubview:_photoImageView]; + [self addSubview:_userNameLabel]; + [self addSubview:_photoLocationLabel]; + [self addSubview:_photoTimeIntervalSincePostLabel]; + [self addSubview:_photoLikesLabel]; + [self addSubview:_photoDescriptionLabel]; + +#if USE_UIKIT_AUTOLAYOUT + [_userAvatarImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [_photoImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [_userNameLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + [_photoLocationLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + [_photoTimeIntervalSincePostLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + [_photoLikesLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + [_photoDescriptionLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [self setupConstraints]; + [self updateConstraints]; +#endif + +#if DEBUG_PHOTOCELL_LAYOUT + _userAvatarImageView.backgroundColor = [UIColor greenColor]; + _userNameLabel.backgroundColor = [UIColor greenColor]; + _photoLocationLabel.backgroundColor = [UIColor greenColor]; + _photoTimeIntervalSincePostLabel.backgroundColor = [UIColor greenColor]; + _photoDescriptionLabel.backgroundColor = [UIColor greenColor]; + _photoLikesLabel.backgroundColor = [UIColor greenColor]; +#endif + } + + return self; +} + +-(void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; +} + +- (void)setupConstraints +{ + // _userAvatarImageView + [self addConstraint:[NSLayoutConstraint constraintWithItem:_userAvatarImageView + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView.superview + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:HORIZONTAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_userAvatarImageView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView.superview + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:HORIZONTAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_userAvatarImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:0.0 + constant:USER_IMAGE_HEIGHT]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_userAvatarImageView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0]]; + + // _userNameLabel + [self addConstraint:[NSLayoutConstraint constraintWithItem:_userNameLabel + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:HORIZONTAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_userNameLabel + attribute:NSLayoutAttributeRight + relatedBy:NSLayoutRelationLessThanOrEqual + toItem:_photoTimeIntervalSincePostLabel + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:-HORIZONTAL_BUFFER]]; + + _userNameYPositionWithoutPhotoLocation = [NSLayoutConstraint constraintWithItem:_userNameLabel + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + [self addConstraint:_userNameYPositionWithoutPhotoLocation]; + + _userNameYPositionWithPhotoLocation = [NSLayoutConstraint constraintWithItem:_userNameLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:-2]; + _userNameYPositionWithPhotoLocation.active = NO; + [self addConstraint:_userNameYPositionWithPhotoLocation]; + + // _photoLocationLabel + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoLocationLabel + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:HORIZONTAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoLocationLabel + attribute:NSLayoutAttributeRight + relatedBy:NSLayoutRelationLessThanOrEqual + toItem:_photoTimeIntervalSincePostLabel + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:-HORIZONTAL_BUFFER]]; + + _photoLocationYPosition = [NSLayoutConstraint constraintWithItem:_photoLocationLabel + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:2]; + _photoLocationYPosition.active = NO; + [self addConstraint:_photoLocationYPosition]; + + // _photoTimeIntervalSincePostLabel + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoTimeIntervalSincePostLabel + attribute:NSLayoutAttributeRight + relatedBy:NSLayoutRelationEqual + toItem:_photoTimeIntervalSincePostLabel.superview + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:-HORIZONTAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoTimeIntervalSincePostLabel + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:_userAvatarImageView + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]]; + + // _photoImageView + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoImageView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_photoImageView.superview + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:HEADER_HEIGHT]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoImageView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:_photoImageView + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0]]; + + // _photoLikesLabel + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoLikesLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_photoImageView + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:VERTICAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoLikesLabel + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:_photoLikesLabel.superview + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:HORIZONTAL_BUFFER]]; + + // _photoDescriptionLabel + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoDescriptionLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_photoLikesLabel + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:VERTICAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoDescriptionLabel + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:_photoDescriptionLabel.superview + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:HORIZONTAL_BUFFER]]; + + [self addConstraint:[NSLayoutConstraint constraintWithItem:_photoDescriptionLabel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:_photoDescriptionLabel.superview + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:-HORIZONTAL_BUFFER]]; +} + +- (void)updateConstraints +{ + [super updateConstraints]; + + if (_photoLocationLabel.attributedText) { + _userNameYPositionWithoutPhotoLocation.active = NO; + _userNameYPositionWithPhotoLocation.active = YES; + _photoLocationYPosition.active = YES; + } else { + _userNameYPositionWithoutPhotoLocation.active = YES; + _userNameYPositionWithPhotoLocation.active = NO; + _photoLocationYPosition.active = NO; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + +#if USE_UIKIT_PROGRAMMATIC_LAYOUT + CGSize boundsSize = self.bounds.size; + + CGRect rect = CGRectMake(HORIZONTAL_BUFFER, (HEADER_HEIGHT - USER_IMAGE_HEIGHT) / 2.0, + USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + _userAvatarImageView.frame = rect; + + rect.size = _photoTimeIntervalSincePostLabel.bounds.size; + rect.origin.x = boundsSize.width - HORIZONTAL_BUFFER - rect.size.width; + rect.origin.y = (HEADER_HEIGHT - rect.size.height) / 2.0; + _photoTimeIntervalSincePostLabel.frame = rect; + + CGFloat availableWidth = CGRectGetMinX(_photoTimeIntervalSincePostLabel.frame) - HORIZONTAL_BUFFER; + rect.size = _userNameLabel.bounds.size; + rect.size.width = MIN(availableWidth, rect.size.width); + + rect.origin.x = HORIZONTAL_BUFFER + USER_IMAGE_HEIGHT + HORIZONTAL_BUFFER; + + if (_photoLocationLabel.attributedText) { + CGSize locationSize = _photoLocationLabel.bounds.size; + locationSize.width = MIN(availableWidth, locationSize.width); + + rect.origin.y = (HEADER_HEIGHT - rect.size.height - locationSize.height) / 2.0; + _userNameLabel.frame = rect; + + // FIXME: Name rects at least for this sub-condition + rect.origin.y += rect.size.height; + rect.size = locationSize; + _photoLocationLabel.frame = rect; + } else { + rect.origin.y = (HEADER_HEIGHT - rect.size.height) / 2.0; + _userNameLabel.frame = rect; + } + + _photoImageView.frame = CGRectMake(0, HEADER_HEIGHT, boundsSize.width, boundsSize.width); + + // FIXME: Make PhotoCellFooterView + rect.size = _photoLikesLabel.bounds.size; + rect.origin = CGPointMake(HORIZONTAL_BUFFER, CGRectGetMaxY(_photoImageView.frame) + VERTICAL_BUFFER); + _photoLikesLabel.frame = rect; + + rect.size = _photoDescriptionLabel.bounds.size; + rect.size.width = MIN(boundsSize.width - HORIZONTAL_BUFFER * 2, rect.size.width); + rect.origin.y = CGRectGetMaxY(_photoLikesLabel.frame) + VERTICAL_BUFFER; + _photoDescriptionLabel.frame = rect; +#endif +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + + _userAvatarImageView.image = nil; + _photoImageView.image = nil; + _userNameLabel.attributedText = nil; + _photoLocationLabel.attributedText = nil; + _photoLocationLabel.frame = CGRectZero; // next cell might not have a _photoLocationLabel + _photoTimeIntervalSincePostLabel.attributedText = nil; + _photoLikesLabel.attributedText = nil; + _photoDescriptionLabel.attributedText = nil; +} + +#pragma mark - Instance Methods + +- (void)updateCellWithPhotoObject:(PhotoModel *)photo +{ + _photoModel = photo; + _userNameLabel.attributedText = [photo.ownerUserProfile usernameAttributedStringWithFontSize:FONT_SIZE]; + _photoTimeIntervalSincePostLabel.attributedText = [photo uploadDateAttributedStringWithFontSize:FONT_SIZE]; + _photoLikesLabel.attributedText = [photo likesAttributedStringWithFontSize:FONT_SIZE]; + _photoDescriptionLabel.attributedText = [photo descriptionAttributedStringWithFontSize:FONT_SIZE]; + + [_userNameLabel sizeToFit]; + [_photoTimeIntervalSincePostLabel sizeToFit]; + [_photoLikesLabel sizeToFit]; + [_photoDescriptionLabel sizeToFit]; + CGRect rect = _photoDescriptionLabel.frame; + CGFloat availableWidth = (self.bounds.size.width - HORIZONTAL_BUFFER * 2); + rect.size = [_photoDescriptionLabel sizeThatFits:CGSizeMake(availableWidth, CGFLOAT_MAX)]; + _photoDescriptionLabel.frame = rect; + + [UIImage downloadImageForURL:photo.URL completion:^(UIImage *image) { + _photoImageView.image = image; + }]; + + [self downloadAndProcessUserAvatarForPhoto:photo]; + + //update location + _photoLocationLabel.attributedText = [photo locationAttributedStringWithFontSize:FONT_SIZE]; + [_photoLocationLabel sizeToFit]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateConstraints]; + [self setNeedsLayout]; + }); +} + +#pragma mark - Helper Methods + +- (void)downloadAndProcessUserAvatarForPhoto:(PhotoModel *)photo +{ + [UIImage downloadImageForURL:photo.URL completion:^(UIImage *image) { + CGSize profileImageSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + _userAvatarImageView.image = [image makeCircularImageWithSize:profileImageSize]; + }]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/RefreshingSectionControllerType.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/RefreshingSectionControllerType.h new file mode 100644 index 0000000000..169a996012 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/RefreshingSectionControllerType.h @@ -0,0 +1,19 @@ +// +// RefreshingSectionControllerType.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol RefreshingSectionControllerType + +- (void)refreshContentWithCompletion:(nullable void(^)())completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Sample.pch b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Sample.pch new file mode 100644 index 0000000000..8c35575c9b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Sample.pch @@ -0,0 +1,15 @@ +// +// ASDKgram.pch +// ASDKgram +// +// Created by Hannah Troisi on 2/26/16. +// Copyright © 2016 Hannah Troisi. All rights reserved. +// + +#ifndef Flickrgram_pch +#define Flickrgram_pch + +#import +#import + +#endif /* Flickrgram_pch */ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TailLoadingNode.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TailLoadingNode.h new file mode 100644 index 0000000000..ae514231f6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TailLoadingNode.h @@ -0,0 +1,17 @@ +// +// TailLoadingNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * A node that shows a UIActivityIndicatorView, useful for putting at the end of a + * list while the next page is loading. + */ +@interface TailLoadingNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TailLoadingNode.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TailLoadingNode.m new file mode 100644 index 0000000000..63c462ab98 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TailLoadingNode.m @@ -0,0 +1,54 @@ +// +// TailLoadingNode.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "TailLoadingNode.h" + +#import "Availability.h" + +@interface TailLoadingNode () +@property (nonatomic, strong) ASDisplayNode *activityIndicatorNode; +@end + +@implementation TailLoadingNode + +- (instancetype)init +{ + if (self = [super init]) { + self.automaticallyManagesSubnodes = YES; + + _activityIndicatorNode = [[ASDisplayNode alloc] initWithViewBlock:^{ + UIActivityIndicatorView *v = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [v startAnimating]; + return v; + }]; + self.style.height = ASDimensionMake(100); + + [self setupYogaLayoutIfNeeded]; + } + return self; +} +#if !YOGA_LAYOUT +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:self.activityIndicatorNode]; +} +#endif + +- (void)setupYogaLayoutIfNeeded +{ +#if YOGA_LAYOUT + [self.style yogaNodeCreateIfNeeded]; + [self.activityIndicatorNode.style yogaNodeCreateIfNeeded]; + [self addYogaChild:self.activityIndicatorNode]; + + self.style.justifyContent = ASStackLayoutJustifyContentCenter; + self.style.alignItems = ASStackLayoutAlignItemsCenter; +#endif +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TextureConfigDelegate.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TextureConfigDelegate.m new file mode 100644 index 0000000000..b6a2ae9717 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/TextureConfigDelegate.m @@ -0,0 +1,37 @@ +// +// TextureConfigDelegate.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface TextureConfigDelegate : NSObject + +@end + +@implementation ASConfiguration (UserProvided) + ++ (ASConfiguration *)textureConfiguration +{ + ASConfiguration *config = [[ASConfiguration alloc] init]; + config.experimentalFeatures = ASExperimentalGraphicsContexts | ASExperimentalTextNode; + config.delegate = [[TextureConfigDelegate alloc] init]; + return config; +} + +@end + +@implementation TextureConfigDelegate + +- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)features +{ + if (features & ASExperimentalGraphicsContexts) { + NSLog(@"Texture activated experimental graphics contexts."); + } +} + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/UserModel.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/UserModel.h new file mode 100644 index 0000000000..299ddb1869 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/UserModel.h @@ -0,0 +1,38 @@ +// +// UserModel.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +@interface UserModel : NSObject + +@property (nonatomic, strong, readonly) NSDictionary *dictionaryRepresentation; +@property (nonatomic, assign, readonly) NSString *userID; +@property (nonatomic, strong, readonly) NSString *username; +@property (nonatomic, strong, readonly) NSString *firstName; +@property (nonatomic, strong, readonly) NSString *lastName; +@property (nonatomic, strong, readonly) NSString *fullName; +@property (nonatomic, strong, readonly) NSString *location; +@property (nonatomic, strong, readonly) NSString *about; +@property (nonatomic, strong, readonly) NSURL *userPicURL; +@property (nonatomic, assign, readonly) NSUInteger photoCount; +@property (nonatomic, assign, readonly) NSUInteger galleriesCount; +@property (nonatomic, assign, readonly) NSUInteger affection; +@property (nonatomic, assign, readonly) NSUInteger friendsCount; +@property (nonatomic, assign, readonly) NSUInteger followersCount; +@property (nonatomic, assign, readonly) BOOL following; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithUnsplashPhoto:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER; + +- (NSAttributedString *)usernameAttributedStringWithFontSize:(CGFloat)size; +- (NSAttributedString *)fullNameAttributedStringWithFontSize:(CGFloat)size; + +- (void)fetchAvatarImageWithCompletionBlock:(void(^)(UserModel *, UIImage *))block; + +- (void)downloadCompleteUserDataWithCompletionBlock:(void(^)(UserModel *))block; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/UserModel.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/UserModel.m new file mode 100644 index 0000000000..53e3626026 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/UserModel.m @@ -0,0 +1,167 @@ +// +// UserModel.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "UserModel.h" +#import "Utilities.h" + +@implementation UserModel +{ + BOOL _fullUserInfoFetchRequested; + BOOL _fullUserInfoFetchDone; + void (^_fullUserInfoCompletionBlock)(UserModel *); +} + +#pragma mark - Lifecycle + +- (instancetype)initWithUnsplashPhoto:(NSDictionary *)dictionary +{ + self = [super init]; + + if (self) { + _fullUserInfoFetchRequested = NO; + _fullUserInfoFetchDone = NO; + + [self loadUserDataFromDictionary:dictionary]; + } + + return self; +} + +#pragma mark - Instance Methods + +- (NSAttributedString *)usernameAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:self.username fontSize:size color:[UIColor darkBlueColor] firstWordColor:nil]; +} + +- (NSAttributedString *)fullNameAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:self.fullName fontSize:size color:[UIColor lightGrayColor] firstWordColor:nil]; +} + +- (void)fetchAvatarImageWithCompletionBlock:(void(^)(UserModel *, UIImage *))block +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + NSURLSessionDataTask *task = [session dataTaskWithURL:_userPicURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (data) { + UIImage *image = [UIImage imageWithData:data]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (block) { + block(self, image); + } + }); + } + }]; + [task resume]; + }); +} + +- (void)downloadCompleteUserDataWithCompletionBlock:(void(^)(UserModel *))block; +{ + if (_fullUserInfoFetchDone) { + NSAssert(!_fullUserInfoCompletionBlock, @"Should not have a waiting block at this point"); + // complete user info fetch complete - excute completion block + if (block) { + block(self); + } + + } else { + NSAssert(!_fullUserInfoCompletionBlock, @"Should not have a waiting block at this point"); + // set completion block + _fullUserInfoCompletionBlock = block; + + if (!_fullUserInfoFetchRequested) { + // if fetch not in progress, beging + [self fetchCompleteUserData]; + } + } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@", self.dictionaryRepresentation]; +} + +#pragma mark - Helper Methods + +- (void)fetchCompleteUserData +{ + _fullUserInfoFetchRequested = YES; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // fetch JSON data from server + NSString *urlString = [NSString stringWithFormat:@"https://api.500px.com/v1/users/show?id=%@&consumer_key=Fi13GVb8g53sGvHICzlram7QkKOlSDmAmp9s9aqC", _userID]; + + NSURL *url = [NSURL URLWithString:urlString]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (data) { + NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + + // parse JSON data + if ([response isKindOfClass:[NSDictionary class]]) { + [self loadUserDataFromDictionary:response]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + _fullUserInfoFetchDone = YES; + + if (_fullUserInfoCompletionBlock) { + _fullUserInfoCompletionBlock(self); + + // IT IS ESSENTIAL to nil the block, as it retains a view controller BECAUSE it uses an instance variable which + // means that self is retained. It could continue to live on forever + // If we don't release this. + _fullUserInfoCompletionBlock = nil; + } + }); + } + }]; + [task resume]; + }); +} + +- (void)loadUserDataFromDictionary:(NSDictionary *)dictionary +{ + NSDictionary *userDictionary = [dictionary objectForKey:@"user"]; + if (![userDictionary isKindOfClass:[NSDictionary class]]) { + return; + } + + _userID = [self guardJSONElement:[userDictionary objectForKey:@"id"]]; + _username = [[self guardJSONElement:[userDictionary objectForKey:@"username"]] lowercaseString]; + + if (_username == nil) { + _username = @"Anonymous"; + } + + _firstName = [self guardJSONElement:[userDictionary objectForKey:@"first_name"]]; + _lastName = [self guardJSONElement:[userDictionary objectForKey:@"last_name"]]; + _fullName = [self guardJSONElement:[userDictionary objectForKey:@"name"]]; + _location = [self guardJSONElement:[userDictionary objectForKey:@"location"]]; + _about = [self guardJSONElement:[userDictionary objectForKey:@"bio"]]; + _photoCount = [[self guardJSONElement:[userDictionary objectForKey:@"total_photos"]] integerValue]; + _galleriesCount = [[self guardJSONElement:[userDictionary objectForKey:@"total_collections"]] integerValue]; + _dictionaryRepresentation = userDictionary; + + NSString *urlString = [self guardJSONElement:[userDictionary objectForKey:@"profile_image"][@"medium"]]; + _userPicURL = urlString ? [NSURL URLWithString:urlString] : nil; + +} + +- (id)guardJSONElement:(id)element +{ + return (element == [NSNull null]) ? nil : element; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Utilities.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Utilities.h new file mode 100644 index 0000000000..63f2ab0553 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Utilities.h @@ -0,0 +1,40 @@ +// +// Utilities.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +@interface UIColor (Additions) + ++ (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 diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Utilities.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Utilities.m new file mode 100644 index 0000000000..732c5f8171 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/Utilities.m @@ -0,0 +1,294 @@ +// +// Utilities.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "Utilities.h" + +#define StrokeRoundedImages 0 + +#define IsDigit(v) (v >= '0' && v <= '9') + +static time_t parseRfc3339ToTimeT(const char *string) +{ + int dy, dm, dd; + int th, tm, ts; + int oh, om, osign; + char current; + + if (!string) + return (time_t)0; + + // date + if (sscanf(string, "%04d-%02d-%02d", &dy, &dm, &dd) == 3) { + string += 10; + + if (*string++ != 'T') + return (time_t)0; + + // time + if (sscanf(string, "%02d:%02d:%02d", &th, &tm, &ts) == 3) { + string += 8; + + current = *string; + + // optional: second fraction + if (current == '.') { + ++string; + while(IsDigit(*string)) + ++string; + + current = *string; + } + + if (current == 'Z') { + oh = om = 0; + osign = 1; + } else if (current == '-') { + ++string; + if (sscanf(string, "%02d:%02d", &oh, &om) != 2) + return (time_t)0; + osign = -1; + } else if (current == '+') { + ++string; + if (sscanf(string, "%02d:%02d", &oh, &om) != 2) + return (time_t)0; + osign = 1; + } else { + return (time_t)0; + } + + struct tm timeinfo; + timeinfo.tm_wday = timeinfo.tm_yday = 0; + timeinfo.tm_zone = NULL; + timeinfo.tm_isdst = -1; + + timeinfo.tm_year = dy - 1900; + timeinfo.tm_mon = dm - 1; + timeinfo.tm_mday = dd; + + timeinfo.tm_hour = th; + timeinfo.tm_min = tm; + timeinfo.tm_sec = ts; + + // convert to utc + return timegm(&timeinfo) - (((oh * 60 * 60) + (om * 60)) * osign); + } + } + + return (time_t)0; +} + +static NSDate *parseRfc3339ToNSDate(NSString *rfc3339DateTimeString) +{ + time_t t = parseRfc3339ToTimeT([rfc3339DateTimeString cStringUsingEncoding:NSUTF8StringEncoding]); + return [NSDate dateWithTimeIntervalSince1970:t]; +} + + +@implementation UIColor (Additions) + ++ (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 +{ + return parseRfc3339ToNSDate(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/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/WindowWithStatusBarUnderlay.h b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/WindowWithStatusBarUnderlay.h new file mode 100644 index 0000000000..d9fb31778d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/WindowWithStatusBarUnderlay.h @@ -0,0 +1,14 @@ +// +// WindowWithStatusBarUnderlay.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +// this subclass is neccessary to make the status bar have an opaque, colored background +@interface WindowWithStatusBarUnderlay : UIWindow + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/WindowWithStatusBarUnderlay.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/WindowWithStatusBarUnderlay.m new file mode 100644 index 0000000000..91670c2985 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/WindowWithStatusBarUnderlay.m @@ -0,0 +1,41 @@ +// +// WindowWithStatusBarUnderlay.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#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 darkBlueColor]; + [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/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/main.m b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/main.m new file mode 100644 index 0000000000..fb6be69952 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/main.m @@ -0,0 +1,16 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/camera.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/camera.png new file mode 100644 index 0000000000..2eeecba825 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/camera.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/camera@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/camera@2x.png new file mode 100644 index 0000000000..c1ea4ab857 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/camera@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/cameraRaw.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/cameraRaw.png new file mode 100644 index 0000000000..dbf13aa13d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/cameraRaw.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/earth.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/earth.png new file mode 100644 index 0000000000..c182ea5565 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/earth.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/earth@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/earth@2x.png new file mode 100644 index 0000000000..b8049a5004 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/earth@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/home.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/home.png new file mode 100644 index 0000000000..b88cd66a4b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/home.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/home@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/home@2x.png new file mode 100644 index 0000000000..838e660097 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/home@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/homeRaw.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/homeRaw.png new file mode 100644 index 0000000000..09aa24c157 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/homeRaw.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profile.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profile.png new file mode 100644 index 0000000000..d885b3aedf Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profile.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profile@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profile@2x.png new file mode 100644 index 0000000000..81352fe0cb Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profile@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profileRaw.png b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profileRaw.png new file mode 100644 index 0000000000..0d2894d0ab Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/Sample/tabBarIcons/profileRaw.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/camera.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/camera.png new file mode 100644 index 0000000000..2eeecba825 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/camera.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/camera@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/camera@2x.png new file mode 100644 index 0000000000..c1ea4ab857 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/camera@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/cameraRaw.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/cameraRaw.png new file mode 100644 index 0000000000..dbf13aa13d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/cameraRaw.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/earth.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/earth.png new file mode 100644 index 0000000000..c182ea5565 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/earth.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/earth@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/earth@2x.png new file mode 100644 index 0000000000..b8049a5004 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/earth@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/home.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/home.png new file mode 100644 index 0000000000..b88cd66a4b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/home.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/home@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/home@2x.png new file mode 100644 index 0000000000..838e660097 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/home@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/homeRaw.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/homeRaw.png new file mode 100644 index 0000000000..09aa24c157 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/homeRaw.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profile.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profile.png new file mode 100644 index 0000000000..d885b3aedf Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profile.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profile@2x.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profile@2x.png new file mode 100644 index 0000000000..81352fe0cb Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profile@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profileRaw.png b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profileRaw.png new file mode 100644 index 0000000000..0d2894d0ab Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASDKgram/tabBarIcons/profileRaw.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Podfile b/submodules/AsyncDisplayKit/examples/ASMapNode/Podfile new file mode 100644 index 0000000000..08d1b7add6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..48093ffda1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,388 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 5CF3EF5E344946731D4F13F2 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 465082D55CCF1B0CB1AEBACC /* libPods-Sample.a */; }; + 5E5E62841D13F39400D81E38 /* MapHandlerNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E5E62831D13F39400D81E38 /* MapHandlerNode.m */; }; + 694993D21C8B334F00491CA5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D11C8B334F00491CA5 /* main.m */; }; + 694993D51C8B334F00491CA5 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D41C8B334F00491CA5 /* AppDelegate.m */; }; + 694993D81C8B334F00491CA5 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D71C8B334F00491CA5 /* ViewController.m */; }; + 694993DD1C8B334F00491CA5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 694993DC1C8B334F00491CA5 /* Assets.xcassets */; }; + 694993E01C8B334F00491CA5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */; }; + 905C815E1D362E9400EA2625 /* CustomMapAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 905C815D1D362E9400EA2625 /* CustomMapAnnotation.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 15AD337503831C4D33FF8B3A /* 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 = ""; }; + 465082D55CCF1B0CB1AEBACC /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E5E62821D13F39400D81E38 /* MapHandlerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapHandlerNode.h; sourceTree = ""; }; + 5E5E62831D13F39400D81E38 /* MapHandlerNode.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = MapHandlerNode.m; sourceTree = ""; tabWidth = 2; }; + 694993CD1C8B334F00491CA5 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 694993D11C8B334F00491CA5 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 694993D31C8B334F00491CA5 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 694993D41C8B334F00491CA5 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 694993D61C8B334F00491CA5 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 694993D71C8B334F00491CA5 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 694993DC1C8B334F00491CA5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 694993DF1C8B334F00491CA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 694993E11C8B334F00491CA5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 905C815C1D362E9400EA2625 /* CustomMapAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomMapAnnotation.h; sourceTree = ""; }; + 905C815D1D362E9400EA2625 /* CustomMapAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomMapAnnotation.m; sourceTree = ""; }; + 97482F27BE2F7583EFE1BC2C /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 694993CA1C8B334F00491CA5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CF3EF5E344946731D4F13F2 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0DFDB4376BA084DAC7C1976E /* Pods */ = { + isa = PBXGroup; + children = ( + 15AD337503831C4D33FF8B3A /* Pods-Sample.debug.xcconfig */, + 97482F27BE2F7583EFE1BC2C /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 478C8D7C412DCBDFE14640D8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 465082D55CCF1B0CB1AEBACC /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 694993C41C8B334F00491CA5 = { + isa = PBXGroup; + children = ( + 694993CF1C8B334F00491CA5 /* Sample */, + 694993CE1C8B334F00491CA5 /* Products */, + 0DFDB4376BA084DAC7C1976E /* Pods */, + 478C8D7C412DCBDFE14640D8 /* Frameworks */, + ); + sourceTree = ""; + }; + 694993CE1C8B334F00491CA5 /* Products */ = { + isa = PBXGroup; + children = ( + 694993CD1C8B334F00491CA5 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 694993CF1C8B334F00491CA5 /* Sample */ = { + isa = PBXGroup; + children = ( + 694993D31C8B334F00491CA5 /* AppDelegate.h */, + 694993D41C8B334F00491CA5 /* AppDelegate.m */, + 694993D61C8B334F00491CA5 /* ViewController.h */, + 694993D71C8B334F00491CA5 /* ViewController.m */, + 5E5E62821D13F39400D81E38 /* MapHandlerNode.h */, + 5E5E62831D13F39400D81E38 /* MapHandlerNode.m */, + 905C815C1D362E9400EA2625 /* CustomMapAnnotation.h */, + 905C815D1D362E9400EA2625 /* CustomMapAnnotation.m */, + 694993DC1C8B334F00491CA5 /* Assets.xcassets */, + 694993D01C8B334F00491CA5 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 694993D01C8B334F00491CA5 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 694993E11C8B334F00491CA5 /* Info.plist */, + 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */, + 694993D11C8B334F00491CA5 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 694993CC1C8B334F00491CA5 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 694993E41C8B334F00491CA5 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 80035273449C25F4B2E1454F /* [CP] Check Pods Manifest.lock */, + 694993C91C8B334F00491CA5 /* Sources */, + 694993CA1C8B334F00491CA5 /* Frameworks */, + 694993CB1C8B334F00491CA5 /* Resources */, + 06EE2E0ABEB6289D4775A867 /* [CP] Copy Pods Resources */, + 23FC03B282CBD9014D868DF6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 694993CD1C8B334F00491CA5 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 694993C51C8B334F00491CA5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = AsyncDisplayKit; + TargetAttributes = { + 694993CC1C8B334F00491CA5 = { + CreatedOnToolsVersion = 7.2.1; + }; + }; + }; + buildConfigurationList = 694993C81C8B334F00491CA5 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 694993C41C8B334F00491CA5; + productRefGroup = 694993CE1C8B334F00491CA5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 694993CC1C8B334F00491CA5 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 694993CB1C8B334F00491CA5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 694993E01C8B334F00491CA5 /* LaunchScreen.storyboard in Resources */, + 694993DD1C8B334F00491CA5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 06EE2E0ABEB6289D4775A867 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 23FC03B282CBD9014D868DF6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 80035273449C25F4B2E1454F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 694993C91C8B334F00491CA5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5E5E62841D13F39400D81E38 /* MapHandlerNode.m in Sources */, + 694993D81C8B334F00491CA5 /* ViewController.m in Sources */, + 694993D51C8B334F00491CA5 /* AppDelegate.m in Sources */, + 694993D21C8B334F00491CA5 /* main.m in Sources */, + 905C815E1D362E9400EA2625 /* CustomMapAnnotation.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 694993DF1C8B334F00491CA5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 694993E21C8B334F00491CA5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 694993E31C8B334F00491CA5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 694993E51C8B334F00491CA5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 15AD337503831C4D33FF8B3A /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 694993E61C8B334F00491CA5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 97482F27BE2F7583EFE1BC2C /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 694993C81C8B334F00491CA5 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 694993E21C8B334F00491CA5 /* Debug */, + 694993E31C8B334F00491CA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 694993E41C8B334F00491CA5 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 694993E51C8B334F00491CA5 /* Debug */, + 694993E61C8B334F00491CA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 694993C51C8B334F00491CA5 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..c00064c54d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/AppDelegate.h new file mode 100644 index 0000000000..8d58a13cbe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/AppDelegate.m new file mode 100644 index 0000000000..cae730c64e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/AppDelegate.m @@ -0,0 +1,30 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "ViewController.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // Override point for customization after application launch. + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[ViewController new]]; + [self.window makeKeyAndVisible]; + + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Contents.json b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/Contents.json new file mode 100644 index 0000000000..273884cba6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "hill.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "hill@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "hill@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill.png b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill.png new file mode 100644 index 0000000000..8998668eb0 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill@2x.png b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill@2x.png new file mode 100644 index 0000000000..d64af0dd9d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill@3x.png b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill@3x.png new file mode 100644 index 0000000000..761c66684a Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Hill.imageset/hill@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/Contents.json new file mode 100644 index 0000000000..f54c1c3b60 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "water.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "water@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "water@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water.png b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water.png new file mode 100644 index 0000000000..cdff6fd035 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water@2x.png b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water@2x.png new file mode 100644 index 0000000000..2cd019f20c Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water@3x.png b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water@3x.png new file mode 100644 index 0000000000..e45cd67f2d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Assets.xcassets/Water.imageset/water@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..f4fc7f7736 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/CustomMapAnnotation.h b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/CustomMapAnnotation.h new file mode 100644 index 0000000000..22e62ab7e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/CustomMapAnnotation.h @@ -0,0 +1,20 @@ +// +// CustomMapAnnotation.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface CustomMapAnnotation : NSObject + +@property (assign, nonatomic) CLLocationCoordinate2D coordinate; +@property (copy, nonatomic, nullable) UIImage *image; +@property (copy, nonatomic, nullable) NSString *title; +@property (copy, nonatomic, nullable) NSString *subtitle; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/CustomMapAnnotation.m b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/CustomMapAnnotation.m new file mode 100644 index 0000000000..c6843dcc3c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/CustomMapAnnotation.m @@ -0,0 +1,14 @@ +// +// CustomMapAnnotation.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "CustomMapAnnotation.h" + +@implementation CustomMapAnnotation + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Info.plist new file mode 100644 index 0000000000..6105445463 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/MapHandlerNode.h b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/MapHandlerNode.h new file mode 100644 index 0000000000..46a4d74686 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/MapHandlerNode.h @@ -0,0 +1,16 @@ +// +// MapHandlerNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface MapHandlerNode : ASDisplayNode + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/MapHandlerNode.m b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/MapHandlerNode.m new file mode 100644 index 0000000000..9226253a10 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/MapHandlerNode.m @@ -0,0 +1,335 @@ +// +// MapHandlerNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "MapHandlerNode.h" +#import "CustomMapAnnotation.h" + +#import +#import + +@interface MapHandlerNode () + +@property (nonatomic, strong) ASEditableTextNode *latEditableNode; +@property (nonatomic, strong) ASEditableTextNode *lonEditableNode; +@property (nonatomic, strong) ASEditableTextNode *deltaLatEditableNode; +@property (nonatomic, strong) ASEditableTextNode *deltaLonEditableNode; +@property (nonatomic, strong) ASButtonNode *updateRegionButton; +@property (nonatomic, strong) ASButtonNode *liveMapToggleButton; +@property (nonatomic, strong) ASMapNode *mapNode; + +@end + +@implementation MapHandlerNode + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + self.automaticallyManagesSubnodes = YES; + + _mapNode = [[ASMapNode alloc] init]; + _mapNode.mapDelegate = self; + + _latEditableNode = [[ASEditableTextNode alloc] init]; + _lonEditableNode = [[ASEditableTextNode alloc] init]; + _deltaLatEditableNode = [[ASEditableTextNode alloc] init]; + _deltaLonEditableNode = [[ASEditableTextNode alloc] init]; + + _updateRegionButton = [[ASButtonNode alloc] init]; + _liveMapToggleButton = [[ASButtonNode alloc] init]; + + UIImage *backgroundImage = [UIImage as_resizableRoundedImageWithCornerRadius:5 + cornerColor:[UIColor whiteColor] + fillColor:[UIColor lightGrayColor]]; + + UIImage *backgroundHiglightedImage = [UIImage as_resizableRoundedImageWithCornerRadius:5 + cornerColor:[UIColor whiteColor] + fillColor:[[UIColor lightGrayColor] colorWithAlphaComponent:0.4] + borderColor:[UIColor lightGrayColor] + borderWidth:2.0]; + + [_updateRegionButton setBackgroundImage:backgroundImage forState:UIControlStateNormal]; + [_updateRegionButton setBackgroundImage:backgroundHiglightedImage forState:UIControlStateHighlighted]; + + [_liveMapToggleButton setBackgroundImage:backgroundImage forState:UIControlStateNormal]; + [_liveMapToggleButton setBackgroundImage:backgroundHiglightedImage forState:UIControlStateHighlighted]; + + _updateRegionButton.contentEdgeInsets = UIEdgeInsetsMake(5, 5, 5, 5); + [_updateRegionButton setTitle:@"Update Region" withFont:nil withColor:[UIColor blueColor] forState:UIControlStateNormal]; + + [_updateRegionButton addTarget:self action:@selector(updateRegion) forControlEvents:ASControlNodeEventTouchUpInside]; + + [_liveMapToggleButton setTitle:[self liveMapStr] withFont:nil withColor:[UIColor blueColor] forState:UIControlStateNormal]; + + [_liveMapToggleButton addTarget:self action:@selector(toggleLiveMap) forControlEvents:ASControlNodeEventTouchUpInside]; + + return self; +} + +- (void)didLoad +{ + [super didLoad]; + + [self configureEditableNodes:_latEditableNode]; + [self configureEditableNodes:_lonEditableNode]; + [self configureEditableNodes:_deltaLatEditableNode]; + [self configureEditableNodes:_deltaLonEditableNode]; + + [self updateLocationTextWithMKCoordinateRegion:_mapNode.region]; + + // avoiding retain cycles + __weak MapHandlerNode *weakSelf = self; + + self.mapNode.imageForStaticMapAnnotationBlock = ^UIImage *(id annotation, CGPoint *centerOffset){ + MapHandlerNode *grabbedSelf = weakSelf; + if (grabbedSelf) { + if ([annotation isKindOfClass:[CustomMapAnnotation class]]) { + CustomMapAnnotation *customAnnotation = (CustomMapAnnotation *)annotation; + return customAnnotation.image; + } + } + return nil; + }; + + [self addAnnotations]; +} + +/** + * ------------------------------------ASStackLayoutSpec----------------------------------- + * | ---------------------------------ASInsetLayoutSpec-------------------------------- | + * | | ------------------------------ASStackLayoutSpec----------------------------- | | + * | | | ---------------------------ASStackLayoutSpec-------------------------- | | | + * | | | | -----------------ASStackLayoutSpec---------------- | | | | + * | | | | | --------------ASStackLayoutSpec------------- | | | | | + * | | | | | | ASEditableTextNode ASEditableTextNode | | | | | | + * | | | | | -------------------------------------------- | | | | | + * | | | | | --------------ASStackLayoutSpec------------- | ASButtonNode | | | | + * | | | | | | ASEditableTextNode ASEditableTextNode | | | | | | + * | | | | | -------------------------------------------- | | | | | + * | | | | -------------------------------------------------- | | | | + * | | | ---------------------------------------------------------------------- | | | + * | | | ASButtonNode | | | + * | | ---------------------------------------------------------------------------- | | + * | ---------------------------------------------------------------------------------- | + * | ASMapNode | + * ---------------------------------------------------------------------------------------- + * + * This diagram was created by setting a breakpoint on the returned `layoutSpec` + * and calling "po [layoutSpec asciiArtString]" in the debugger. + */ +#define SPACING 5 +#define HEIGHT 30 +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + _latEditableNode.style.width = ASDimensionMake(@"50%"); + _lonEditableNode.style.width = ASDimensionMake(@"50%"); + _deltaLatEditableNode.style.width = ASDimensionMake(@"50%"); + _deltaLonEditableNode.style.width = ASDimensionMake(@"50%"); + + _liveMapToggleButton.style.maxHeight = ASDimensionMake(HEIGHT); + + _mapNode.style.flexGrow = 1.0; + + ASStackLayoutSpec *lonlatSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:SPACING + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_latEditableNode, _lonEditableNode]]; + + ASStackLayoutSpec *deltaLonlatSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:SPACING + justifyContent:ASStackLayoutJustifyContentSpaceBetween + alignItems:ASStackLayoutAlignItemsCenter + children:@[_deltaLatEditableNode, _deltaLonEditableNode]]; + + ASStackLayoutSpec *lonlatConfigSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:SPACING + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[lonlatSpec, deltaLonlatSpec]]; + + lonlatConfigSpec.style.flexGrow = 1.0; + + ASStackLayoutSpec *dashboardSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:SPACING + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[lonlatConfigSpec, _updateRegionButton]]; + + ASStackLayoutSpec *headerVerticalStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:SPACING + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[dashboardSpec, _liveMapToggleButton]]; + + dashboardSpec.style.flexGrow = 1.0; + + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(40, 10, 0, 10) + child:headerVerticalStack]; + + ASStackLayoutSpec *layoutSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:SPACING + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[insetSpec, _mapNode]]; + + return layoutSpec; +} + +#pragma mark - Button Actions + +- (void)updateRegion +{ + NSNumberFormatter *f = [[NSNumberFormatter alloc] init]; + f.numberStyle = NSNumberFormatterDecimalStyle; + + double const lat = [f numberFromString:_latEditableNode.attributedText.string].doubleValue; + double const lon = [f numberFromString:_lonEditableNode.attributedText.string].doubleValue; + double const deltaLat = [f numberFromString:_deltaLatEditableNode.attributedText.string].doubleValue; + double const deltaLon = [f numberFromString:_deltaLonEditableNode.attributedText.string].doubleValue; + + // TODO: check for valid latitude / longitude coordinates + MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(lat, lon), + MKCoordinateSpanMake(deltaLat, deltaLon)); + + _mapNode.region = region; +} + +- (void)toggleLiveMap +{ + _mapNode.liveMap = !_mapNode.liveMap; + NSString * const liveMapStr = [self liveMapStr]; + [_liveMapToggleButton setTitle:liveMapStr withFont:nil withColor:[UIColor blueColor] forState:UIControlStateNormal]; + [_liveMapToggleButton setTitle:liveMapStr withFont:[UIFont systemFontOfSize:14] withColor:[UIColor blueColor] forState:UIControlStateHighlighted]; +} + +- (void)updateLocationTextWithMKCoordinateRegion:(MKCoordinateRegion)region +{ + _latEditableNode.attributedText = [self attributedStringFromFloat:region.center.latitude]; + _lonEditableNode.attributedText = [self attributedStringFromFloat:region.center.longitude]; + _deltaLatEditableNode.attributedText = [self attributedStringFromFloat:region.span.latitudeDelta]; + _deltaLonEditableNode.attributedText = [self attributedStringFromFloat:region.span.longitudeDelta]; +} + +#pragma mark - Helper Methods + +- (NSAttributedString *)attributedStringFromFloat:(CGFloat)value +{ + return [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%0.3f", value]]; +} + +- (void)addAnnotations { + + MKPointAnnotation *brno = [MKPointAnnotation new]; + brno.coordinate = CLLocationCoordinate2DMake(49.2002211, 16.6078411); + brno.title = @"Brno City"; + + CustomMapAnnotation *atlantic = [CustomMapAnnotation new]; + atlantic.coordinate = CLLocationCoordinate2DMake(38.6442228, -29.9956942); + atlantic.title = @"Atlantic Ocean"; + atlantic.image = [UIImage imageNamed:@"Water"]; + + CustomMapAnnotation *kilimanjaro = [CustomMapAnnotation new]; + kilimanjaro.coordinate = CLLocationCoordinate2DMake(-3.075833, 37.353333); + kilimanjaro.title = @"Kilimanjaro"; + kilimanjaro.image = [UIImage imageNamed:@"Hill"]; + + CustomMapAnnotation *mtblanc = [CustomMapAnnotation new]; + mtblanc.coordinate = CLLocationCoordinate2DMake(45.8325, 6.864444); + mtblanc.title = @"Mont Blanc"; + mtblanc.image = [UIImage imageNamed:@"Hill"]; + + self.mapNode.annotations = @[brno, atlantic, kilimanjaro, mtblanc]; +} + +-(NSString *)liveMapStr +{ + return _mapNode.liveMap ? @"Live Map is ON" : @"Live Map is OFF"; +} + +-(void)configureEditableNodes:(ASEditableTextNode *)node +{ + node.returnKeyType = node == _deltaLonEditableNode ? UIReturnKeyDone : UIReturnKeyNext; + node.delegate = self; +} + +#pragma mark - ASEditableTextNodeDelegate + +- (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if([text isEqualToString:@"\n"]) { + if(editableTextNode == _latEditableNode) + [_lonEditableNode becomeFirstResponder]; + else if(editableTextNode == _lonEditableNode) + [_deltaLatEditableNode becomeFirstResponder]; + else if(editableTextNode == _deltaLatEditableNode) + [_deltaLonEditableNode becomeFirstResponder]; + else if(editableTextNode == _deltaLonEditableNode) { + [_deltaLonEditableNode resignFirstResponder]; + [self updateRegion]; + } + return NO; + } + + NSMutableCharacterSet * s = [NSMutableCharacterSet characterSetWithCharactersInString:@".-"]; + [s formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]]; + [s invert]; + + NSRange r = [text rangeOfCharacterFromSet:s]; + if(r.location != NSNotFound) { + return NO; + } + + if([editableTextNode.attributedText.string rangeOfString:@"."].location != NSNotFound && + [text rangeOfString:@"."].location != NSNotFound) { + return NO; + } + + if ([editableTextNode.attributedText.string rangeOfString:@"-"].location != NSNotFound && + [text rangeOfString:@"-"].location != NSNotFound && + range.location > 0) { + return NO; + } + + return YES; +} + +- (MKAnnotationView *)annotationViewForAnnotation:(id)annotation +{ + MKAnnotationView *av; + + if ([annotation isKindOfClass:[CustomMapAnnotation class]]) { + av = [[MKAnnotationView alloc] init]; + av.centerOffset = CGPointMake(21, 21); + av.image = [(CustomMapAnnotation *)annotation image]; + } else { + av = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; + } + + av.opaque = NO; + + return av; +} + +#pragma mark - MKMapViewDelegate + +- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated +{ + [self updateLocationTextWithMKCoordinateRegion:mapView.region]; +} + +- (MKAnnotationView *)mapView:(MKMapView *)__unused mapView viewForAnnotation:(id)annotation +{ + return [self annotationViewForAnnotation:annotation]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/ViewController.h new file mode 100644 index 0000000000..7054e2d89c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/ViewController.h @@ -0,0 +1,16 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/ViewController.m new file mode 100644 index 0000000000..390532d662 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/ViewController.m @@ -0,0 +1,38 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import "MapHandlerNode.h" + +@interface ViewController () + +@end + +@implementation ViewController + + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super initWithNode:[[MapHandlerNode alloc] init]]; + if (self == nil) { return self; } + + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + self.navigationController.navigationBarHidden = YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/main.m b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASMapNode/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Podfile b/submodules/AsyncDisplayKit/examples/ASViewController/Podfile new file mode 100644 index 0000000000..08d1b7add6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/ASViewController/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e304fdb8e3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,392 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 5CF3EF5E344946731D4F13F2 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 465082D55CCF1B0CB1AEBACC /* libPods-Sample.a */; }; + 694993D21C8B334F00491CA5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D11C8B334F00491CA5 /* main.m */; }; + 694993D51C8B334F00491CA5 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D41C8B334F00491CA5 /* AppDelegate.m */; }; + 694993D81C8B334F00491CA5 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D71C8B334F00491CA5 /* ViewController.m */; }; + 694993DD1C8B334F00491CA5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 694993DC1C8B334F00491CA5 /* Assets.xcassets */; }; + 694993E01C8B334F00491CA5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */; }; + 69DCA5221C8B3D30006FF548 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 69DCA5211C8B3D30006FF548 /* DetailViewController.m */; }; + 69DCA5251C8BE01F006FF548 /* DetailRootNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 69DCA5241C8BE01F006FF548 /* DetailRootNode.m */; }; + 69DCA5281C8BE031006FF548 /* DetailCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 69DCA5271C8BE031006FF548 /* DetailCellNode.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 15AD337503831C4D33FF8B3A /* 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 = ""; }; + 465082D55CCF1B0CB1AEBACC /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 694993CD1C8B334F00491CA5 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 694993D11C8B334F00491CA5 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 694993D31C8B334F00491CA5 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 694993D41C8B334F00491CA5 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 694993D61C8B334F00491CA5 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 694993D71C8B334F00491CA5 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 694993DC1C8B334F00491CA5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 694993DF1C8B334F00491CA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 694993E11C8B334F00491CA5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 69DCA5201C8B3D30006FF548 /* DetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; }; + 69DCA5211C8B3D30006FF548 /* DetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; }; + 69DCA5231C8BE01F006FF548 /* DetailRootNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetailRootNode.h; sourceTree = ""; }; + 69DCA5241C8BE01F006FF548 /* DetailRootNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DetailRootNode.m; sourceTree = ""; }; + 69DCA5261C8BE031006FF548 /* DetailCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetailCellNode.h; sourceTree = ""; }; + 69DCA5271C8BE031006FF548 /* DetailCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DetailCellNode.m; sourceTree = ""; }; + 97482F27BE2F7583EFE1BC2C /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 694993CA1C8B334F00491CA5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CF3EF5E344946731D4F13F2 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0DFDB4376BA084DAC7C1976E /* Pods */ = { + isa = PBXGroup; + children = ( + 15AD337503831C4D33FF8B3A /* Pods-Sample.debug.xcconfig */, + 97482F27BE2F7583EFE1BC2C /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 478C8D7C412DCBDFE14640D8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 465082D55CCF1B0CB1AEBACC /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 694993C41C8B334F00491CA5 = { + isa = PBXGroup; + children = ( + 694993CF1C8B334F00491CA5 /* Sample */, + 694993CE1C8B334F00491CA5 /* Products */, + 0DFDB4376BA084DAC7C1976E /* Pods */, + 478C8D7C412DCBDFE14640D8 /* Frameworks */, + ); + sourceTree = ""; + }; + 694993CE1C8B334F00491CA5 /* Products */ = { + isa = PBXGroup; + children = ( + 694993CD1C8B334F00491CA5 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 694993CF1C8B334F00491CA5 /* Sample */ = { + isa = PBXGroup; + children = ( + 694993D31C8B334F00491CA5 /* AppDelegate.h */, + 694993D41C8B334F00491CA5 /* AppDelegate.m */, + 694993D61C8B334F00491CA5 /* ViewController.h */, + 694993D71C8B334F00491CA5 /* ViewController.m */, + 69DCA5201C8B3D30006FF548 /* DetailViewController.h */, + 69DCA5211C8B3D30006FF548 /* DetailViewController.m */, + 69DCA5231C8BE01F006FF548 /* DetailRootNode.h */, + 69DCA5241C8BE01F006FF548 /* DetailRootNode.m */, + 69DCA5261C8BE031006FF548 /* DetailCellNode.h */, + 69DCA5271C8BE031006FF548 /* DetailCellNode.m */, + 694993DC1C8B334F00491CA5 /* Assets.xcassets */, + 694993D01C8B334F00491CA5 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 694993D01C8B334F00491CA5 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 694993E11C8B334F00491CA5 /* Info.plist */, + 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */, + 694993D11C8B334F00491CA5 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 694993CC1C8B334F00491CA5 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 694993E41C8B334F00491CA5 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 80035273449C25F4B2E1454F /* [CP] Check Pods Manifest.lock */, + 694993C91C8B334F00491CA5 /* Sources */, + 694993CA1C8B334F00491CA5 /* Frameworks */, + 694993CB1C8B334F00491CA5 /* Resources */, + 06EE2E0ABEB6289D4775A867 /* [CP] Copy Pods Resources */, + 23FC03B282CBD9014D868DF6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 694993CD1C8B334F00491CA5 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 694993C51C8B334F00491CA5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = AsyncDisplayKit; + TargetAttributes = { + 694993CC1C8B334F00491CA5 = { + CreatedOnToolsVersion = 7.2.1; + }; + }; + }; + buildConfigurationList = 694993C81C8B334F00491CA5 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 694993C41C8B334F00491CA5; + productRefGroup = 694993CE1C8B334F00491CA5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 694993CC1C8B334F00491CA5 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 694993CB1C8B334F00491CA5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 694993E01C8B334F00491CA5 /* LaunchScreen.storyboard in Resources */, + 694993DD1C8B334F00491CA5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 06EE2E0ABEB6289D4775A867 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 23FC03B282CBD9014D868DF6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 80035273449C25F4B2E1454F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 694993C91C8B334F00491CA5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 694993D81C8B334F00491CA5 /* ViewController.m in Sources */, + 694993D51C8B334F00491CA5 /* AppDelegate.m in Sources */, + 694993D21C8B334F00491CA5 /* main.m in Sources */, + 69DCA5221C8B3D30006FF548 /* DetailViewController.m in Sources */, + 69DCA5281C8BE031006FF548 /* DetailCellNode.m in Sources */, + 69DCA5251C8BE01F006FF548 /* DetailRootNode.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 694993DF1C8B334F00491CA5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 694993E21C8B334F00491CA5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 694993E31C8B334F00491CA5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 694993E51C8B334F00491CA5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 15AD337503831C4D33FF8B3A /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 694993E61C8B334F00491CA5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 97482F27BE2F7583EFE1BC2C /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 694993C81C8B334F00491CA5 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 694993E21C8B334F00491CA5 /* Debug */, + 694993E31C8B334F00491CA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 694993E41C8B334F00491CA5 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 694993E51C8B334F00491CA5 /* Debug */, + 694993E61C8B334F00491CA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 694993C51C8B334F00491CA5 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/ASViewController/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..c00064c54d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/AppDelegate.h new file mode 100644 index 0000000000..8d58a13cbe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/AppDelegate.m new file mode 100644 index 0000000000..ed2724b182 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/AppDelegate.m @@ -0,0 +1,28 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "ViewController.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[ViewController new]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..90d6157f11 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailCellNode.h b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailCellNode.h new file mode 100644 index 0000000000..d524ff1c02 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailCellNode.h @@ -0,0 +1,18 @@ +// +// DetailCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASNetworkImageNode; + +@interface DetailCellNode : ASCellNode +@property (nonatomic, assign) NSInteger row; +@property (nonatomic, copy) NSString *imageCategory; +@property (nonatomic, strong) ASNetworkImageNode *imageNode; +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailCellNode.m b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailCellNode.m new file mode 100644 index 0000000000..edc9932076 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailCellNode.m @@ -0,0 +1,57 @@ +// +// DetailCellNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "DetailCellNode.h" +#import + +@implementation DetailCellNode + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + self.automaticallyManagesSubnodes = YES; + + _imageNode = [[ASNetworkImageNode alloc] init]; + _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + + return self; +} + +#pragma mark - ASDisplayNode + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0 child:self.imageNode]; +} + +- (void)layoutDidFinish +{ + [super layoutDidFinish]; + + // In general set URL of ASNetworkImageNode as soon as possible. Ideally in init or a + // view model setter method. + // In this case as we need to know the size of the node the url is set in layoutDidFinish so + // we have the calculatedSize available + self.imageNode.URL = [self imageURL]; +} + +#pragma mark - Image + +- (NSURL *)imageURL +{ + CGSize imageSize = self.calculatedSize; + NSString *imageURLString = [NSString stringWithFormat:@"http://lorempixel.com/%ld/%ld/%@/%ld", (NSInteger)imageSize.width, (NSInteger)imageSize.height, self.imageCategory, self.row]; + return [NSURL URLWithString:imageURLString]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailRootNode.h b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailRootNode.h new file mode 100644 index 0000000000..648900b30e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailRootNode.h @@ -0,0 +1,20 @@ +// +// DetailRootNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASCollectionNode; + +@interface DetailRootNode : ASDisplayNode + +@property (nonatomic, strong, readonly) ASCollectionNode *collectionNode; + +- (instancetype)initWithImageCategory:(NSString *)imageCategory; + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailRootNode.m b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailRootNode.m new file mode 100644 index 0000000000..5d478059d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailRootNode.m @@ -0,0 +1,88 @@ +// +// DetailRootNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "DetailRootNode.h" +#import "DetailCellNode.h" + +#import + +static const NSInteger kImageHeight = 200; + + +@interface DetailRootNode () + +@property (nonatomic, copy) NSString *imageCategory; +@property (nonatomic, strong) ASCollectionNode *collectionNode; + +@end + + +@implementation DetailRootNode + +#pragma mark - Lifecycle + +- (instancetype)initWithImageCategory:(NSString *)imageCategory +{ + self = [super init]; + if (self) { + // Enable automaticallyManagesSubnodes so the first time the layout pass of the node is happening all nodes that are referenced + // in the laaout specification within layoutSpecThatFits: will be added automatically + self.automaticallyManagesSubnodes = YES; + + _imageCategory = imageCategory; + + // Create ASCollectionView. We don't have to add it explicitly as subnode as we will set usesImplicitHierarchyManagement to YES + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + _collectionNode.delegate = self; + _collectionNode.dataSource = self; + _collectionNode.backgroundColor = [UIColor whiteColor]; + } + + return self; +} + +- (void)dealloc +{ + _collectionNode.delegate = nil; + _collectionNode.dataSource = nil; +} + +#pragma mark - ASDisplayNode + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASWrapperLayoutSpec wrapperWithLayoutElement:self.collectionNode]; +} + +#pragma mark - ASCollectionDataSource + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return 10; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + NSString *imageCategory = self.imageCategory; + return ^{ + DetailCellNode *node = [[DetailCellNode alloc] init]; + node.row = indexPath.row; + node.imageCategory = imageCategory; + return node; + }; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGSize imageSize = CGSizeMake(CGRectGetWidth(collectionNode.view.frame), kImageHeight); + return ASSizeRangeMake(imageSize, imageSize); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailViewController.h b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailViewController.h new file mode 100644 index 0000000000..0c5eb85891 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailViewController.h @@ -0,0 +1,15 @@ +// +// DetailViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "DetailRootNode.h" + +@interface DetailViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailViewController.m b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailViewController.m new file mode 100644 index 0000000000..2d6471cd5d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/DetailViewController.m @@ -0,0 +1,26 @@ +// +// DetailViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "DetailViewController.h" +#import + +#import "DetailRootNode.h" + +@implementation DetailViewController + +#pragma mark - Rotation + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + [self.node.collectionNode.view.collectionViewLayout invalidateLayout]; +} + + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Info.plist new file mode 100644 index 0000000000..6105445463 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/ViewController.h new file mode 100644 index 0000000000..f1700621f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/ViewController.h @@ -0,0 +1,17 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ViewController : ASViewController + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/ViewController.m new file mode 100644 index 0000000000..f66c7add93 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/ViewController.m @@ -0,0 +1,92 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import + +#import "DetailViewController.h" + + +@interface ViewController () + +@property (nonatomic, copy) NSArray *imageCategories; +@property (nonatomic, strong, readonly) ASTableNode *tableNode; + +@end + + +@implementation ViewController + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super initWithNode:[ASTableNode new]]; + if (self == nil) { return self; } + + _imageCategories = @[@"abstract", @"animals", @"business", @"cats", @"city", @"food", @"nightlife", @"fashion", @"people", @"nature", @"sports", @"technics", @"transport"]; + + return self; +} + +- (void)dealloc +{ + self.node.delegate = nil; + self.node.dataSource = nil; +} + + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = @"Image Categories"; + + self.node.delegate = self; + self.node.dataSource = self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [self.node deselectRowAtIndexPath:self.node.indexPathForSelectedRow animated:YES]; +} + + +#pragma mark - ASTableDataSource / ASTableDelegate + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return self.imageCategories.count; +} + +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // As the block is executed on a background thread we need to cache the image category string outside + NSString *imageCategory = self.imageCategories[indexPath.row]; + return ^{ + ASTextCellNode *textCellNode = [ASTextCellNode new]; + textCellNode.text = [imageCategory capitalizedString]; + return textCellNode; + }; +} + +- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSString *imageCategory = self.imageCategories[indexPath.row]; + DetailRootNode *detailRootNode = [[DetailRootNode alloc] initWithImageCategory:imageCategory]; + DetailViewController *detailViewController = [[DetailViewController alloc] initWithNode:detailRootNode]; + detailViewController.title = [imageCategory capitalizedString]; + [self.navigationController pushViewController:detailViewController animated:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/ASViewController/Sample/main.m b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/ASViewController/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.h b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.h new file mode 100644 index 0000000000..8d58a13cbe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m new file mode 100644 index 0000000000..a21b00ffd1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m @@ -0,0 +1,24 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..36d2c80d88 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..2e721e1833 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/Main.storyboard b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..f56d2f3bb5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/Main.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Info.plist b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Info.plist new file mode 100644 index 0000000000..40c6215d90 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/ViewController.h b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/ViewController.h new file mode 100644 index 0000000000..4627e29285 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/ViewController.h @@ -0,0 +1,16 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/ViewController.m b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/ViewController.m new file mode 100644 index 0000000000..6fd98321db --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/ViewController.m @@ -0,0 +1,35 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import + +@interface ViewController () + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + + ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init]; + imageNode.URL = [NSURL URLWithString:@"https://i.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; + // Uncomment to see animated webp support + // imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"]; + imageNode.frame = self.view.bounds; + imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + imageNode.contentMode = UIViewContentModeScaleAspectFit; + + [self.view addSubnode:imageNode]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/main.m b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/main.m new file mode 100644 index 0000000000..65850400e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/ASAnimatedImage/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/Podfile b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Podfile new file mode 100644 index 0000000000..c998fa0a8d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Podfile @@ -0,0 +1,7 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' + pod 'PINRemoteImage/WebP' +end + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..453c359c0e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,394 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 683ADBA31CA19883005863A4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 683ADBA21CA19883005863A4 /* main.m */; }; + 683ADBA61CA19883005863A4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 683ADBA51CA19883005863A4 /* AppDelegate.m */; }; + 683ADBA91CA19883005863A4 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 683ADBA81CA19883005863A4 /* ViewController.m */; }; + 683ADBAC1CA19883005863A4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 683ADBAA1CA19883005863A4 /* Main.storyboard */; }; + 683ADBAE1CA19883005863A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 683ADBAD1CA19883005863A4 /* Assets.xcassets */; }; + 683ADBB11CA19883005863A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 683ADBAF1CA19883005863A4 /* LaunchScreen.storyboard */; }; + 9AE987532BDC7AC1FD693515 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 26E3F7C63D79A1F47BEF1AA2 /* libPods-Sample.a */; }; + DE5187D91CD9AA2A00EC11DE /* Pods-Sample.debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DE5187D71CD9AA2A00EC11DE /* Pods-Sample.debug.xcconfig */; }; + DE5187DA1CD9AA2A00EC11DE /* Pods-Sample.release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DE5187D81CD9AA2A00EC11DE /* Pods-Sample.release.xcconfig */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 26E3F7C63D79A1F47BEF1AA2 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 683ADB9E1CA19883005863A4 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 683ADBA21CA19883005863A4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 683ADBA41CA19883005863A4 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 683ADBA51CA19883005863A4 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 683ADBA71CA19883005863A4 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 683ADBA81CA19883005863A4 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 683ADBAB1CA19883005863A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 683ADBAD1CA19883005863A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 683ADBB01CA19883005863A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 683ADBB21CA19883005863A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A6F2399FA1A86586D9BDAE05 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + DE5187D71CD9AA2A00EC11DE /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; + DE5187D81CD9AA2A00EC11DE /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 683ADB9B1CA19883005863A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9AE987532BDC7AC1FD693515 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 683ADB951CA19883005863A4 = { + isa = PBXGroup; + children = ( + 683ADBA01CA19883005863A4 /* ASAnimatedImage */, + 683ADB9F1CA19883005863A4 /* Products */, + 71A772B0DB9B7760CE330DD9 /* Pods */, + 8C6AC07DE55B51935C632F56 /* Frameworks */, + ); + sourceTree = ""; + }; + 683ADB9F1CA19883005863A4 /* Products */ = { + isa = PBXGroup; + children = ( + 683ADB9E1CA19883005863A4 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 683ADBA01CA19883005863A4 /* ASAnimatedImage */ = { + isa = PBXGroup; + children = ( + 683ADBA41CA19883005863A4 /* AppDelegate.h */, + 683ADBA51CA19883005863A4 /* AppDelegate.m */, + 683ADBA71CA19883005863A4 /* ViewController.h */, + 683ADBA81CA19883005863A4 /* ViewController.m */, + 683ADBAA1CA19883005863A4 /* Main.storyboard */, + 683ADBAD1CA19883005863A4 /* Assets.xcassets */, + 683ADBAF1CA19883005863A4 /* LaunchScreen.storyboard */, + 683ADBB21CA19883005863A4 /* Info.plist */, + 683ADBA11CA19883005863A4 /* Supporting Files */, + ); + path = ASAnimatedImage; + sourceTree = ""; + }; + 683ADBA11CA19883005863A4 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 683ADBA21CA19883005863A4 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 71A772B0DB9B7760CE330DD9 /* Pods */ = { + isa = PBXGroup; + children = ( + DE5187D71CD9AA2A00EC11DE /* Pods-Sample.debug.xcconfig */, + DE5187D81CD9AA2A00EC11DE /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 8C6AC07DE55B51935C632F56 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A6F2399FA1A86586D9BDAE05 /* libPods.a */, + 26E3F7C63D79A1F47BEF1AA2 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 683ADB9D1CA19883005863A4 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 683ADBB51CA19883005863A4 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 694B306B43ED1C3916B0D909 /* [CP] Check Pods Manifest.lock */, + 683ADB9A1CA19883005863A4 /* Sources */, + 683ADB9B1CA19883005863A4 /* Frameworks */, + 683ADB9C1CA19883005863A4 /* Resources */, + 26A96BEEF893B1FA39F144CF /* [CP] Embed Pods Frameworks */, + 2ADE0E7B5309A9CD043DDB3E /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = ASAnimatedImage; + productReference = 683ADB9E1CA19883005863A4 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 683ADB961CA19883005863A4 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = "Facebook, Inc."; + TargetAttributes = { + 683ADB9D1CA19883005863A4 = { + CreatedOnToolsVersion = 7.2; + }; + }; + }; + buildConfigurationList = 683ADB991CA19883005863A4 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 683ADB951CA19883005863A4; + productRefGroup = 683ADB9F1CA19883005863A4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 683ADB9D1CA19883005863A4 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 683ADB9C1CA19883005863A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 683ADBB11CA19883005863A4 /* LaunchScreen.storyboard in Resources */, + 683ADBAE1CA19883005863A4 /* Assets.xcassets in Resources */, + 683ADBAC1CA19883005863A4 /* Main.storyboard in Resources */, + DE5187DA1CD9AA2A00EC11DE /* Pods-Sample.release.xcconfig in Resources */, + DE5187D91CD9AA2A00EC11DE /* Pods-Sample.debug.xcconfig in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 26A96BEEF893B1FA39F144CF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2ADE0E7B5309A9CD043DDB3E /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 694B306B43ED1C3916B0D909 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 683ADB9A1CA19883005863A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 683ADBA91CA19883005863A4 /* ViewController.m in Sources */, + 683ADBA61CA19883005863A4 /* AppDelegate.m in Sources */, + 683ADBA31CA19883005863A4 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 683ADBAA1CA19883005863A4 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 683ADBAB1CA19883005863A4 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 683ADBAF1CA19883005863A4 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 683ADBB01CA19883005863A4 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 683ADBB31CA19883005863A4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 683ADBB41CA19883005863A4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 683ADBB61CA19883005863A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DE5187D71CD9AA2A00EC11DE /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = ASAnimatedImage/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 683ADBB71CA19883005863A4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DE5187D81CD9AA2A00EC11DE /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = ASAnimatedImage/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 683ADB991CA19883005863A4 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 683ADBB31CA19883005863A4 /* Debug */, + 683ADBB41CA19883005863A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 683ADBB51CA19883005863A4 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 683ADBB61CA19883005863A4 /* Debug */, + 683ADBB71CA19883005863A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 683ADB961CA19883005863A4 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..dd7b72cdff --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..002b3cdc4e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AnimatedGIF/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Podfile b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Podfile new file mode 100644 index 0000000000..ff6cb63a31 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Podfile @@ -0,0 +1,10 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '9.0' + +# Uncomment this line if you're using Swift +# use_frameworks! + +target 'Sample' do + pod 'Texture', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..3683dbd638 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,410 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 697216351CCD8FB300122312 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 697216341CCD8FB300122312 /* main.m */; }; + 697216381CCD8FB300122312 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 697216371CCD8FB300122312 /* AppDelegate.m */; }; + 697216401CCD8FB300122312 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6972163F1CCD8FB300122312 /* Assets.xcassets */; }; + 697216431CCD8FB300122312 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 697216411CCD8FB300122312 /* LaunchScreen.storyboard */; }; + 6972164E1CCD938A00122312 /* OverviewComponentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6972164B1CCD938A00122312 /* OverviewComponentsViewController.m */; }; + 6972164F1CCD938A00122312 /* OverviewDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6972164D1CCD938A00122312 /* OverviewDetailViewController.m */; }; + 697216571CCD939000122312 /* OverviewASCollectionNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 697216521CCD939000122312 /* OverviewASCollectionNode.m */; }; + 697216581CCD939000122312 /* OverviewASPagerNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 697216541CCD939000122312 /* OverviewASPagerNode.m */; }; + 697216591CCD939000122312 /* OverviewASTableNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 697216561CCD939000122312 /* OverviewASTableNode.m */; }; + D06F1FFA9226EAB58D090CD4 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 70BE04668F29651A74A0DDDC /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 697216301CCD8FB300122312 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 697216341CCD8FB300122312 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 697216361CCD8FB300122312 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 697216371CCD8FB300122312 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 6972163F1CCD8FB300122312 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 697216421CCD8FB300122312 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 697216441CCD8FB300122312 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6972164A1CCD938A00122312 /* OverviewComponentsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverviewComponentsViewController.h; sourceTree = ""; }; + 6972164B1CCD938A00122312 /* OverviewComponentsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverviewComponentsViewController.m; sourceTree = ""; }; + 6972164C1CCD938A00122312 /* OverviewDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverviewDetailViewController.h; sourceTree = ""; }; + 6972164D1CCD938A00122312 /* OverviewDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverviewDetailViewController.m; sourceTree = ""; }; + 697216511CCD939000122312 /* OverviewASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverviewASCollectionNode.h; sourceTree = ""; }; + 697216521CCD939000122312 /* OverviewASCollectionNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverviewASCollectionNode.m; sourceTree = ""; }; + 697216531CCD939000122312 /* OverviewASPagerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverviewASPagerNode.h; sourceTree = ""; }; + 697216541CCD939000122312 /* OverviewASPagerNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverviewASPagerNode.m; sourceTree = ""; }; + 697216551CCD939000122312 /* OverviewASTableNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverviewASTableNode.h; sourceTree = ""; }; + 697216561CCD939000122312 /* OverviewASTableNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverviewASTableNode.m; sourceTree = ""; }; + 70BE04668F29651A74A0DDDC /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 77E10F80392B14772BED991F /* 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 = ""; }; + 9C0C5C6BF9E5B05EEF57DB2A /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6972162D1CCD8FB300122312 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D06F1FFA9226EAB58D090CD4 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2BEE2DA0B40FBAF7BD30B7C2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 70BE04668F29651A74A0DDDC /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 697216271CCD8FB300122312 = { + isa = PBXGroup; + children = ( + 697216321CCD8FB300122312 /* Sample */, + 697216311CCD8FB300122312 /* Products */, + 7DAA9CC1331AADE28CA32C05 /* Pods */, + 2BEE2DA0B40FBAF7BD30B7C2 /* Frameworks */, + ); + sourceTree = ""; + }; + 697216311CCD8FB300122312 /* Products */ = { + isa = PBXGroup; + children = ( + 697216301CCD8FB300122312 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 697216321CCD8FB300122312 /* Sample */ = { + isa = PBXGroup; + children = ( + 697216501CCD939000122312 /* Node Containers */, + 697216361CCD8FB300122312 /* AppDelegate.h */, + 697216371CCD8FB300122312 /* AppDelegate.m */, + 6972164A1CCD938A00122312 /* OverviewComponentsViewController.h */, + 6972164B1CCD938A00122312 /* OverviewComponentsViewController.m */, + 6972164C1CCD938A00122312 /* OverviewDetailViewController.h */, + 6972164D1CCD938A00122312 /* OverviewDetailViewController.m */, + 6972163F1CCD8FB300122312 /* Assets.xcassets */, + 697216411CCD8FB300122312 /* LaunchScreen.storyboard */, + 697216441CCD8FB300122312 /* Info.plist */, + 697216331CCD8FB300122312 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 697216331CCD8FB300122312 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 697216341CCD8FB300122312 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 697216501CCD939000122312 /* Node Containers */ = { + isa = PBXGroup; + children = ( + 697216511CCD939000122312 /* OverviewASCollectionNode.h */, + 697216521CCD939000122312 /* OverviewASCollectionNode.m */, + 697216531CCD939000122312 /* OverviewASPagerNode.h */, + 697216541CCD939000122312 /* OverviewASPagerNode.m */, + 697216551CCD939000122312 /* OverviewASTableNode.h */, + 697216561CCD939000122312 /* OverviewASTableNode.m */, + ); + path = "Node Containers"; + sourceTree = ""; + }; + 7DAA9CC1331AADE28CA32C05 /* Pods */ = { + isa = PBXGroup; + children = ( + 77E10F80392B14772BED991F /* Pods-Sample.debug.xcconfig */, + 9C0C5C6BF9E5B05EEF57DB2A /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6972162F1CCD8FB300122312 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 697216471CCD8FB300122312 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 78A0D09A94A74B3737920EA7 /* [CP] Check Pods Manifest.lock */, + 6972162C1CCD8FB300122312 /* Sources */, + 6972162D1CCD8FB300122312 /* Frameworks */, + 6972162E1CCD8FB300122312 /* Resources */, + 267658CA53A0F4A2D24A8438 /* [CP] Embed Pods Frameworks */, + 84F93825AFB1CA7FBB116BA4 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 697216301CCD8FB300122312 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 697216281CCD8FB300122312 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 6972162F1CCD8FB300122312 = { + CreatedOnToolsVersion = 7.3; + }; + }; + }; + buildConfigurationList = 6972162B1CCD8FB300122312 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 697216271CCD8FB300122312; + productRefGroup = 697216311CCD8FB300122312 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6972162F1CCD8FB300122312 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6972162E1CCD8FB300122312 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 697216431CCD8FB300122312 /* LaunchScreen.storyboard in Resources */, + 697216401CCD8FB300122312 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 267658CA53A0F4A2D24A8438 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 78A0D09A94A74B3737920EA7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 84F93825AFB1CA7FBB116BA4 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6972162C1CCD8FB300122312 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 697216581CCD939000122312 /* OverviewASPagerNode.m in Sources */, + 697216381CCD8FB300122312 /* AppDelegate.m in Sources */, + 697216351CCD8FB300122312 /* main.m in Sources */, + 697216571CCD939000122312 /* OverviewASCollectionNode.m in Sources */, + 697216591CCD939000122312 /* OverviewASTableNode.m in Sources */, + 6972164F1CCD938A00122312 /* OverviewDetailViewController.m in Sources */, + 6972164E1CCD938A00122312 /* OverviewComponentsViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 697216411CCD8FB300122312 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 697216421CCD8FB300122312 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 697216451CCD8FB300122312 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 697216461CCD8FB300122312 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 697216481CCD8FB300122312 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 77E10F80392B14772BED991F /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 697216491CCD8FB300122312 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9C0C5C6BF9E5B05EEF57DB2A /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6972162B1CCD8FB300122312 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 697216451CCD8FB300122312 /* Debug */, + 697216461CCD8FB300122312 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 697216471CCD8FB300122312 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 697216481CCD8FB300122312 /* Debug */, + 697216491CCD8FB300122312 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 697216281CCD8FB300122312 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..18b29fc007 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/AppDelegate.h new file mode 100644 index 0000000000..8d58a13cbe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/AppDelegate.m new file mode 100644 index 0000000000..8221f4c755 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/AppDelegate.m @@ -0,0 +1,33 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "OverviewComponentsViewController.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[OverviewComponentsViewController new]]; + self.window.backgroundColor = [UIColor whiteColor]; + [self.window makeKeyAndVisible]; + + [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:47/255.0 green:184/255.0 blue:253/255.0 alpha:1.0]]; + [[UINavigationBar appearance] setTintColor:[UIColor whiteColor]]; + [[UINavigationBar appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor whiteColor]}];; + + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..eeea76c2db --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/Contents.json b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/image.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/image.imageset/Contents.json new file mode 100644 index 0000000000..28461488b5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/image.imageset/image.jpg b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/image.imageset/image.jpg new file mode 100644 index 0000000000..84428e0164 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Assets.xcassets/image.imageset/image.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..2e721e1833 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Info.plist new file mode 100644 index 0000000000..028aa2b33f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASCollectionNode.h b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASCollectionNode.h new file mode 100644 index 0000000000..8fc32e4e43 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASCollectionNode.h @@ -0,0 +1,13 @@ +// +// OverviewASCollectionNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface OverviewASCollectionNode : ASDisplayNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASCollectionNode.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASCollectionNode.m new file mode 100644 index 0000000000..5d29eea95e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASCollectionNode.m @@ -0,0 +1,65 @@ +// +// OverviewASCollectionNode.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewASCollectionNode.h" + +#import + +@interface OverviewASCollectionNode () +@property (nonatomic, strong) ASCollectionNode *node; +@end + +@implementation OverviewASCollectionNode + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + UICollectionViewFlowLayout *flowLayout = [UICollectionViewFlowLayout new]; + _node = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; + _node.dataSource = self; + _node.delegate = self; + [self addSubnode:_node];; + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // 100% of container + _node.style.width = ASDimensionMakeWithFraction(1.0); + _node.style.height = ASDimensionMakeWithFraction(1.0); + return [ASWrapperLayoutSpec wrapperWithLayoutElement:_node]; +} + +#pragma mark - + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return 100; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + ASTextCellNode *cellNode = [ASTextCellNode new]; + cellNode.backgroundColor = [UIColor lightGrayColor]; + cellNode.text = [NSString stringWithFormat:@"Row: %ld", indexPath.row]; + return cellNode; + }; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ASSizeRangeMake(CGSizeMake(100, 100)); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASPagerNode.h b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASPagerNode.h new file mode 100644 index 0000000000..dd6d09d014 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASPagerNode.h @@ -0,0 +1,13 @@ +// +// OverviewASPagerNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface OverviewASPagerNode : ASDisplayNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASPagerNode.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASPagerNode.m new file mode 100644 index 0000000000..7e37448497 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASPagerNode.m @@ -0,0 +1,80 @@ +// +// OverviewASPagerNode.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewASPagerNode.h" + +#pragma mark - Helper + +static UIColor *OverViewASPagerNodeRandomColor() { + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; +} + + +#pragma mark - OverviewASPageNode + +@interface OverviewASPageNode : ASCellNode @end + +@implementation OverviewASPageNode + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + return [ASLayout layoutWithLayoutElement:self size:constrainedSize.max]; +} + +@end + + +#pragma mark - OverviewASPagerNode + +@interface OverviewASPagerNode () +@property (nonatomic, strong) ASPagerNode *node; +@property (nonatomic, copy) NSArray *data; +@end + +@implementation OverviewASPagerNode + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + _node = [ASPagerNode new]; + _node.dataSource = self; + _node.delegate = self; + [self addSubnode:_node]; + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // 100% of container + _node.style.width = ASDimensionMakeWithFraction(1.0); + _node.style.height = ASDimensionMakeWithFraction(1.0); + return [ASWrapperLayoutSpec wrapperWithLayoutElement:_node]; +} + +- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode +{ + return 4; +} + +- (ASCellNodeBlock)pagerNode:(ASPagerNode *)pagerNode nodeBlockAtIndex:(NSInteger)index +{ + return ^{ + ASCellNode *cellNode = [OverviewASPageNode new]; + cellNode.backgroundColor = OverViewASPagerNodeRandomColor(); + return cellNode; + }; +} + + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASTableNode.h b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASTableNode.h new file mode 100644 index 0000000000..f80af0d2c9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASTableNode.h @@ -0,0 +1,13 @@ +// +// OverviewASTableNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface OverviewASTableNode : ASDisplayNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASTableNode.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASTableNode.m new file mode 100644 index 0000000000..de050c3694 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/Node Containers/OverviewASTableNode.m @@ -0,0 +1,57 @@ +// +// OverviewASTableNode.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewASTableNode.h" + +@interface OverviewASTableNode () +@property (nonatomic, strong) ASTableNode *node; +@end + +@implementation OverviewASTableNode + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + _node = [ASTableNode new]; + _node.dataSource = self; + _node.delegate = self; + [self addSubnode:_node]; + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // 100% of container + _node.style.width = ASDimensionMakeWithFraction(1.0); + _node.style.height = ASDimensionMakeWithFraction(1.0); + return [ASWrapperLayoutSpec wrapperWithLayoutElement:_node]; +} + + +#pragma mark - + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return 100; +} + +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + ASTextCellNode *cellNode = [ASTextCellNode new]; + cellNode.text = [NSString stringWithFormat:@"Row: %ld", indexPath.row]; + return cellNode; + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewComponentsViewController.h b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewComponentsViewController.h new file mode 100644 index 0000000000..cae34ad209 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewComponentsViewController.h @@ -0,0 +1,24 @@ +// +// OverviewComponentsViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + + +@protocol ASLayoutSpecListEntry + +- (NSString *)entryTitle; +- (NSString *)entryDescription; + +@end + +@interface OverviewComponentsViewController : ASViewController + + +@end + diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewComponentsViewController.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewComponentsViewController.m new file mode 100644 index 0000000000..740d975d2a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewComponentsViewController.m @@ -0,0 +1,556 @@ +// +// OverviewComponentsViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewComponentsViewController.h" + +#import "OverviewDetailViewController.h" +#import "OverviewASCollectionNode.h" +#import "OverviewASTableNode.h" +#import "OverviewASPagerNode.h" + +#import + + +#pragma mark - ASCenterLayoutSpecSizeThatFitsBlock + +typedef ASLayoutSpec *(^OverviewDisplayNodeSizeThatFitsBlock)(ASSizeRange constrainedSize); + + +#pragma mark - OverviewDisplayNodeWithSizeBlock + +@interface OverviewDisplayNodeWithSizeBlock : ASDisplayNode + +@property (nonatomic, copy) NSString *entryTitle; +@property (nonatomic, copy) NSString *entryDescription; +@property (nonatomic, copy) OverviewDisplayNodeSizeThatFitsBlock sizeThatFitsBlock; + +@end + +@implementation OverviewDisplayNodeWithSizeBlock + +// FIXME: Use new ASDisplayNodeAPI (layoutSpecBlock) API if shipped +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + OverviewDisplayNodeSizeThatFitsBlock block = self.sizeThatFitsBlock; + if (block != nil) { + return block(constrainedSize); + } + + return [super layoutSpecThatFits:constrainedSize]; +} + +@end + + +#pragma mark - OverviewTitleDescriptionCellNode + +@interface OverviewTitleDescriptionCellNode : ASCellNode + +@property (nonatomic, strong) ASTextNode *titleNode; +@property (nonatomic, strong) ASTextNode *descriptionNode; + +@end + +@implementation OverviewTitleDescriptionCellNode + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + _titleNode = [[ASTextNode alloc] init]; + _descriptionNode = [[ASTextNode alloc] init]; + + [self addSubnode:_titleNode]; + [self addSubnode:_descriptionNode]; + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + BOOL hasDescription = self.descriptionNode.attributedText.length > 0; + + ASStackLayoutSpec *verticalStackLayoutSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackLayoutSpec.alignItems = ASStackLayoutAlignItemsStart; + verticalStackLayoutSpec.spacing = 5.0; + verticalStackLayoutSpec.children = hasDescription ? @[self.titleNode, self.descriptionNode] : @[self.titleNode]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 16, 10, 10) child:verticalStackLayoutSpec]; +} + +@end + + +#pragma mark - OverviewComponentsViewController + +@interface OverviewComponentsViewController () + +@property (nonatomic, copy) NSArray *data; +@property (nonatomic, strong) ASTableNode *tableNode; + +@end + +@implementation OverviewComponentsViewController + + +#pragma mark - Lifecycle Methods + +- (instancetype)init +{ + _tableNode = [ASTableNode new]; + + self = [super initWithNode:_tableNode]; + + if (self) { + _tableNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _tableNode.delegate = (id)self; + _tableNode.dataSource = (id)self; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = @"AsyncDisplayKit"; + + [self setupData]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [_tableNode deselectRowAtIndexPath:_tableNode.indexPathForSelectedRow animated:YES]; +} + + +#pragma mark - Data Model + +- (void)setupData +{ + OverviewDisplayNodeWithSizeBlock *parentNode = nil; + ASDisplayNode *childNode = nil; + + +// Setup Nodes Container +// --------------------------------------------------------------------------------------------------------- + NSMutableArray *mutableNodesContainerData = [NSMutableArray array]; + +#pragma mark ASCollectionNode + childNode = [OverviewASCollectionNode new]; + + parentNode = [self centeringParentNodeWithInset:UIEdgeInsetsZero child:childNode]; + parentNode.entryTitle = @"ASCollectionNode"; + parentNode.entryDescription = @"ASCollectionNode is a node based class that wraps an ASCollectionView. It can be used as a subnode of another node, and provide room for many (great) features and improvements later on."; + [mutableNodesContainerData addObject:parentNode]; + +#pragma mark ASTableNode + childNode = [OverviewASTableNode new]; + + parentNode = [self centeringParentNodeWithInset:UIEdgeInsetsZero child:childNode]; + parentNode.entryTitle = @"ASTableNode"; + parentNode.entryDescription = @"ASTableNode is a node based class that wraps an ASTableView. It can be used as a subnode of another node, and provide room for many (great) features and improvements later on."; + [mutableNodesContainerData addObject:parentNode]; + +#pragma mark ASPagerNode + childNode = [OverviewASPagerNode new]; + + parentNode = [self centeringParentNodeWithInset:UIEdgeInsetsZero child:childNode]; + parentNode.entryTitle = @"ASPagerNode"; + parentNode.entryDescription = @"ASPagerNode is a specialized subclass of ASCollectionNode. Using it allows you to produce a page style UI similar to what you'd create with a UIPageViewController with UIKit. Luckily, the API is quite a bit simpler than UIPageViewController's."; + [mutableNodesContainerData addObject:parentNode]; + + +// Setup Nodes +// --------------------------------------------------------------------------------------------------------- + NSMutableArray *mutableNodesData = [NSMutableArray array]; + +#pragma mark ASDisplayNode + ASDisplayNode *displayNode = [self childNode]; + + parentNode = [self centeringParentNodeWithChild:displayNode]; + parentNode.entryTitle = @"ASDisplayNode"; + parentNode.entryDescription = @"ASDisplayNode is the main view abstraction over UIView and CALayer. It initializes and owns a UIView in the same way UIViews create and own their own backing CALayers."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASButtonNode + ASButtonNode *buttonNode = [ASButtonNode new]; + + // Set title for button node with a given font or color. If you pass in nil for font or color the default system + // font and black as color will be used + [buttonNode setTitle:@"Button Title Normal" withFont:nil withColor:[UIColor blueColor] forState:UIControlStateNormal]; + [buttonNode setTitle:@"Button Title Highlighted" withFont:[UIFont systemFontOfSize:14] withColor:nil forState:UIControlStateHighlighted]; + [buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchUpInside]; + + parentNode = [self centeringParentNodeWithChild:buttonNode]; + parentNode.entryTitle = @"ASButtonNode"; + parentNode.entryDescription = @"ASButtonNode (a subclass of ASControlNode) supports simple buttons, with multiple states for a text label and an image with a few different layout options. Enables layerBacking for subnodes to significantly lighten main thread impact relative to UIButton (though async preparation is the bigger win)."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASTextNode + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum varius nisi quis mattis dignissim. Proin convallis odio nec ipsum molestie, in porta quam viverra. Fusce ornare dapibus velit, nec malesuada mauris pretium vitae. Etiam malesuada ligula magna."]; + + parentNode = [self centeringParentNodeWithChild:textNode]; + parentNode.entryTitle = @"ASTextNode"; + parentNode.entryDescription = @"Like UITextView — built on TextKit with full-featured rich text support."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASEditableTextNode + ASEditableTextNode *editableTextNode = [ASEditableTextNode new]; + editableTextNode.backgroundColor = [UIColor lightGrayColor]; + editableTextNode.attributedText = [[NSAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum varius nisi quis mattis dignissim. Proin convallis odio nec ipsum molestie, in porta quam viverra. Fusce ornare dapibus velit, nec malesuada mauris pretium vitae. Etiam malesuada ligula magna."]; + + parentNode = [self centeringParentNodeWithChild:editableTextNode]; + parentNode.entryTitle = @"ASEditableTextNode"; + parentNode.entryDescription = @"ASEditableTextNode provides a flexible, efficient, and animation-friendly editable text component."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASImageNode + ASImageNode *imageNode = [ASImageNode new]; + imageNode.image = [UIImage imageNamed:@"image.jpg"]; + + CGSize imageNetworkImageNodeSize = (CGSize){imageNode.image.size.width / 7, imageNode.image.size.height / 7}; + + imageNode.style.preferredSize = imageNetworkImageNodeSize; + + parentNode = [self centeringParentNodeWithChild:imageNode]; + parentNode.entryTitle = @"ASImageNode"; + parentNode.entryDescription = @"Like UIImageView — decodes images asynchronously."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASNetworkImageNode + ASNetworkImageNode *networkImageNode = [ASNetworkImageNode new]; + networkImageNode.URL = [NSURL URLWithString:@"http://i.imgur.com/FjOR9kX.jpg"]; + networkImageNode.style.preferredSize = imageNetworkImageNodeSize; + + parentNode = [self centeringParentNodeWithChild:networkImageNode]; + parentNode.entryTitle = @"ASNetworkImageNode"; + parentNode.entryDescription = @"ASNetworkImageNode is a simple image node that can download and display an image from the network, with support for a placeholder image."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASMapNode + ASMapNode *mapNode = [ASMapNode new]; + mapNode.style.preferredSize = CGSizeMake(300.0, 300.0); + + // San Francisco + CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(37.7749, -122.4194); + mapNode.region = MKCoordinateRegionMakeWithDistance(coord, 20000, 20000); + + parentNode = [self centeringParentNodeWithChild:mapNode]; + parentNode.entryTitle = @"ASMapNode"; + parentNode.entryDescription = @"ASMapNode offers completely asynchronous preparation, automatic preloading, and efficient memory handling. Its standard mode is a fully asynchronous snapshot, with liveMap mode loading automatically triggered by any ASTableView or ASCollectionView; its .liveMap mode can be flipped on with ease (even on a background thread) to provide a cached, fully interactive map when necessary."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASVideoNode + ASVideoNode *videoNode = [ASVideoNode new]; + videoNode.style.preferredSize = CGSizeMake(300.0, 400.0); + + AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"http://www.w3schools.com/html/mov_bbb.mp4"]]; + videoNode.asset = asset; + + parentNode = [self centeringParentNodeWithChild:videoNode]; + parentNode.entryTitle = @"ASVideoNode"; + parentNode.entryDescription = @"ASVideoNode is a newer class that exposes a relatively full-featured API, and is designed for both efficient and convenient implementation of embedded videos in scrolling views."; + [mutableNodesData addObject:parentNode]; + +#pragma mark ASScrollNode + UIImage *scrollNodeImage = [UIImage imageNamed:@"image"]; + + ASScrollNode *scrollNode = [ASScrollNode new]; + scrollNode.style.preferredSize = CGSizeMake(300.0, 400.0); + + UIScrollView *scrollNodeView = scrollNode.view; + [scrollNodeView addSubview:[[UIImageView alloc] initWithImage:scrollNodeImage]]; + scrollNodeView.contentSize = scrollNodeImage.size; + + parentNode = [self centeringParentNodeWithChild:scrollNode]; + parentNode.entryTitle = @"ASScrollNode"; + parentNode.entryDescription = @"Simple node that wraps UIScrollView."; + [mutableNodesData addObject:parentNode]; + + +// Layout Specs +// --------------------------------------------------------------------------------------------------------- + NSMutableArray *mutableLayoutSpecData = [NSMutableArray array]; + +#pragma mark ASInsetLayoutSpec + childNode = [self childNode]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASInsetLayoutSpec"; + parentNode.entryDescription = @"Applies an inset margin around a component."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(20, 10, 0, 0) child:childNode]; + }; + [parentNode addSubnode:childNode]; + [mutableLayoutSpecData addObject:parentNode]; + + +#pragma mark ASBackgroundLayoutSpec + ASDisplayNode *backgroundNode = [ASDisplayNode new]; + backgroundNode.backgroundColor = [UIColor greenColor]; + + childNode = [self childNode]; + childNode.backgroundColor = [childNode.backgroundColor colorWithAlphaComponent:0.5]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASBackgroundLayoutSpec"; + parentNode.entryDescription = @"Lays out a component, stretching another component behind it as a backdrop."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:childNode background:backgroundNode]; + }; + [parentNode addSubnode:backgroundNode]; + [parentNode addSubnode:childNode]; + [mutableLayoutSpecData addObject:parentNode]; + + +#pragma mark ASOverlayLayoutSpec + ASDisplayNode *overlayNode = [ASDisplayNode new]; + overlayNode.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5]; + + childNode = [self childNode]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASOverlayLayoutSpec"; + parentNode.entryDescription = @"Lays out a component, stretching another component on top of it as an overlay."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:childNode overlay:overlayNode]; + }; + [parentNode addSubnode:childNode]; + [parentNode addSubnode:overlayNode]; + [mutableLayoutSpecData addObject:parentNode]; + + +#pragma mark ASCenterLayoutSpec + childNode = [self childNode]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASCenterLayoutSpec"; + parentNode.entryDescription = @"Centers a component in the available space."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY + sizingOptions:ASCenterLayoutSpecSizingOptionDefault + child:childNode]; + }; + [parentNode addSubnode:childNode]; + [mutableLayoutSpecData addObject:parentNode]; + +#pragma mark ASRatioLayoutSpec + childNode = [self childNode]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASRatioLayoutSpec"; + parentNode.entryDescription = @"Lays out a component at a fixed aspect ratio. Great for images, gifs and videos."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASRatioLayoutSpec ratioLayoutSpecWithRatio:0.25 child:childNode]; + }; + [parentNode addSubnode:childNode]; + [mutableLayoutSpecData addObject:parentNode]; + +#pragma mark ASRelativeLayoutSpec + childNode = [self childNode]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASRelativeLayoutSpec"; + parentNode.entryDescription = @"Lays out a component and positions it within the layout bounds according to vertical and horizontal positional specifiers. Similar to the “9-part” image areas, a child can be positioned at any of the 4 corners, or the middle of any of the 4 edges, as well as the center."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASRelativeLayoutSpec relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionEnd + verticalPosition:ASRelativeLayoutSpecPositionCenter + sizingOption:ASRelativeLayoutSpecSizingOptionDefault + child:childNode]; + }; + [parentNode addSubnode:childNode]; + [mutableLayoutSpecData addObject:parentNode]; + +#pragma mark ASAbsoluteLayoutSpec + childNode = [self childNode]; + // Add a layout position to the child node that the absolute layout spec will pick up and place it on that position + childNode.style.layoutPosition = CGPointMake(10.0, 10.0); + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"ASAbsoluteLayoutSpec"; + parentNode.entryDescription = @"Allows positioning children at fixed offsets."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[childNode]]; + }; + [parentNode addSubnode:childNode]; + [mutableLayoutSpecData addObject:parentNode]; + + +#pragma mark Vertical ASStackLayoutSpec + ASDisplayNode *childNode1 = [self childNode]; + childNode1.backgroundColor = [UIColor greenColor]; + + ASDisplayNode *childNode2 = [self childNode]; + childNode2.backgroundColor = [UIColor blueColor]; + + ASDisplayNode *childNode3 = [self childNode]; + childNode3.backgroundColor = [UIColor yellowColor]; + + // If we just would add the childrent to the stack layout the layout would be to tall and run out of the edge of + // the node as 50+50+50 = 150 but the parent node is only 100 height. To prevent that we set flexShrink on 2 of the + // children to let the stack layout know it should shrink these children in case the layout will run over the edge + childNode2.style.flexShrink = 1.0; + childNode3.style.flexShrink = 1.0; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"Vertical ASStackLayoutSpec"; + parentNode.entryDescription = @"Is based on a simplified version of CSS flexbox. It allows you to stack components vertically or horizontally and specify how they should be flexed and aligned to fit in the available space."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + ASStackLayoutSpec *verticalStackLayoutSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackLayoutSpec.alignItems = ASStackLayoutAlignItemsStart; + verticalStackLayoutSpec.children = @[childNode1, childNode2, childNode3]; + return verticalStackLayoutSpec; + }; + [parentNode addSubnode:childNode1]; + [parentNode addSubnode:childNode2]; + [parentNode addSubnode:childNode3]; + [mutableLayoutSpecData addObject:parentNode]; + +#pragma mark Horizontal ASStackLayoutSpec + childNode1 = [ASDisplayNode new]; + childNode1.style.preferredSize = CGSizeMake(10.0, 20.0); + childNode1.style.flexGrow = 1.0; + childNode1.backgroundColor = [UIColor greenColor]; + + childNode2 = [ASDisplayNode new]; + childNode2.style.preferredSize = CGSizeMake(10.0, 20.0); + childNode2.style.alignSelf = ASStackLayoutAlignSelfStretch; + childNode2.backgroundColor = [UIColor blueColor]; + + childNode3 = [ASDisplayNode new]; + childNode3.style.preferredSize = CGSizeMake(10.0, 20.0); + childNode3.backgroundColor = [UIColor yellowColor]; + + parentNode = [self parentNodeWithChild:childNode]; + parentNode.entryTitle = @"Horizontal ASStackLayoutSpec"; + parentNode.entryDescription = @"Is based on a simplified version of CSS flexbox. It allows you to stack components vertically or horizontally and specify how they should be flexed and aligned to fit in the available space."; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + + // Create stack alyout spec to layout children + ASStackLayoutSpec *horizontalStackSpec = [ASStackLayoutSpec horizontalStackLayoutSpec]; + horizontalStackSpec.alignItems = ASStackLayoutAlignItemsStart; + horizontalStackSpec.children = @[childNode1, childNode2, childNode3]; + horizontalStackSpec.spacing = 5.0; // Spacing between children + + // Layout the stack layout with 100% width and 100% height of the parent node + horizontalStackSpec.style.height = ASDimensionMakeWithFraction(1.0); + horizontalStackSpec.style.width = ASDimensionMakeWithFraction(1.0); + + // Add a bit of inset + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(0.0, 5.0, 0.0, 5.0) child:horizontalStackSpec]; + }; + [parentNode addSubnode:childNode1]; + [parentNode addSubnode:childNode2]; + [parentNode addSubnode:childNode3]; + [mutableLayoutSpecData addObject:parentNode]; + + +// Setup Data +// --------------------------------------------------------------------------------------------------------- + NSMutableArray *mutableData = [NSMutableArray array]; + [mutableData addObject:@{@"title" : @"Node Containers", @"data" : mutableNodesContainerData}]; + [mutableData addObject:@{@"title" : @"Nodes", @"data" : mutableNodesData}]; + [mutableData addObject:@{@"title" : @"Layout Specs", @"data" : [mutableLayoutSpecData copy]}]; + self.data = mutableData; +} + +#pragma mark - Parent / Child Helper + +- (OverviewDisplayNodeWithSizeBlock *)parentNodeWithChild:(ASDisplayNode *)child +{ + OverviewDisplayNodeWithSizeBlock *parentNode = [OverviewDisplayNodeWithSizeBlock new]; + parentNode.style.preferredSize = CGSizeMake(100, 100); + parentNode.backgroundColor = [UIColor redColor]; + return parentNode; +} + +- (OverviewDisplayNodeWithSizeBlock *)centeringParentNodeWithChild:(ASDisplayNode *)child +{ + return [self centeringParentNodeWithInset:UIEdgeInsetsMake(10, 10, 10, 10) child:child]; +} + +- (OverviewDisplayNodeWithSizeBlock *)centeringParentNodeWithInset:(UIEdgeInsets)insets child:(ASDisplayNode *)child +{ + OverviewDisplayNodeWithSizeBlock *parentNode = [OverviewDisplayNodeWithSizeBlock new]; + [parentNode addSubnode:child]; + parentNode.sizeThatFitsBlock = ^ASLayoutSpec *(ASSizeRange constrainedSize) { + ASCenterLayoutSpec *centerLayoutSpec = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:child]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:centerLayoutSpec]; + }; + return parentNode; +} + +- (ASDisplayNode *)childNode +{ + ASDisplayNode *childNode = [ASDisplayNode new]; + childNode.style.preferredSize = CGSizeMake(50, 50); + childNode.backgroundColor = [UIColor blueColor]; + return childNode; +} + +#pragma mark - Actions + +- (void)buttonPressed:(ASButtonNode *)buttonNode +{ + NSLog(@"Button Pressed"); +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode +{ + return self.data.count; +} + +- (nullable NSString *)tableNode:(ASTableNode *)tableNode titleForHeaderInSection:(NSInteger)section +{ + return self.data[section][@"title"]; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return [self.data[section][@"data"] count]; +} + +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // You should get the node or data you want to pass to the cell node outside of the ASCellNodeBlock + ASDisplayNode *node = self.data[indexPath.section][@"data"][indexPath.row]; + return ^{ + OverviewTitleDescriptionCellNode *cellNode = [OverviewTitleDescriptionCellNode new]; + + NSDictionary *titleNodeAttributes = @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:14.0], + NSForegroundColorAttributeName : [UIColor blackColor] + }; + cellNode.titleNode.attributedText = [[NSAttributedString alloc] initWithString:node.entryTitle attributes:titleNodeAttributes]; + + if (node.entryDescription) { + NSDictionary *descriptionNodeAttributes = @{NSForegroundColorAttributeName : [UIColor lightGrayColor]}; + cellNode.descriptionNode.attributedText = [[NSAttributedString alloc] initWithString:node.entryDescription attributes:descriptionNodeAttributes]; + } + + return cellNode; + }; +} + +- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNode *node = self.data[indexPath.section][@"data"][indexPath.row]; + OverviewDetailViewController *detail = [[OverviewDetailViewController alloc] initWithNode:node]; + [self.navigationController pushViewController:detail animated:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewDetailViewController.h b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewDetailViewController.h new file mode 100644 index 0000000000..8a006b4428 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewDetailViewController.h @@ -0,0 +1,16 @@ +// +// OverviewDetailViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASDisplayNode; + +@interface OverviewDetailViewController : UIViewController +- (instancetype)initWithNode:(ASDisplayNode *)node; +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewDetailViewController.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewDetailViewController.m new file mode 100644 index 0000000000..b81b307921 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/OverviewDetailViewController.m @@ -0,0 +1,51 @@ +// +// OverviewDetailViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewDetailViewController.h" + +@interface OverviewDetailViewController () +@property (nonatomic, strong) ASDisplayNode *node; +@end + +@implementation OverviewDetailViewController + +#pragma mark - Lifecycle + +- (instancetype)initWithNode:(ASDisplayNode *)node +{ + self = [super initWithNibName:nil bundle:nil]; + if (self == nil) { return self; } + _node = node; + return self; +} + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor whiteColor]; + [self.view addSubnode:self.node]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + // Center node frame + CGRect bounds = self.view.bounds; + CGSize nodeSize = [self.node layoutThatFits:ASSizeRangeMake(CGSizeZero, bounds.size)].size; + self.node.frame = CGRectMake(CGRectGetMidX(bounds) - (nodeSize.width / 2.0), + CGRectGetMidY(bounds) - (nodeSize.height / 2.0), + nodeSize.width, + nodeSize.height); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/main.m b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/AsyncDisplayKitOverview/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Podfile b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..bc9a71bfb5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,412 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FDEC911BF31EE700CEB123 /* ItemNode.m */; }; + 7A83848E1C34359D002CDD08 /* ItemViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A83848D1C34359D002CDD08 /* ItemViewModel.m */; }; + 7A8384941C343680002CDD08 /* BlurbNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A8384921C343680002CDD08 /* BlurbNode.m */; }; + 7A8384971C344057002CDD08 /* ItemStyles.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A8384961C344057002CDD08 /* ItemStyles.m */; }; + 7ACD5F891C415B7500E7BE16 /* LoadingNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD5F881C415B7500E7BE16 /* LoadingNode.m */; }; + 7ACD5F961C4847C000E7BE16 /* PlaceholderNetworkImageNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD5F951C4847C000E7BE16 /* PlaceholderNetworkImageNode.m */; }; + 91C402838901BD02685337A8 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F1E539014E1F516F00A8F167 /* libPods-Sample.a */; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; }; + AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + CC142599219742CA009851B7 /* ASDisplayNode+CatDeals.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC142598219742CA009851B7 /* ASDisplayNode+CatDeals.mm */; }; + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 25FDEC901BF31EE700CEB123 /* ItemNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemNode.h; sourceTree = ""; }; + 25FDEC911BF31EE700CEB123 /* ItemNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemNode.m; sourceTree = ""; }; + 65F7AC562A910A742522053C /* 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 = ""; }; + 7A83848C1C34359D002CDD08 /* ItemViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemViewModel.h; sourceTree = ""; }; + 7A83848D1C34359D002CDD08 /* ItemViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemViewModel.m; sourceTree = ""; }; + 7A8384911C343680002CDD08 /* BlurbNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurbNode.h; sourceTree = ""; }; + 7A8384921C343680002CDD08 /* BlurbNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurbNode.m; sourceTree = ""; }; + 7A8384951C344057002CDD08 /* ItemStyles.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemStyles.h; sourceTree = ""; }; + 7A8384961C344057002CDD08 /* ItemStyles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemStyles.m; sourceTree = ""; }; + 7ACD5F871C415B7500E7BE16 /* LoadingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoadingNode.h; sourceTree = ""; }; + 7ACD5F881C415B7500E7BE16 /* LoadingNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoadingNode.m; sourceTree = ""; }; + 7ACD5F941C4847C000E7BE16 /* PlaceholderNetworkImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaceholderNetworkImageNode.h; sourceTree = ""; }; + 7ACD5F951C4847C000E7BE16 /* PlaceholderNetworkImageNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaceholderNetworkImageNode.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; + AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AC3C4A651A11F47200143C57 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AC3C4A661A11F47200143C57 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AC3C4A681A11F47200143C57 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + CC142598219742CA009851B7 /* ASDisplayNode+CatDeals.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+CatDeals.mm"; sourceTree = ""; }; + CC14259A219746C2009851B7 /* ASDisplayNode+CatDeals.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+CatDeals.h"; sourceTree = ""; }; + F1E539014E1F516F00A8F167 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F3A72D578C378357FF56486A /* 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 = ""; }; + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PresentingViewController.h; sourceTree = ""; }; + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PresentingViewController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC3C4A5B1A11F47200143C57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 91C402838901BD02685337A8 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 90A2B9C5397C46134C8A793B /* Pods */ = { + isa = PBXGroup; + children = ( + F3A72D578C378357FF56486A /* Pods-Sample.debug.xcconfig */, + 65F7AC562A910A742522053C /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AC3C4A551A11F47200143C57 = { + isa = PBXGroup; + children = ( + AC3C4A601A11F47200143C57 /* Sample */, + AC3C4A5F1A11F47200143C57 /* Products */, + 90A2B9C5397C46134C8A793B /* Pods */, + D6E38FF0CB18E3F55CF06437 /* Frameworks */, + ); + sourceTree = ""; + }; + AC3C4A5F1A11F47200143C57 /* Products */ = { + isa = PBXGroup; + children = ( + AC3C4A5E1A11F47200143C57 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + AC3C4A601A11F47200143C57 /* Sample */ = { + isa = PBXGroup; + children = ( + AC3C4A651A11F47200143C57 /* AppDelegate.h */, + AC3C4A661A11F47200143C57 /* AppDelegate.m */, + AC3C4A681A11F47200143C57 /* ViewController.h */, + AC3C4A691A11F47200143C57 /* ViewController.m */, + CC14259A219746C2009851B7 /* ASDisplayNode+CatDeals.h */, + CC142598219742CA009851B7 /* ASDisplayNode+CatDeals.mm */, + 7ACD5F941C4847C000E7BE16 /* PlaceholderNetworkImageNode.h */, + 7ACD5F951C4847C000E7BE16 /* PlaceholderNetworkImageNode.m */, + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */, + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */, + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, + AC3C4A611A11F47200143C57 /* Supporting Files */, + 25FDEC901BF31EE700CEB123 /* ItemNode.h */, + 25FDEC911BF31EE700CEB123 /* ItemNode.m */, + 7A8384951C344057002CDD08 /* ItemStyles.h */, + 7A8384961C344057002CDD08 /* ItemStyles.m */, + 7A83848C1C34359D002CDD08 /* ItemViewModel.h */, + 7A83848D1C34359D002CDD08 /* ItemViewModel.m */, + 7A8384911C343680002CDD08 /* BlurbNode.h */, + 7A8384921C343680002CDD08 /* BlurbNode.m */, + 7ACD5F871C415B7500E7BE16 /* LoadingNode.h */, + 7ACD5F881C415B7500E7BE16 /* LoadingNode.m */, + ); + indentWidth = 2; + path = Sample; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + AC3C4A611A11F47200143C57 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + AC3C4A621A11F47200143C57 /* Info.plist */, + AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D6E38FF0CB18E3F55CF06437 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F1E539014E1F516F00A8F167 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AC3C4A5D1A11F47200143C57 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + F868CFBB21824CC9521B6588 /* [CP] Check Pods Manifest.lock */, + AC3C4A5A1A11F47200143C57 /* Sources */, + AC3C4A5B1A11F47200143C57 /* Frameworks */, + AC3C4A5C1A11F47200143C57 /* Resources */, + A6902C454C7661D0D277AC62 /* [CP] Copy Pods Resources */, + B4CD33E927E6F4EE5DD6CCF0 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = AC3C4A5E1A11F47200143C57 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AC3C4A561A11F47200143C57 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + AC3C4A5D1A11F47200143C57 = { + CreatedOnToolsVersion = 6.1; + }; + }; + }; + buildConfigurationList = AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AC3C4A551A11F47200143C57; + productRefGroup = AC3C4A5F1A11F47200143C57 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AC3C4A5D1A11F47200143C57 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AC3C4A5C1A11F47200143C57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A6902C454C7661D0D277AC62 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + B4CD33E927E6F4EE5DD6CCF0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F868CFBB21824CC9521B6588 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AC3C4A5A1A11F47200143C57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CC142599219742CA009851B7 /* ASDisplayNode+CatDeals.mm in Sources */, + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */, + 7ACD5F891C415B7500E7BE16 /* LoadingNode.m in Sources */, + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + 7A8384971C344057002CDD08 /* ItemStyles.m in Sources */, + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */, + 7A8384941C343680002CDD08 /* BlurbNode.m in Sources */, + 7A83848E1C34359D002CDD08 /* ItemViewModel.m in Sources */, + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, + 7ACD5F961C4847C000E7BE16 /* PlaceholderNetworkImageNode.m in Sources */, + AC3C4A641A11F47200143C57 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AC3C4A7F1A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A801A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AC3C4A821A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F3A72D578C378357FF56486A /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A831A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 65F7AC562A910A742522053C /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A7F1A11F47200143C57 /* Debug */, + AC3C4A801A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A821A11F47200143C57 /* Debug */, + AC3C4A831A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AC3C4A561A11F47200143C57 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..f49edc75d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.h new file mode 100644 index 0000000000..a851e58122 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.h @@ -0,0 +1,19 @@ +// +// ASDisplayNode+CatDeals.h +// Sample +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASDisplayNode (CatDeals) + +@property (nullable, copy) NSString *catsLoggingID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm new file mode 100644 index 0000000000..f7154ae5e3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm @@ -0,0 +1,61 @@ +// +// ASDisplayNode+CatDeals.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +#import "ASDisplayNode+CatDeals.h" + +#import + +// A place to store info on any display node in this app. +struct CatDealsNodeContext { + NSString *loggingID = nil; +}; + +// Convenience to cast _displayNodeContext into our struct reference. +NS_INLINE CatDealsNodeContext &GetNodeContext(ASDisplayNode *node) { + return *static_cast(node->_displayNodeContext); +} + +@implementation ASDisplayNode (CatDeals) + +- (void)baseDidInit +{ + _displayNodeContext = new CatDealsNodeContext; +} + +- (void)baseWillDealloc +{ + delete &GetNodeContext(self); +} + +- (void)setCatsLoggingID:(NSString *)catsLoggingID +{ + NSString *copy = [catsLoggingID copy]; + ASLockScopeSelf(); + GetNodeContext(self).loggingID = copy; +} + +- (NSString *)catsLoggingID +{ + ASLockScopeSelf(); + return GetNodeContext(self).loggingID; +} + +- (void)didEnterVisibleState +{ + if (NSString *loggingID = self.catsLoggingID) { + NSLog(@"Visible: %@", loggingID); + } +} + +- (void)didExitVisibleState +{ + if (NSString *loggingID = self.catsLoggingID) { + NSLog(@"NotVisible: %@", loggingID); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/AppDelegate.h new file mode 100644 index 0000000000..db20870a31 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define SIMULATE_WEB_RESPONSE 0 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/AppDelegate.m new file mode 100644 index 0000000000..447ff93b88 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/AppDelegate.m @@ -0,0 +1,49 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "PresentingViewController.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 = [[UINavigationController alloc] init]; + + [self pushNewViewControllerAnimated:NO]; + + [self.window makeKeyAndVisible]; + + return YES; +} + +- (void)pushNewViewControllerAnimated:(BOOL)animated +{ + UINavigationController *navController = (UINavigationController *)self.window.rootViewController; + +#if SIMULATE_WEB_RESPONSE + UIViewController *viewController = [[PresentingViewController alloc] init]; +#else + UIViewController *viewController = [[ViewController alloc] init]; + viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Another Copy" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; +#endif + + [navController pushViewController:viewController animated:animated]; +} + +- (void)pushNewViewController +{ + [self pushNewViewControllerAnimated:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/BlurbNode.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/BlurbNode.h new file mode 100644 index 0000000000..5451fb39f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/BlurbNode.h @@ -0,0 +1,17 @@ +// +// BlurbNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Simple node that displays a placekitten.com attribution. + */ +@interface BlurbNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/BlurbNode.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/BlurbNode.m new file mode 100644 index 0000000000..9c72b6d573 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/BlurbNode.m @@ -0,0 +1,104 @@ +// +// BlurbNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "BlurbNode.h" + +#import +#import + +#import +#import + +static CGFloat kTextPadding = 10.0f; + +@interface BlurbNode () +{ + ASTextNode *_textNode; +} + +@end + + +@implementation BlurbNode + +#pragma mark - +#pragma mark ASCellNode. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + self.backgroundColor = [UIColor lightGrayColor]; + // create a text node + _textNode = [[ASTextNode alloc] init]; + _textNode.maximumNumberOfLines = 2; + + // configure the node to support tappable links + _textNode.delegate = self; + _textNode.userInteractionEnabled = YES; + + // generate an attributed string using the custom link attribute specified above + NSString *blurb = @"Kittens courtesy lorempixel.com \U0001F638 \nTitles courtesy of catipsum.com"; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)]; + [string addAttributes:@{ + NSLinkAttributeName: [NSURL URLWithString:@"http://lorempixel.com/"], + NSForegroundColorAttributeName: [UIColor blueColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } range:[blurb rangeOfString:@"lorempixel.com"]]; + [string addAttributes:@{ + NSLinkAttributeName: [NSURL URLWithString:@"http://www.catipsum.com/"], + NSForegroundColorAttributeName: [UIColor blueColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } range:[blurb rangeOfString:@"catipsum.com"]]; + _textNode.attributedText = string; + + // add it as a subnode, and we're done + [self addSubnode:_textNode]; + + return self; +} + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding = UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; +} + + +#pragma mark - +#pragma mark ASTextNodeDelegate methods. + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // the node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..c5fda8e395 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,9 @@ +{ + "images" : [ + + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json new file mode 100644 index 0000000000..a9e3a5b11b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "cat_face.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png new file mode 100644 index 0000000000..ee4407212c Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png differ diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Info.plist new file mode 100644 index 0000000000..8c57e7a83d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Info.plist @@ -0,0 +1,59 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + http://lorempixel.com + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launchboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemNode.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemNode.h new file mode 100644 index 0000000000..19bfec29cc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemNode.h @@ -0,0 +1,18 @@ +// +// ItemNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ItemViewModel.h" + +@interface ItemNode : ASCellNode + ++ (CGSize)sizeForWidth:(CGFloat)width; ++ (CGSize)preferredViewSize; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemNode.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemNode.m new file mode 100644 index 0000000000..6cd05accdb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemNode.m @@ -0,0 +1,393 @@ +// +// ItemNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ItemNode.h" + +#import "ASDisplayNode+CatDeals.h" +#import "ItemStyles.h" +#import "PlaceholderNetworkImageNode.h" +#import + +static CGFloat ASIsRTL() +{ + static BOOL __isRTL = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __isRTL = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; + }); + return __isRTL; +} + +const CGFloat kFixedLabelsAreaHeight = 96.0; +const CGFloat kDesignWidth = 320.0; +const CGFloat kDesignHeight = 299.0; +const CGFloat kBadgeHeight = 34.0; +const CGFloat kSoldOutGBHeight = 50.0; + +@interface ItemNode() + +@property (nonatomic, strong) ItemViewModel *nodeModel; + +@property (nonatomic, strong) PlaceholderNetworkImageNode *dealImageView; + +@property (nonatomic, strong) ASTextNode *titleLabel; +@property (nonatomic, strong) ASTextNode *firstInfoLabel; +@property (nonatomic, strong) ASTextNode *distanceLabel; +@property (nonatomic, strong) ASTextNode *secondInfoLabel; +@property (nonatomic, strong) ASTextNode *originalPriceLabel; +@property (nonatomic, strong) ASTextNode *finalPriceLabel; +@property (nonatomic, strong) ASTextNode *soldOutLabelFlat; +@property (nonatomic, strong) ASDisplayNode *soldOutLabelBackground; +@property (nonatomic, strong) ASDisplayNode *soldOutOverlay; +@property (nonatomic, strong) ASTextNode *badge; + +@end + +@implementation ItemNode +// Defined on ASCellNode +@dynamic nodeModel; + ++ (void)load +{ + // Need to happen on main thread. + ASIsRTL(); +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + [self setupNodes]; + [self updateBackgroundColor]; + } + return self; +} + +- (void)setNodeModel:(ItemViewModel *)nodeModel +{ + [super setNodeModel:nodeModel]; + + self.catsLoggingID = [NSString stringWithFormat:@"CatDeal#%ld", (long)nodeModel.identifier]; + [self updateLabels]; + [self updateAccessibilityIdentifier]; +} + +- (void)setupNodes +{ + self.dealImageView = [[PlaceholderNetworkImageNode alloc] init]; + self.dealImageView.delegate = self; + self.dealImageView.placeholderEnabled = YES; + self.dealImageView.placeholderImageOverride = [ItemStyles placeholderImage]; + self.dealImageView.defaultImage = [ItemStyles placeholderImage]; + self.dealImageView.contentMode = UIViewContentModeScaleToFill; + self.dealImageView.placeholderFadeDuration = 0.0; + self.dealImageView.layerBacked = YES; + + self.titleLabel = [[ASTextNode alloc] init]; + self.titleLabel.maximumNumberOfLines = 2; + self.titleLabel.style.alignSelf = ASStackLayoutAlignSelfStart; + self.titleLabel.style.flexGrow = 1.0; + self.titleLabel.layerBacked = YES; + + self.firstInfoLabel = [[ASTextNode alloc] init]; + self.firstInfoLabel.maximumNumberOfLines = 1; + self.firstInfoLabel.layerBacked = YES; + + self.secondInfoLabel = [[ASTextNode alloc] init]; + self.secondInfoLabel.maximumNumberOfLines = 1; + self.secondInfoLabel.layerBacked = YES; + + self.distanceLabel = [[ASTextNode alloc] init]; + self.distanceLabel.maximumNumberOfLines = 1; + self.distanceLabel.layerBacked = YES; + + self.originalPriceLabel = [[ASTextNode alloc] init]; + self.originalPriceLabel.maximumNumberOfLines = 1; + self.originalPriceLabel.layerBacked = YES; + + self.finalPriceLabel = [[ASTextNode alloc] init]; + self.finalPriceLabel.maximumNumberOfLines = 1; + self.finalPriceLabel.layerBacked = YES; + + self.badge = [[ASTextNode alloc] init]; + self.badge.hidden = YES; + self.badge.layerBacked = YES; + + self.soldOutLabelFlat = [[ASTextNode alloc] init]; + self.soldOutLabelFlat.layerBacked = YES; + + self.soldOutLabelBackground = [[ASDisplayNode alloc] init]; + self.soldOutLabelBackground.style.width = ASDimensionMakeWithFraction(1.0); + self.soldOutLabelBackground.style.height = ASDimensionMakeWithPoints(kSoldOutGBHeight); + self.soldOutLabelBackground.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; + self.soldOutLabelBackground.style.flexGrow = 1.0; + self.soldOutLabelBackground.layerBacked = YES; + + self.soldOutOverlay = [[ASDisplayNode alloc] init]; + self.soldOutOverlay.style.flexGrow = 1.0; + self.soldOutOverlay.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5]; + self.soldOutOverlay.layerBacked = YES; + + [self addSubnode:self.dealImageView]; + [self addSubnode:self.titleLabel]; + [self addSubnode:self.firstInfoLabel]; + [self addSubnode:self.secondInfoLabel]; + [self addSubnode:self.originalPriceLabel]; + [self addSubnode:self.finalPriceLabel]; + [self addSubnode:self.distanceLabel]; + [self addSubnode:self.badge]; + + [self addSubnode:self.soldOutLabelBackground]; + [self addSubnode:self.soldOutLabelFlat]; + [self addSubnode:self.soldOutOverlay]; + self.soldOutOverlay.hidden = YES; + self.soldOutLabelBackground.hidden = YES; + self.soldOutLabelFlat.hidden = YES; + + if (ASIsRTL()) { + self.titleLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; + self.firstInfoLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; + self.distanceLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; + self.secondInfoLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; + self.originalPriceLabel.style.alignSelf = ASStackLayoutAlignSelfStart; + self.finalPriceLabel.style.alignSelf = ASStackLayoutAlignSelfStart; + } else { + self.firstInfoLabel.style.alignSelf = ASStackLayoutAlignSelfStart; + self.distanceLabel.style.alignSelf = ASStackLayoutAlignSelfStart; + self.secondInfoLabel.style.alignSelf = ASStackLayoutAlignSelfStart; + self.originalPriceLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; + self.finalPriceLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; + } +} + +- (void)updateLabels +{ + // Set Title text + if (self.nodeModel.titleText) { + self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.titleText attributes:[ItemStyles titleStyle]]; + } + if (self.nodeModel.firstInfoText) { + self.firstInfoLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.firstInfoText attributes:[ItemStyles subtitleStyle]]; + } + + if (self.nodeModel.secondInfoText) { + self.secondInfoLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.secondInfoText attributes:[ItemStyles secondInfoStyle]]; + } + if (self.nodeModel.originalPriceText) { + self.originalPriceLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.originalPriceText attributes:[ItemStyles originalPriceStyle]]; + } + if (self.nodeModel.finalPriceText) { + self.finalPriceLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.finalPriceText attributes:[ItemStyles finalPriceStyle]]; + } + if (self.nodeModel.distanceLabelText) { + NSString *format = ASIsRTL() ? @"%@ •" : @"• %@"; + NSString *distanceText = [NSString stringWithFormat:format, self.nodeModel.distanceLabelText]; + + self.distanceLabel.attributedText = [[NSAttributedString alloc] initWithString:distanceText attributes:[ItemStyles distanceStyle]]; + } + + BOOL isSoldOut = self.nodeModel.soldOutText != nil; + + if (isSoldOut) { + NSString *soldOutText = self.nodeModel.soldOutText; + self.soldOutLabelFlat.attributedText = [[NSAttributedString alloc] initWithString:soldOutText attributes:[ItemStyles soldOutStyle]]; + } + self.soldOutOverlay.hidden = !isSoldOut; + self.soldOutLabelFlat.hidden = !isSoldOut; + self.soldOutLabelBackground.hidden = !isSoldOut; + + BOOL hasBadge = self.nodeModel.badgeText != nil; + if (hasBadge) { + self.badge.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.badgeText attributes:[ItemStyles badgeStyle]]; + self.badge.backgroundColor = [ItemStyles badgeColor]; + } + self.badge.hidden = !hasBadge; +} + +- (void)updateAccessibilityIdentifier +{ + ASSetDebugName(self, @"Item #%zd", self.nodeModel.identifier); + self.accessibilityIdentifier = self.nodeModel.titleText; +} + +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.3]; + } else if (self.selected) { + self.backgroundColor = [UIColor lightGrayColor]; + } else { + self.backgroundColor = [UIColor whiteColor]; + } +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self updateBackgroundColor]; +} + +#pragma mark - ASDisplayNode + +- (void)displayWillStart +{ + [super displayWillStart]; + [self didEnterPreloadState]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + if (self.nodeModel) { + [self loadImage]; + } +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { + + ASLayoutSpec *textSpec = [self textSpec]; + ASLayoutSpec *imageSpec = [self imageSpecWithSize:constrainedSize]; + ASOverlayLayoutSpec *soldOutOverImage = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imageSpec overlay:[self soldOutLabelSpec]]; + + NSArray *stackChildren = @[soldOutOverImage, textSpec]; + + ASStackLayoutSpec *mainStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch children:stackChildren]; + + ASOverlayLayoutSpec *soldOutOverlay = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:mainStack overlay:self.soldOutOverlay]; + + return soldOutOverlay; +} + +- (ASLayoutSpec *)textSpec +{ + CGFloat kInsetHorizontal = 16.0; + CGFloat kInsetTop = 6.0; + CGFloat kInsetBottom = 0.0; + + UIEdgeInsets textInsets = UIEdgeInsetsMake(kInsetTop, kInsetHorizontal, kInsetBottom, kInsetHorizontal); + + ASLayoutSpec *verticalSpacer = [[ASLayoutSpec alloc] init]; + verticalSpacer.style.flexGrow = 1.0; + + ASLayoutSpec *horizontalSpacer1 = [[ASLayoutSpec alloc] init]; + horizontalSpacer1.style.flexGrow = 1.0; + + ASLayoutSpec *horizontalSpacer2 = [[ASLayoutSpec alloc] init]; + horizontalSpacer2.style.flexGrow = 1.0; + + NSArray *info1Children = @[self.firstInfoLabel, self.distanceLabel, horizontalSpacer1, self.originalPriceLabel]; + NSArray *info2Children = @[self.secondInfoLabel, horizontalSpacer2, self.finalPriceLabel]; + if (ASIsRTL()) { + info1Children = [[info1Children reverseObjectEnumerator] allObjects]; + info2Children = [[info2Children reverseObjectEnumerator] allObjects]; + } + + ASStackLayoutSpec *info1Stack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:1.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsBaselineLast children:info1Children]; + + ASStackLayoutSpec *info2Stack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentCenter alignItems:ASStackLayoutAlignItemsBaselineLast children:info2Children]; + + ASStackLayoutSpec *textStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:0.0 justifyContent:ASStackLayoutJustifyContentEnd alignItems:ASStackLayoutAlignItemsStretch children:@[self.titleLabel, verticalSpacer, info1Stack, info2Stack]]; + + ASInsetLayoutSpec *textWrapper = [ASInsetLayoutSpec insetLayoutSpecWithInsets:textInsets child:textStack]; + textWrapper.style.flexGrow = 1.0; + + return textWrapper; +} + +- (ASLayoutSpec *)imageSpecWithSize:(ASSizeRange)constrainedSize +{ + CGFloat imageRatio = [self imageRatioFromSize:constrainedSize.max]; + + ASRatioLayoutSpec *imagePlace = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:imageRatio child:self.dealImageView]; + + self.badge.style.layoutPosition = CGPointMake(0, constrainedSize.max.height - kFixedLabelsAreaHeight - kBadgeHeight); + self.badge.style.height = ASDimensionMakeWithPoints(kBadgeHeight); + ASAbsoluteLayoutSpec *badgePosition = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[self.badge]]; + + ASOverlayLayoutSpec *badgeOverImage = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imagePlace overlay:badgePosition]; + badgeOverImage.style.flexGrow = 1.0; + + return badgeOverImage; +} + +- (ASLayoutSpec *)soldOutLabelSpec { + ASCenterLayoutSpec *centerSoldOutLabel = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:self.soldOutLabelFlat]; + ASCenterLayoutSpec *centerSoldOut = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.soldOutLabelBackground]; + ASBackgroundLayoutSpec *soldOutLabelOverBackground = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:centerSoldOutLabel background:centerSoldOut]; + return soldOutLabelOverBackground; +} + ++ (CGSize)sizeForWidth:(CGFloat)width +{ + CGFloat height = [self scaledHeightForPreferredSize:[self preferredViewSize] scaledWidth:width]; + return CGSizeMake(width, height); +} + + ++ (CGSize)preferredViewSize +{ + return CGSizeMake(kDesignWidth, kDesignHeight); +} + ++ (CGFloat)scaledHeightForPreferredSize:(CGSize)preferredSize scaledWidth:(CGFloat)scaledWidth +{ + CGFloat scale = scaledWidth / kDesignWidth; + CGFloat scaledHeight = ceilf(scale * (kDesignHeight - kFixedLabelsAreaHeight)) + kFixedLabelsAreaHeight; + + return scaledHeight; +} + +#pragma mark - Image + +- (CGFloat)imageRatioFromSize:(CGSize)size +{ + CGFloat imageHeight = size.height - kFixedLabelsAreaHeight; + CGFloat imageRatio = imageHeight / size.width; + + return imageRatio; +} + +- (CGSize)imageSize +{ + if (!CGSizeEqualToSize(self.dealImageView.frame.size, CGSizeZero)) { + return self.dealImageView.frame.size; + } else if (!CGSizeEqualToSize(self.calculatedSize, CGSizeZero)) { + CGFloat imageRatio = [self imageRatioFromSize:self.calculatedSize]; + CGFloat imageWidth = self.calculatedSize.width; + return CGSizeMake(imageWidth, imageRatio * imageWidth); + } else { + return CGSizeZero; + } +} + +- (void)loadImage +{ + CGSize imageSize = [self imageSize]; + if (CGSizeEqualToSize(CGSizeZero, imageSize)) { + return; + } + + NSURL *url = [self.nodeModel imageURLWithSize:imageSize]; + + // if we're trying to set the deal image to what it already was, skip the work + if ([[url absoluteString] isEqualToString:[self.dealImageView.URL absoluteString]]) { + return; + } + + // Clear the flag that says we've loaded our image + [self.dealImageView setURL:url]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemStyles.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemStyles.h new file mode 100644 index 0000000000..db4d0d08d5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemStyles.h @@ -0,0 +1,24 @@ +// +// ItemStyles.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ItemStyles : NSObject ++ (NSDictionary *)titleStyle; ++ (NSDictionary *)subtitleStyle; ++ (NSDictionary *)distanceStyle; ++ (NSDictionary *)secondInfoStyle; ++ (NSDictionary *)originalPriceStyle; ++ (NSDictionary *)finalPriceStyle; ++ (NSDictionary *)soldOutStyle; ++ (NSDictionary *)badgeStyle; ++ (UIColor *)badgeColor; ++ (UIImage *)placeholderImage; +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemStyles.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemStyles.m new file mode 100644 index 0000000000..1659341161 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemStyles.m @@ -0,0 +1,95 @@ +// +// ItemStyles.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ItemStyles.h" + +const CGFloat kTitleFontSize = 20.0; +const CGFloat kInfoFontSize = 14.0; + +UIColor *kTitleColor; +UIColor *kInfoColor; +UIColor *kFinalPriceColor; +UIFont *kTitleFont; +UIFont *kInfoFont; + +@implementation ItemStyles + ++ (void)initialize { + if (self == [ItemStyles class]) { + kTitleColor = [UIColor darkGrayColor]; + kInfoColor = [UIColor grayColor]; + kFinalPriceColor = [UIColor greenColor]; + kTitleFont = [UIFont boldSystemFontOfSize:kTitleFontSize]; + kInfoFont = [UIFont systemFontOfSize:kInfoFontSize]; + } +} + ++ (NSDictionary *)titleStyle { + // Title Label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:kTitleColor }; +} + ++ (NSDictionary *)subtitleStyle { + // First Subtitle + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor }; +} + ++ (NSDictionary *)distanceStyle { + // Distance Label + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor}; +} + ++ (NSDictionary *)secondInfoStyle { + // Second Subtitle + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor}; +} + ++ (NSDictionary *)originalPriceStyle { + // Original price + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor, + NSStrikethroughStyleAttributeName:@(NSUnderlineStyleSingle)}; +} + ++ (NSDictionary *)finalPriceStyle { + // Discounted / Claimable price label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:kFinalPriceColor}; +} + ++ (NSDictionary *)soldOutStyle { + // Setup Sold Out Label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:kTitleColor}; +} + ++ (NSDictionary *)badgeStyle { + // Setup Sold Out Label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:[UIColor whiteColor]}; +} + ++ (UIColor *)badgeColor { + return [[UIColor purpleColor] colorWithAlphaComponent:0.4]; +} + ++ (UIImage *)placeholderImage { + static UIImage *__catFace = nil; + static dispatch_once_t onceToken; + dispatch_once (&onceToken, ^{ + __catFace = [UIImage imageNamed:@"cat_face"]; + }); + return __catFace; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemViewModel.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemViewModel.h new file mode 100644 index 0000000000..489a10515b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemViewModel.h @@ -0,0 +1,29 @@ +// +// ItemViewModel.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ItemViewModel : NSObject + ++ (ItemViewModel *)randomItem; + +@property (nonatomic, readonly) NSInteger identifier; +@property (nonatomic, copy) NSString *titleText; +@property (nonatomic, copy) NSString *firstInfoText; +@property (nonatomic, copy) NSString *secondInfoText; +@property (nonatomic, copy) NSString *originalPriceText; +@property (nonatomic, copy) NSString *finalPriceText; +@property (nonatomic, copy) NSString *soldOutText; +@property (nonatomic, copy) NSString *distanceLabelText; +@property (nonatomic, copy) NSString *badgeText; + +- (NSURL *)imageURLWithSize:(CGSize)size; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemViewModel.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemViewModel.m new file mode 100644 index 0000000000..8f60900c60 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ItemViewModel.m @@ -0,0 +1,103 @@ +// +// ItemViewModel.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ItemViewModel.h" +#import + +NSArray *titles; +NSArray *firstInfos; +NSArray *badges; + +@interface ItemViewModel() + +@property (nonatomic, assign) NSInteger catNumber; +@property (nonatomic, assign) NSInteger labelNumber; + +@end + +@implementation ItemViewModel + ++ (ItemViewModel *)randomItem { + return [[ItemViewModel alloc] init]; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + static _Atomic(NSInteger) nextID = ATOMIC_VAR_INIT(1); + _identifier = atomic_fetch_add(&nextID, 1); + _titleText = [self randomObjectFromArray:titles]; + _firstInfoText = [self randomObjectFromArray:firstInfos]; + _secondInfoText = [NSString stringWithFormat:@"%zd+ bought", [self randomNumberInRange:5 to:6000]]; + _originalPriceText = [NSString stringWithFormat:@"$%zd", [self randomNumberInRange:40 to:90]]; + _finalPriceText = [NSString stringWithFormat:@"$%zd", [self randomNumberInRange:5 to:30]]; + _soldOutText = (arc4random() % 5 == 0) ? @"SOLD OUT" : nil; + _distanceLabelText = [NSString stringWithFormat:@"%zd mi", [self randomNumberInRange:1 to:20]]; + if (arc4random() % 2 == 0) { + _badgeText = [self randomObjectFromArray:badges]; + } + _catNumber = [self randomNumberInRange:1 to:10]; + _labelNumber = [self randomNumberInRange:1 to:10000]; + } + return self; +} + +- (NSURL *)imageURLWithSize:(CGSize)size +{ + NSString *imageText = [NSString stringWithFormat:@"Fun cat pic %zd", self.labelNumber]; + NSString *urlString = [NSString stringWithFormat:@"http://lorempixel.com/%zd/%zd/cats/%zd/%@", + (NSInteger)roundl(size.width), + (NSInteger)roundl(size.height), self.catNumber, imageText]; + + urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]]; + return [NSURL URLWithString:urlString]; +} + +// titles courtesy of http://www.catipsum.com/ ++ (void)initialize +{ + titles = @[@"Leave fur on owners clothes intrigued by the shower", + @"Meowwww", + @"Immediately regret falling into bathtub stare out the window", + @"Jump launch to pounce upon little yarn mouse, bare fangs at toy run hide in litter box until treats are fed", + @"Sleep nap", + @"Lick butt", + @"Chase laser lick arm hair present belly, scratch hand when stroked"]; + firstInfos = @[@"Kitty Shop", + @"Cat's r us", + @"Fantastic Felines", + @"The Cat Shop", + @"Cat in a hat", + @"Cat-tastic" + ]; + + badges = @[@"ADORABLE", + @"BOUNCES", + @"HATES CUCUMBERS", + @"SCRATCHY" + ]; +} + + +- (id)randomObjectFromArray:(NSArray *)strings +{ + u_int32_t ipsumCount = (u_int32_t)[strings count]; + u_int32_t location = arc4random_uniform(ipsumCount); + + return strings[location]; +} + +- (uint32_t)randomNumberInRange:(uint32_t)start to:(uint32_t)end { + + return start + arc4random_uniform(end - start); +} + + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Launchboard.storyboard b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/LoadingNode.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/LoadingNode.h new file mode 100644 index 0000000000..996a6798cf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/LoadingNode.h @@ -0,0 +1,14 @@ +// +// LoadingNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface LoadingNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/LoadingNode.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/LoadingNode.m new file mode 100644 index 0000000000..18681c011c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/LoadingNode.m @@ -0,0 +1,47 @@ +// +// LoadingNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "LoadingNode.h" + +#import + +@implementation LoadingNode { + ASDisplayNode *_loadingSpinner; +} + +#pragma mark - ASCellNode + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _loadingSpinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{ + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [spinner startAnimating]; + return spinner; + }]; + _loadingSpinner.style.preferredSize = CGSizeMake(50, 50); + + // add it as a subnode, and we're done + [self addSubnode:_loadingSpinner]; + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringXY; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionDefault; + centerSpec.child = _loadingSpinner; + return centerSpec; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h new file mode 100644 index 0000000000..a18e17c9ea --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h @@ -0,0 +1,16 @@ +// +// PlaceholderNetworkImageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface PlaceholderNetworkImageNode : ASNetworkImageNode + +@property (nonatomic, strong) UIImage *placeholderImageOverride; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m new file mode 100644 index 0000000000..81129392a9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m @@ -0,0 +1,19 @@ +// +// PlaceholderNetworkImageNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PlaceholderNetworkImageNode.h" + +@implementation PlaceholderNetworkImageNode + +- (UIImage *)placeholderImage +{ + return self.placeholderImageOverride; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PresentingViewController.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PresentingViewController.h new file mode 100644 index 0000000000..44c07c6a76 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PresentingViewController.h @@ -0,0 +1,14 @@ +// +// PresentingViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface PresentingViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PresentingViewController.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PresentingViewController.m new file mode 100644 index 0000000000..216cf4dd28 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/PresentingViewController.m @@ -0,0 +1,31 @@ +// +// PresentingViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PresentingViewController.h" +#import "ViewController.h" + +@interface PresentingViewController () + +@end + +@implementation PresentingViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Details" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; +} + +- (void)pushNewViewController +{ + ViewController *controller = [[ViewController alloc] init]; + [self.navigationController pushViewController:controller animated:true]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ViewController.h new file mode 100644 index 0000000000..560b6a2d03 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ViewController.m new file mode 100644 index 0000000000..c1f9111ad0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/ViewController.m @@ -0,0 +1,221 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import "ItemNode.h" +#import "BlurbNode.h" +#import "LoadingNode.h" + +static const NSTimeInterval kWebResponseDelay = 1.0; +static const BOOL kSimulateWebResponse = YES; +static const NSInteger kBatchSize = 20; + +static const CGFloat kHorizontalSectionPadding = 10.0f; + +@interface ViewController () +{ + ASCollectionNode *_collectionNode; + NSMutableArray *_data; +} + +@end + + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + + self = [super initWithNode:_collectionNode]; + + if (self) { + self.title = @"Cat Deals"; + + _collectionNode.dataSource = self; + _collectionNode.delegate = self; + _collectionNode.backgroundColor = [UIColor grayColor]; + _collectionNode.accessibilityIdentifier = @"Cat deals list"; + + ASRangeTuningParameters preloadTuning; + preloadTuning.leadingBufferScreenfuls = 2; + preloadTuning.trailingBufferScreenfuls = 1; + [_collectionNode setTuningParameters:preloadTuning forRangeType:ASLayoutRangeTypePreload]; + + ASRangeTuningParameters displayTuning; + displayTuning.leadingBufferScreenfuls = 1; + displayTuning.trailingBufferScreenfuls = 0.5; + [_collectionNode setTuningParameters:displayTuning forRangeType:ASLayoutRangeTypeDisplay]; + + [_collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [_collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; + + _data = [[NSMutableArray alloc] init]; + + self.navigationItem.leftItemsSupplementBackButton = YES; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadTapped)]; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // set any collectionView properties here (once the node's backing view is loaded) + _collectionNode.leadingScreensForBatching = 2; + [self fetchMoreCatsWithCompletion:nil]; +} + +- (void)fetchMoreCatsWithCompletion:(void (^)(BOOL))completion +{ + if (kSimulateWebResponse) { + __weak typeof(self) weakSelf = self; + void(^mockWebService)() = ^{ + NSLog(@"ViewController \"got data from a web service\""); + ViewController *strongSelf = weakSelf; + if (strongSelf != nil) + { + [strongSelf appendMoreItems:kBatchSize completion:completion]; + } + else { + NSLog(@"ViewController is nil - won't update collection"); + } + }; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kWebResponseDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), mockWebService); + } else { + [self appendMoreItems:kBatchSize completion:completion]; + } +} + +- (void)appendMoreItems:(NSInteger)numberOfNewItems completion:(void (^)(BOOL))completion +{ + NSArray *newData = [self getMoreData:numberOfNewItems]; + [_collectionNode performBatchAnimated:YES updates:^{ + [_data addObjectsFromArray:newData]; + NSArray *addedIndexPaths = [self indexPathsForObjects:newData]; + [_collectionNode insertItemsAtIndexPaths:addedIndexPaths]; + } completion:completion]; +} + +- (NSArray *)getMoreData:(NSInteger)count +{ + NSMutableArray *data = [NSMutableArray array]; + for (int i = 0; i < count; i++) { + [data addObject:[ItemViewModel randomItem]]; + } + return data; +} + +- (NSArray *)indexPathsForObjects:(NSArray *)data +{ + NSMutableArray *indexPaths = [NSMutableArray array]; + NSInteger section = 0; + for (ItemViewModel *viewModel in data) { + NSInteger item = [_data indexOfObject:viewModel]; + NSAssert(item < [_data count] && item != NSNotFound, @"Item should be in _data"); + [indexPaths addObject:[NSIndexPath indexPathForItem:item inSection:section]]; + } + return indexPaths; +} + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [_collectionNode.view.collectionViewLayout invalidateLayout]; +} + +- (void)reloadTapped +{ + [_collectionNode reloadData]; +} + +#pragma mark - ASCollectionNodeDelegate / ASCollectionNodeDataSource + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + return [[ItemNode alloc] init]; + }; +} + +- (id)collectionNode:(ASCollectionNode *)collectionNode nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return _data[indexPath.item]; +} + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if ([kind isEqualToString:UICollectionElementKindSectionHeader] && indexPath.section == 0) { + return [[BlurbNode alloc] init]; + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter] && indexPath.section == 0) { + return [[LoadingNode alloc] init]; + } + return nil; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat collectionViewWidth = CGRectGetWidth(self.view.frame) - 2 * kHorizontalSectionPadding; + CGFloat oneItemWidth = [ItemNode preferredViewSize].width; + NSInteger numColumns = floor(collectionViewWidth / oneItemWidth); + // Number of columns should be at least 1 + numColumns = MAX(1, numColumns); + + CGFloat totalSpaceBetweenColumns = (numColumns - 1) * kHorizontalSectionPadding; + CGFloat itemWidth = ((collectionViewWidth - totalSpaceBetweenColumns) / numColumns); + CGSize itemSize = [ItemNode sizeForWidth:itemWidth]; + return ASSizeRangeMake(itemSize, itemSize); +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return [_data count]; +} + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ + return 1; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + [self fetchMoreCatsWithCompletion:^(BOOL finished){ + [context completeBatchFetching:YES]; + }]; +} + +#pragma mark - ASCollectionDelegateFlowLayout + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForHeaderInSection:(NSInteger)section +{ + if (section == 0) { + return ASSizeRangeUnconstrained; + } else { + return ASSizeRangeZero; + } +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForFooterInSection:(NSInteger)section +{ + if (section == 0) { + return ASSizeRangeUnconstrained; + } else { + return ASSizeRangeZero; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/main.m b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/main.m new file mode 100644 index 0000000000..65850400e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CatDealsCollectionView/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Podfile b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Podfile new file mode 100644 index 0000000000..92a9acc9c8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' + +use_frameworks! + +target 'Sample' do + pod 'Texture', :path => '../..' +end \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj new file mode 100755 index 0000000000..e1912f909f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,407 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 27F2D2683285DCB73EE734BB /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7DA4A9952245B7E9BA8201F /* Pods_Sample.framework */; }; + 5D823AD51DD3B7770075E14A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D823AD41DD3B7770075E14A /* AppDelegate.swift */; }; + 5D823AD71DD3B7770075E14A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D823AD61DD3B7770075E14A /* ViewController.swift */; }; + 5D823ADC1DD3B7770075E14A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D823ADB1DD3B7770075E14A /* Assets.xcassets */; }; + 5D823ADF1DD3B7770075E14A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D823ADD1DD3B7770075E14A /* LaunchScreen.storyboard */; }; + 5D823AE71DD3B7D30075E14A /* MosaicCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D823AE61DD3B7D30075E14A /* MosaicCollectionViewLayout.swift */; }; + 5D823AE91DD3B7D70075E14A /* ImageCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D823AE81DD3B7D70075E14A /* ImageCellNode.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 5D823AD11DD3B7770075E14A /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5D823AD41DD3B7770075E14A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5D823AD61DD3B7770075E14A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 5D823ADB1DD3B7770075E14A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5D823ADE1DD3B7770075E14A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 5D823AE01DD3B7770075E14A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5D823AE61DD3B7D30075E14A /* MosaicCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicCollectionViewLayout.swift; sourceTree = ""; }; + 5D823AE81DD3B7D70075E14A /* ImageCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCellNode.swift; sourceTree = ""; }; + 73F1C0E45062A0A8CDC033A1 /* 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 = ""; }; + A9E9A143FF858FD89A482A84 /* 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 = ""; }; + F7DA4A9952245B7E9BA8201F /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5D823ACE1DD3B7770075E14A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 27F2D2683285DCB73EE734BB /* Pods_Sample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3A21D910663270E99063573E /* Pods */ = { + isa = PBXGroup; + children = ( + A9E9A143FF858FD89A482A84 /* Pods-Sample.debug.xcconfig */, + 73F1C0E45062A0A8CDC033A1 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 5D823AC81DD3B7760075E14A = { + isa = PBXGroup; + children = ( + 5D823AD31DD3B7770075E14A /* Sample */, + 5D823AD21DD3B7770075E14A /* Products */, + 3A21D910663270E99063573E /* Pods */, + 728C7877715727493EFEE42D /* Frameworks */, + ); + sourceTree = ""; + }; + 5D823AD21DD3B7770075E14A /* Products */ = { + isa = PBXGroup; + children = ( + 5D823AD11DD3B7770075E14A /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 5D823AD31DD3B7770075E14A /* Sample */ = { + isa = PBXGroup; + children = ( + 5D823AD41DD3B7770075E14A /* AppDelegate.swift */, + 5D823AD61DD3B7770075E14A /* ViewController.swift */, + 5D823AE81DD3B7D70075E14A /* ImageCellNode.swift */, + 5D823AE61DD3B7D30075E14A /* MosaicCollectionViewLayout.swift */, + 5D823ADB1DD3B7770075E14A /* Assets.xcassets */, + 5D823ADD1DD3B7770075E14A /* LaunchScreen.storyboard */, + 5D823AE01DD3B7770075E14A /* Info.plist */, + ); + path = Sample; + sourceTree = ""; + }; + 728C7877715727493EFEE42D /* Frameworks */ = { + isa = PBXGroup; + children = ( + F7DA4A9952245B7E9BA8201F /* Pods_Sample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5D823AD01DD3B7770075E14A /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5D823AE31DD3B7770075E14A /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + BAA73690D42731AA5D8001CF /* [CP] Check Pods Manifest.lock */, + 5D823ACD1DD3B7770075E14A /* Sources */, + 5D823ACE1DD3B7770075E14A /* Frameworks */, + 5D823ACF1DD3B7770075E14A /* Resources */, + 8293091514A70C5E7E487A36 /* [CP] Embed Pods Frameworks */, + 641DF857294FFEAA1878D05C /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 5D823AD11DD3B7770075E14A /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5D823AC91DD3B7760075E14A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0810; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = AsyncDisplayKit; + TargetAttributes = { + 5D823AD01DD3B7770075E14A = { + CreatedOnToolsVersion = 8.1; + DevelopmentTeam = 888KTQ92ZP; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 5D823ACC1DD3B7760075E14A /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5D823AC81DD3B7760075E14A; + productRefGroup = 5D823AD21DD3B7770075E14A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5D823AD01DD3B7770075E14A /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5D823ACF1DD3B7770075E14A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5D823ADF1DD3B7770075E14A /* LaunchScreen.storyboard in Resources */, + 5D823ADC1DD3B7770075E14A /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 641DF857294FFEAA1878D05C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8293091514A70C5E7E487A36 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BAA73690D42731AA5D8001CF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5D823ACD1DD3B7770075E14A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5D823AE71DD3B7D30075E14A /* MosaicCollectionViewLayout.swift in Sources */, + 5D823AD71DD3B7770075E14A /* ViewController.swift in Sources */, + 5D823AD51DD3B7770075E14A /* AppDelegate.swift in Sources */, + 5D823AE91DD3B7D70075E14A /* ImageCellNode.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 5D823ADD1DD3B7770075E14A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5D823ADE1DD3B7770075E14A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5D823AE11DD3B7770075E14A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 5D823AE21DD3B7770075E14A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5D823AE41DD3B7770075E14A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A9E9A143FF858FD89A482A84 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 888KTQ92ZP; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 5D823AE51DD3B7770075E14A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 73F1C0E45062A0A8CDC033A1 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 888KTQ92ZP; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5D823ACC1DD3B7760075E14A /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5D823AE11DD3B7770075E14A /* Debug */, + 5D823AE21DD3B7770075E14A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5D823AE31DD3B7770075E14A /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5D823AE41DD3B7770075E14A /* Debug */, + 5D823AE51DD3B7770075E14A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5D823AC91DD3B7760075E14A /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..ec900dc24f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/AppDelegate.swift b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/AppDelegate.swift new file mode 100644 index 0000000000..5556f8c77e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/AppDelegate.swift @@ -0,0 +1,54 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = .white + window.rootViewController = ViewController() + window.makeKeyAndVisible() + + self.window = window + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // 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 invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // 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. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // 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. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..b8236c6534 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,48 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/Contents.json new file mode 100644 index 0000000000..09ec0851ee --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_0.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/image_0.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/image_0.jpg new file mode 100644 index 0000000000..4a365897ea Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/image_0.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/Contents.json new file mode 100644 index 0000000000..6d2e9f5f7c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_1.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/image_1.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/image_1.jpg new file mode 100644 index 0000000000..5cb4828f44 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/image_1.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/Contents.json new file mode 100644 index 0000000000..ea10700189 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_10.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/image_10.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/image_10.jpg new file mode 100644 index 0000000000..ea5cd6d268 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/image_10.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/Contents.json new file mode 100644 index 0000000000..dc85469057 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_11.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/image_11.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/image_11.jpg new file mode 100644 index 0000000000..e93c68e512 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/image_11.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/Contents.json new file mode 100644 index 0000000000..a6d99003d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_12.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/image_12.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/image_12.jpg new file mode 100644 index 0000000000..d520b6d80f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/image_12.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/Contents.json new file mode 100644 index 0000000000..4eb6baad3b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_13.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/image_13.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/image_13.jpg new file mode 100644 index 0000000000..c0232370cd Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/image_13.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/Contents.json new file mode 100644 index 0000000000..b2536e53de --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_2.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/image_2.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/image_2.jpg new file mode 100644 index 0000000000..175343454d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/image_2.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/Contents.json new file mode 100644 index 0000000000..512e735090 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_3.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/image_3.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/image_3.jpg new file mode 100644 index 0000000000..f5398cac79 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/image_3.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/Contents.json new file mode 100644 index 0000000000..88b2b7b98a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_4.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/image_4.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/image_4.jpg new file mode 100644 index 0000000000..2a6fe4c264 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/image_4.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/Contents.json new file mode 100644 index 0000000000..1f24c086d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_5.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/image_5.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/image_5.jpg new file mode 100644 index 0000000000..4e507b8064 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/image_5.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/Contents.json new file mode 100644 index 0000000000..25f33f2acd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_6.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/image_6.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/image_6.jpg new file mode 100644 index 0000000000..35fe778b3a Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/image_6.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/Contents.json new file mode 100644 index 0000000000..5fdd6ba2cf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_7.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/image_7.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/image_7.jpg new file mode 100644 index 0000000000..8f5e037722 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/image_7.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/Contents.json new file mode 100644 index 0000000000..563d5ba824 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_8.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/image_8.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/image_8.jpg new file mode 100644 index 0000000000..5651436bb6 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/image_8.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/Contents.json new file mode 100644 index 0000000000..66c1b859b1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_9.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/image_9.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/image_9.jpg new file mode 100644 index 0000000000..9fb6e47d3f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/image_9.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..fdf3f97d1b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/ImageCellNode.swift b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/ImageCellNode.swift new file mode 100644 index 0000000000..70c1692679 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/ImageCellNode.swift @@ -0,0 +1,38 @@ +// +// ImageCellNode.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +class ImageCellNode: ASCellNode { + let imageNode = ASImageNode() + required init(with image : UIImage) { + super.init() + imageNode.image = image + self.addSubnode(self.imageNode) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + var imageRatio: CGFloat = 0.5 + if imageNode.image != nil { + imageRatio = (imageNode.image?.size.height)! / (imageNode.image?.size.width)! + } + + let imagePlace = ASRatioLayoutSpec(ratio: imageRatio, child: imageNode) + + let stackLayout = ASStackLayoutSpec.horizontal() + stackLayout.justifyContent = .start + stackLayout.alignItems = .start + stackLayout.style.flexShrink = 1.0 + stackLayout.children = [imagePlace] + + return ASInsetLayoutSpec(insets: UIEdgeInsets.zero, child: stackLayout) + } + +} diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Info.plist new file mode 100755 index 0000000000..b8901eef4f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/MosaicCollectionViewLayout.swift b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/MosaicCollectionViewLayout.swift new file mode 100644 index 0000000000..976481c8d2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/MosaicCollectionViewLayout.swift @@ -0,0 +1,246 @@ +// +// MosaicCollectionViewLayout.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation +import UIKit +import AsyncDisplayKit + +protocol MosaicCollectionViewLayoutDelegate: ASCollectionDelegate { + func collectionView(_ collectionView: UICollectionView, layout: MosaicCollectionViewLayout, originalItemSizeAtIndexPath: IndexPath) -> CGSize +} + +class MosaicCollectionViewLayout: UICollectionViewFlowLayout { + var numberOfColumns: Int + var columnSpacing: CGFloat + var _sectionInset: UIEdgeInsets + var interItemSpacing: UIEdgeInsets + var headerHeight: CGFloat + var _columnHeights: [[CGFloat]]? + var _itemAttributes = [[UICollectionViewLayoutAttributes]]() + var _headerAttributes = [UICollectionViewLayoutAttributes]() + var _allAttributes = [UICollectionViewLayoutAttributes]() + + required override init() { + self.numberOfColumns = 2 + self.columnSpacing = 10.0 + self.headerHeight = 44.0 //viewcontroller + self._sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) + self.interItemSpacing = UIEdgeInsetsMake(10.0, 0, 10.0, 0) + super.init() + self.scrollDirection = .vertical + } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public var delegate : MosaicCollectionViewLayoutDelegate? + + override func prepare() { + super.prepare() + guard let collectionView = self.collectionView else { return } + + _itemAttributes = [] + _allAttributes = [] + _headerAttributes = [] + _columnHeights = [] + + var top: CGFloat = 0 + + let numberOfSections: NSInteger = collectionView.numberOfSections + + for section in 0 ..< numberOfSections { + let numberOfItems = collectionView.numberOfItems(inSection: section) + + top += _sectionInset.top + + if (headerHeight > 0) { + let headerSize: CGSize = self._headerSizeForSection(section: section) + + let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: NSIndexPath(row: 0, section: section) as IndexPath) + + attributes.frame = CGRect(x: _sectionInset.left, y: top, width: headerSize.width, height: headerSize.height) + _headerAttributes.append(attributes) + _allAttributes.append(attributes) + top = attributes.frame.maxY + } + + _columnHeights?.append([]) //Adding new Section + for _ in 0 ..< self.numberOfColumns { + self._columnHeights?[section].append(top) + } + + let columnWidth = self._columnWidthForSection(section: section) + _itemAttributes.append([]) + for idx in 0 ..< numberOfItems { + let columnIndex: Int = self._shortestColumnIndexInSection(section: section) + let indexPath = IndexPath(item: idx, section: section) + + let itemSize = self._itemSizeAtIndexPath(indexPath: indexPath); + let xOffset = _sectionInset.left + (columnWidth + columnSpacing) * CGFloat(columnIndex) + let yOffset = _columnHeights![section][columnIndex] + + let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) + + attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height) + + _columnHeights?[section][columnIndex] = attributes.frame.maxY + interItemSpacing.bottom + + _itemAttributes[section].append(attributes) + _allAttributes.append(attributes) + } + + let columnIndex: Int = self._tallestColumnIndexInSection(section: section) + top = (_columnHeights?[section][columnIndex])! - interItemSpacing.bottom + _sectionInset.bottom + + for idx in 0 ..< _columnHeights![section].count { + _columnHeights![section][idx] = top + } + } + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? + { + var includedAttributes: [UICollectionViewLayoutAttributes] = [] + // Slow search for small batches + for attribute in _allAttributes { + if (attribute.frame.intersects(rect)) { + includedAttributes.append(attribute) + } + } + return includedAttributes + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? + { + guard indexPath.section < _itemAttributes.count, + indexPath.item < _itemAttributes[indexPath.section].count + else { + return nil + } + return _itemAttributes[indexPath.section][indexPath.item] + } + + override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? + { + if (elementKind == UICollectionElementKindSectionHeader) { + return _headerAttributes[indexPath.section] + } + return nil + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if (!(self.collectionView?.bounds.size.equalTo(newBounds.size))!) { + return true; + } + return false; + } + + func _widthForSection (section: Int) -> CGFloat + { + return self.collectionView!.bounds.size.width - _sectionInset.left - _sectionInset.right; + } + + func _columnWidthForSection(section: Int) -> CGFloat + { + return (self._widthForSection(section: section) - ((CGFloat(numberOfColumns - 1)) * columnSpacing)) / CGFloat(numberOfColumns) + } + + func _itemSizeAtIndexPath(indexPath: IndexPath) -> CGSize + { + var size = CGSize(width: self._columnWidthForSection(section: indexPath.section), height: 0) + let originalSize = self.delegate!.collectionView(self.collectionView!, layout:self, originalItemSizeAtIndexPath:indexPath) + if (originalSize.height > 0 && originalSize.width > 0) { + size.height = originalSize.height / originalSize.width * size.width + } + return size + } + + func _headerSizeForSection(section: Int) -> CGSize + { + return CGSize(width: self._widthForSection(section: section), height: headerHeight) + } + + override var collectionViewContentSize: CGSize + { + var height: CGFloat = 0 + if ((_columnHeights?.count)! > 0) { + if (_columnHeights?[(_columnHeights?.count)!-1].count)! > 0 { + height = (_columnHeights?[(_columnHeights?.count)!-1][0])! + } + } + return CGSize(width: self.collectionView!.bounds.size.width, height: height) + } + + func _tallestColumnIndexInSection(section: Int) -> Int + { + var index: Int = 0; + var tallestHeight: CGFloat = 0; + _ = _columnHeights?[section].enumerated().map { (idx,height) in + if (height > tallestHeight) { + index = idx; + tallestHeight = height + } + } + return index + } + + func _shortestColumnIndexInSection(section: Int) -> Int + { + var index: Int = 0; + var shortestHeight: CGFloat = CGFloat.greatestFiniteMagnitude + _ = _columnHeights?[section].enumerated().map { (idx,height) in + if (height < shortestHeight) { + index = idx; + shortestHeight = height + } + } + return index + } + +} + +class MosaicCollectionViewLayoutInspector: NSObject, ASCollectionViewLayoutInspecting +{ + func collectionView(_ collectionView: ASCollectionView, constrainedSizeForNodeAt indexPath: IndexPath) -> ASSizeRange { + let layout = collectionView.collectionViewLayout as! MosaicCollectionViewLayout + return ASSizeRangeMake(CGSize.zero, layout._itemSizeAtIndexPath(indexPath: indexPath)) + } + + func collectionView(_ collectionView: ASCollectionView, constrainedSizeForSupplementaryNodeOfKind: String, at atIndexPath: IndexPath) -> ASSizeRange + { + let layout = collectionView.collectionViewLayout as! MosaicCollectionViewLayout + return ASSizeRange.init(min: CGSize.zero, max: layout._headerSizeForSection(section: atIndexPath.section)) + } + + /** + * Asks the inspector for the number of supplementary sections in the collection view for the given kind. + */ + func collectionView(_ collectionView: ASCollectionView, numberOfSectionsForSupplementaryNodeOfKind kind: String) -> UInt { + if (kind == UICollectionElementKindSectionHeader) { + return UInt((collectionView.dataSource?.numberOfSections!(in: collectionView))!) + } else { + return 0 + } + } + + /** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ + func collectionView(_ collectionView: ASCollectionView, supplementaryNodesOfKind kind: String, inSection section: UInt) -> UInt { + if (kind == UICollectionElementKindSectionHeader) { + return 1 + } else { + return 0 + } + } + + func scrollableDirections() -> ASScrollDirection { + return ASScrollDirectionVerticalDirections; + } +} diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/ViewController.swift b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/ViewController.swift new file mode 100644 index 0000000000..24345cdcef --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView-Swift/Sample/ViewController.swift @@ -0,0 +1,85 @@ +// +// ViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +class ViewController: ASViewController, MosaicCollectionViewLayoutDelegate, ASCollectionDataSource, ASCollectionDelegate { + + var _sections = [[UIImage]]() + let _collectionNode: ASCollectionNode + let _layoutInspector = MosaicCollectionViewLayoutInspector() + let kNumberOfImages: UInt = 14 + + init() { + let layout = MosaicCollectionViewLayout() + layout.numberOfColumns = 3; + layout.headerHeight = 44; + _collectionNode = ASCollectionNode(frame: CGRect.zero, collectionViewLayout: layout) + super.init(node: _collectionNode) + layout.delegate = self + + _sections.append([]); + var section = 0 + for idx in 0 ..< kNumberOfImages { + let name = String(format: "image_%d.jpg", idx) + _sections[section].append(UIImage(named: name)!) + if ((idx + 1) % 5 == 0 && idx < kNumberOfImages - 1) { + section += 1 + _sections.append([]) + } + } + + _collectionNode.backgroundColor = UIColor.white + _collectionNode.dataSource = self + _collectionNode.delegate = self + _collectionNode.layoutInspector = _layoutInspector + _collectionNode.registerSupplementaryNode(ofKind: UICollectionElementKindSectionHeader) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + _collectionNode.view.isScrollEnabled = true + } + + func collectionNode(_ collectionNode: ASCollectionNode, nodeForItemAt indexPath: IndexPath) -> ASCellNode { + let image = _sections[indexPath.section][indexPath.item] + return ImageCellNode(with: image) + } + + + func collectionNode(_ collectionNode: ASCollectionNode, nodeForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> ASCellNode { + let textAttributes : NSDictionary = [ + NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline), + NSForegroundColorAttributeName: UIColor.gray + ] + let textInsets = UIEdgeInsets(top: 11, left: 0, bottom: 11, right: 0) + let textCellNode = ASTextCellNode(attributes: textAttributes as! [AnyHashable : Any], insets: textInsets) + textCellNode.text = String(format: "Section %zd", indexPath.section + 1) + return textCellNode + } + + + func numberOfSections(in collectionNode: ASCollectionNode) -> Int { + return _sections.count + } + + func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int { + return _sections[section].count + } + + internal func collectionView(_ collectionView: UICollectionView, layout: MosaicCollectionViewLayout, originalItemSizeAtIndexPath: IndexPath) -> CGSize { + return _sections[originalItemSizeAtIndexPath.section][originalItemSizeAtIndexPath.item].size + } +} + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Podfile b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..edaa0d1278 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,393 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25A1FA851C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A1FA841C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m */; }; + 25A1FA881C02FCB000193875 /* ImageCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A1FA871C02FCB000193875 /* ImageCellNode.m */; }; + 576F970133B34DFD583D5CE4 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC0FB9EE0030992E8FBC0A0 /* libPods-Sample.a */; }; + 80364CCA1E3D95A90094400C /* ImageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 80364CC91E3D95A90094400C /* ImageCollectionViewCell.m */; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; }; + AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + E5B2252C1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 25A1FA841C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MosaicCollectionLayoutDelegate.m; sourceTree = ""; }; + 25A1FA861C02FCB000193875 /* ImageCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageCellNode.h; sourceTree = ""; }; + 25A1FA871C02FCB000193875 /* ImageCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageCellNode.m; sourceTree = ""; }; + 4CC0FB9EE0030992E8FBC0A0 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 80364CC81E3D95A90094400C /* ImageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageCollectionViewCell.h; sourceTree = ""; }; + 80364CC91E3D95A90094400C /* ImageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageCollectionViewCell.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; + AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AC3C4A651A11F47200143C57 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AC3C4A661A11F47200143C57 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AC3C4A681A11F47200143C57 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + E2F287D91FFDEA2A747630CE /* 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 = ""; }; + E5B2252A1F1791DE001E1431 /* MosaicCollectionLayoutInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionLayoutInfo.h; sourceTree = ""; }; + E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MosaicCollectionLayoutInfo.m; sourceTree = ""; }; + E5D73A3A1EA6766B006418A8 /* MosaicCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionLayoutDelegate.h; sourceTree = ""; }; + F36BCD8EBAF79797AB5C6708 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC3C4A5B1A11F47200143C57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 576F970133B34DFD583D5CE4 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 90A2B9C5397C46134C8A793B /* Pods */ = { + isa = PBXGroup; + children = ( + F36BCD8EBAF79797AB5C6708 /* Pods-Sample.debug.xcconfig */, + E2F287D91FFDEA2A747630CE /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AC3C4A551A11F47200143C57 = { + isa = PBXGroup; + children = ( + AC3C4A601A11F47200143C57 /* Sample */, + AC3C4A5F1A11F47200143C57 /* Products */, + 90A2B9C5397C46134C8A793B /* Pods */, + D6E38FF0CB18E3F55CF06437 /* Frameworks */, + ); + sourceTree = ""; + }; + AC3C4A5F1A11F47200143C57 /* Products */ = { + isa = PBXGroup; + children = ( + AC3C4A5E1A11F47200143C57 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + AC3C4A601A11F47200143C57 /* Sample */ = { + isa = PBXGroup; + children = ( + E5D73A3A1EA6766B006418A8 /* MosaicCollectionLayoutDelegate.h */, + 25A1FA841C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m */, + E5B2252A1F1791DE001E1431 /* MosaicCollectionLayoutInfo.h */, + E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */, + AC3C4A651A11F47200143C57 /* AppDelegate.h */, + AC3C4A661A11F47200143C57 /* AppDelegate.m */, + AC3C4A681A11F47200143C57 /* ViewController.h */, + AC3C4A691A11F47200143C57 /* ViewController.m */, + 80364CC81E3D95A90094400C /* ImageCollectionViewCell.h */, + 80364CC91E3D95A90094400C /* ImageCollectionViewCell.m */, + 25A1FA861C02FCB000193875 /* ImageCellNode.h */, + 25A1FA871C02FCB000193875 /* ImageCellNode.m */, + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, + AC3C4A611A11F47200143C57 /* Supporting Files */, + ); + indentWidth = 2; + path = Sample; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + AC3C4A611A11F47200143C57 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + AC3C4A621A11F47200143C57 /* Info.plist */, + AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D6E38FF0CB18E3F55CF06437 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4CC0FB9EE0030992E8FBC0A0 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AC3C4A5D1A11F47200143C57 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + F868CFBB21824CC9521B6588 /* [CP] Check Pods Manifest.lock */, + AC3C4A5A1A11F47200143C57 /* Sources */, + AC3C4A5B1A11F47200143C57 /* Frameworks */, + AC3C4A5C1A11F47200143C57 /* Resources */, + A6902C454C7661D0D277AC62 /* [CP] Copy Pods Resources */, + 3760AAE3843D6EA89A9A166B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = AC3C4A5E1A11F47200143C57 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AC3C4A561A11F47200143C57 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + AC3C4A5D1A11F47200143C57 = { + CreatedOnToolsVersion = 6.1; + DevelopmentTeam = XSR3D45JSF; + }; + }; + }; + buildConfigurationList = AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AC3C4A551A11F47200143C57; + productRefGroup = AC3C4A5F1A11F47200143C57 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AC3C4A5D1A11F47200143C57 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AC3C4A5C1A11F47200143C57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3760AAE3843D6EA89A9A166B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A6902C454C7661D0D277AC62 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + F868CFBB21824CC9521B6588 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AC3C4A5A1A11F47200143C57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25A1FA851C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m in Sources */, + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, + AC3C4A641A11F47200143C57 /* main.m in Sources */, + 80364CCA1E3D95A90094400C /* ImageCollectionViewCell.m in Sources */, + 25A1FA881C02FCB000193875 /* ImageCellNode.m in Sources */, + E5B2252C1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AC3C4A7F1A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A801A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AC3C4A821A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F36BCD8EBAF79797AB5C6708 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = XSR3D45JSF; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample.CustomCollectionView; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + AC3C4A831A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E2F287D91FFDEA2A747630CE /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = XSR3D45JSF; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample.CustomCollectionView; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A7F1A11F47200143C57 /* Debug */, + AC3C4A801A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A821A11F47200143C57 /* Debug */, + AC3C4A831A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AC3C4A561A11F47200143C57 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..f49edc75d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/AppDelegate.m new file mode 100644 index 0000000000..867dafbc92 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/AppDelegate.m @@ -0,0 +1,27 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + + [self.window makeKeyAndVisible]; + + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCellNode.h b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCellNode.h new file mode 100644 index 0000000000..787ee041ca --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCellNode.h @@ -0,0 +1,17 @@ +// +// ImageCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ImageCellNode : ASCellNode + +- (instancetype)initWithImage:(UIImage *)image; +@property (nonatomic, strong) UIImage *image; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCellNode.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCellNode.m new file mode 100644 index 0000000000..9e3e4a72ff --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCellNode.m @@ -0,0 +1,45 @@ +// +// ImageCellNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ImageCellNode.h" + +@implementation ImageCellNode { + ASImageNode *_imageNode; +} + +- (id)initWithImage:(UIImage *)image +{ + self = [super init]; + if (self != nil) { + _imageNode = [[ASImageNode alloc] init]; + _imageNode.image = image; + [self addSubnode:_imageNode]; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGSize imageSize = self.image.size; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero + child:[ASRatioLayoutSpec ratioLayoutSpecWithRatio:imageSize.height/imageSize.width + child:_imageNode]]; +} + +- (void)setImage:(UIImage *)image +{ + _imageNode.image = image; +} + +- (UIImage *)image +{ + return _imageNode.image; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCollectionViewCell.h b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCollectionViewCell.h new file mode 100644 index 0000000000..8359fb72a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCollectionViewCell.h @@ -0,0 +1,13 @@ +// +// ImageCollectionViewCell.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ImageCollectionViewCell : UICollectionViewCell + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCollectionViewCell.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCollectionViewCell.m new file mode 100644 index 0000000000..c04c865039 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ImageCollectionViewCell.m @@ -0,0 +1,46 @@ +// +// ImageCollectionViewCell.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ImageCollectionViewCell.h" + +@implementation ImageCollectionViewCell +{ + UILabel *_title; + UILabel *_description; +} + +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + if (self) { + _title = [[UILabel alloc] init]; + _title.text = @"UICollectionViewCell"; + [self.contentView addSubview:_title]; + + _description = [[UILabel alloc] init]; + _description.text = @"description for cell"; + [self.contentView addSubview:_description]; + + self.contentView.backgroundColor = [UIColor orangeColor]; + } + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + [_title sizeToFit]; + [_description sizeToFit]; + + CGRect frame = _title.frame; + frame.origin.y = _title.frame.size.height; + _description.frame = frame; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..f0fce54771 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,39 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "scale" : "1x", + "orientation" : "portrait" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "orientation" : "portrait" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png new file mode 100644 index 0000000000..1547a98454 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_0.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_0.imageset/Contents.json new file mode 100644 index 0000000000..4eaff61cc1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_0.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_0.imageset/image_0.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_0.imageset/image_0.jpg new file mode 100644 index 0000000000..4a365897ea Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_0.imageset/image_0.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_1.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_1.imageset/Contents.json new file mode 100644 index 0000000000..80c90eca3e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_1.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_1.imageset/image_1.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_1.imageset/image_1.jpg new file mode 100644 index 0000000000..5cb4828f44 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_1.imageset/image_1.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_10.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_10.imageset/Contents.json new file mode 100644 index 0000000000..d61e934e39 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_10.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_10.imageset/image_10.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_10.imageset/image_10.jpg new file mode 100644 index 0000000000..ea5cd6d268 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_10.imageset/image_10.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_11.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_11.imageset/Contents.json new file mode 100644 index 0000000000..94921077f9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_11.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_11.imageset/image_11.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_11.imageset/image_11.jpg new file mode 100644 index 0000000000..e93c68e512 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_11.imageset/image_11.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_12.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_12.imageset/Contents.json new file mode 100644 index 0000000000..61488a9fdc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_12.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_12.imageset/image_12.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_12.imageset/image_12.jpg new file mode 100644 index 0000000000..d520b6d80f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_12.imageset/image_12.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_13.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_13.imageset/Contents.json new file mode 100644 index 0000000000..7f83f8a390 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_13.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_13.imageset/image_13.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_13.imageset/image_13.jpg new file mode 100644 index 0000000000..c0232370cd Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_13.imageset/image_13.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_2.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_2.imageset/Contents.json new file mode 100644 index 0000000000..774cde7833 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_2.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_2.imageset/image_2.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_2.imageset/image_2.jpg new file mode 100644 index 0000000000..175343454d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_2.imageset/image_2.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_3.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_3.imageset/Contents.json new file mode 100644 index 0000000000..c0abe414cd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_3.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_3.imageset/image_3.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_3.imageset/image_3.jpg new file mode 100644 index 0000000000..f5398cac79 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_3.imageset/image_3.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_4.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_4.imageset/Contents.json new file mode 100644 index 0000000000..55a498a8a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_4.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_4.imageset/image_4.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_4.imageset/image_4.jpg new file mode 100644 index 0000000000..2a6fe4c264 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_4.imageset/image_4.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_5.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_5.imageset/Contents.json new file mode 100644 index 0000000000..9a1181e83b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_5.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_5.imageset/image_5.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_5.imageset/image_5.jpg new file mode 100644 index 0000000000..4e507b8064 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_5.imageset/image_5.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_6.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_6.imageset/Contents.json new file mode 100644 index 0000000000..6aef7d6047 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_6.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_6.imageset/image_6.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_6.imageset/image_6.jpg new file mode 100644 index 0000000000..35fe778b3a Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_6.imageset/image_6.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_7.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_7.imageset/Contents.json new file mode 100644 index 0000000000..acdb0e87f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_7.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_7.imageset/image_7.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_7.imageset/image_7.jpg new file mode 100644 index 0000000000..8f5e037722 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_7.imageset/image_7.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_8.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_8.imageset/Contents.json new file mode 100644 index 0000000000..40d616ed40 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_8.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_8.imageset/image_8.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_8.imageset/image_8.jpg new file mode 100644 index 0000000000..5651436bb6 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_8.imageset/image_8.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_9.imageset/Contents.json b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_9.imageset/Contents.json new file mode 100644 index 0000000000..b3b3c74e12 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_9.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_9.imageset/image_9.jpg b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_9.imageset/image_9.jpg new file mode 100644 index 0000000000..9fb6e47d3f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Images.xcassets/image_9.imageset/image_9.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Info.plist new file mode 100644 index 0000000000..eeb71a8d35 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launchboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Launchboard.storyboard b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.h b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.h new file mode 100644 index 0000000000..b6651953d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.h @@ -0,0 +1,16 @@ +// +// MosaicCollectionLayoutDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface MosaicCollectionLayoutDelegate : NSObject + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns headerHeight:(CGFloat)headerHeight; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m new file mode 100644 index 0000000000..1715e1c374 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -0,0 +1,167 @@ +// +// MosaicCollectionLayoutDelegate.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "MosaicCollectionLayoutDelegate.h" +#import "MosaicCollectionLayoutInfo.h" +#import "ImageCellNode.h" + +#import + +@implementation MosaicCollectionLayoutDelegate { + // Read-only properties + MosaicCollectionLayoutInfo *_info; +} + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns headerHeight:(CGFloat)headerHeight +{ + self = [super init]; + if (self != nil) { + _info = [[MosaicCollectionLayoutInfo alloc] initWithNumberOfColumns:numberOfColumns + headerHeight:headerHeight + columnSpacing:10.0 + sectionInsets:UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) + interItemSpacing:UIEdgeInsetsMake(10.0, 0, 10.0, 0)]; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertMainThread(); + return ASScrollDirectionVerticalDirections; +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + return _info; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + CGFloat layoutWidth = context.viewportSize.width; + ASElementMap *elements = context.elements; + CGFloat top = 0; + MosaicCollectionLayoutInfo *info = (MosaicCollectionLayoutInfo *)context.additionalInfo; + + NSMapTable *attrsMap = [NSMapTable elementToLayoutAttributesTable]; + NSMutableArray *columnHeights = [NSMutableArray array]; + + NSInteger numberOfSections = [elements numberOfSections]; + for (NSUInteger section = 0; section < numberOfSections; section++) { + NSInteger numberOfItems = [elements numberOfItemsInSection:section]; + + top += info.sectionInsets.top; + + if (info.headerHeight > 0) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; + ASCollectionElement *element = [elements supplementaryElementOfKind:UICollectionElementKindSectionHeader + atIndexPath:indexPath]; + UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader + withIndexPath:indexPath]; + + ASSizeRange sizeRange = [self _sizeRangeForHeaderOfSection:section withLayoutWidth:layoutWidth info:info]; + CGSize size = [element.node layoutThatFits:sizeRange].size; + CGRect frame = CGRectMake(info.sectionInsets.left, top, size.width, size.height); + + attrs.frame = frame; + [attrsMap setObject:attrs forKey:element]; + top = CGRectGetMaxY(frame); + } + + [columnHeights addObject:[NSMutableArray array]]; + for (NSUInteger idx = 0; idx < info.numberOfColumns; idx++) { + [columnHeights[section] addObject:@(top)]; + } + + CGFloat columnWidth = [self _columnWidthForSection:section withLayoutWidth:layoutWidth info:info]; + for (NSUInteger idx = 0; idx < numberOfItems; idx++) { + NSUInteger columnIndex = [self _shortestColumnIndexInSection:section withColumnHeights:columnHeights]; + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; + ASCollectionElement *element = [elements elementForItemAtIndexPath:indexPath]; + UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; + + ASSizeRange sizeRange = [self _sizeRangeForItem:element.node atIndexPath:indexPath withLayoutWidth:layoutWidth info:info]; + CGSize size = [element.node layoutThatFits:sizeRange].size; + CGPoint position = CGPointMake(info.sectionInsets.left + (columnWidth + info.columnSpacing) * columnIndex, + [columnHeights[section][columnIndex] floatValue]); + CGRect frame = CGRectMake(position.x, position.y, size.width, size.height); + + attrs.frame = frame; + [attrsMap setObject:attrs forKey:element]; + // TODO Profile and avoid boxing if there are significant retain/release overheads + columnHeights[section][columnIndex] = @(CGRectGetMaxY(frame) + info.interItemSpacing.bottom); + } + + NSUInteger columnIndex = [self _tallestColumnIndexInSection:section withColumnHeights:columnHeights]; + top = [columnHeights[section][columnIndex] floatValue] - info.interItemSpacing.bottom + info.sectionInsets.bottom; + + for (NSUInteger idx = 0; idx < [columnHeights[section] count]; idx++) { + columnHeights[section][idx] = @(top); + } + } + + CGFloat contentHeight = [[[columnHeights lastObject] firstObject] floatValue]; + CGSize contentSize = CGSizeMake(layoutWidth, contentHeight); + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:contentSize + elementToLayoutAttributesTable:attrsMap]; +} + ++ (CGFloat)_columnWidthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info +{ + return ([self _widthForSection:section withLayoutWidth:layoutWidth info:info] - ((info.numberOfColumns - 1) * info.columnSpacing)) / info.numberOfColumns; +} + ++ (CGFloat)_widthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info +{ + return layoutWidth - info.sectionInsets.left - info.sectionInsets.right; +} + ++ (ASSizeRange)_sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)indexPath withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info +{ + CGFloat itemWidth = [self _columnWidthForSection:indexPath.section withLayoutWidth:layoutWidth info:info]; + if ([item isKindOfClass:[ImageCellNode class]]) { + return ASSizeRangeMake(CGSizeMake(itemWidth, 0), CGSizeMake(itemWidth, CGFLOAT_MAX)); + } else { + return ASSizeRangeMake(CGSizeMake(itemWidth, itemWidth)); // In kShowUICollectionViewCells = YES mode, make those cells itemWidth x itemWidth. + } +} + ++ (ASSizeRange)_sizeRangeForHeaderOfSection:(NSInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info +{ + return ASSizeRangeMake(CGSizeMake(0, info.headerHeight), CGSizeMake([self _widthForSection:section withLayoutWidth:layoutWidth info:info], info.headerHeight)); +} + ++ (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights +{ + __block NSUInteger index = 0; + __block CGFloat tallestHeight = 0; + [columnHeights[section] enumerateObjectsUsingBlock:^(NSNumber *height, NSUInteger idx, BOOL *stop) { + if (height.floatValue > tallestHeight) { + index = idx; + tallestHeight = height.floatValue; + } + }]; + return index; +} + ++ (NSUInteger)_shortestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights +{ + __block NSUInteger index = 0; + __block CGFloat shortestHeight = CGFLOAT_MAX; + [columnHeights[section] enumerateObjectsUsingBlock:^(NSNumber *height, NSUInteger idx, BOOL *stop) { + if (height.floatValue < shortestHeight) { + index = idx; + shortestHeight = height.floatValue; + } + }]; + return index; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h new file mode 100644 index 0000000000..b887e1d2be --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h @@ -0,0 +1,28 @@ +// +// MosaicCollectionLayoutInfo.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface MosaicCollectionLayoutInfo : NSObject + +// Read-only properties +@property (nonatomic, assign, readonly) NSInteger numberOfColumns; +@property (nonatomic, assign, readonly) CGFloat headerHeight; +@property (nonatomic, assign, readonly) CGFloat columnSpacing; +@property (nonatomic, assign, readonly) UIEdgeInsets sectionInsets; +@property (nonatomic, assign, readonly) UIEdgeInsets interItemSpacing; + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns + headerHeight:(CGFloat)headerHeight + columnSpacing:(CGFloat)columnSpacing + sectionInsets:(UIEdgeInsets)sectionInsets + interItemSpacing:(UIEdgeInsets)interItemSpacing NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m new file mode 100644 index 0000000000..9eb80c2186 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m @@ -0,0 +1,74 @@ +// +// MosaicCollectionLayoutInfo.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "MosaicCollectionLayoutInfo.h" + +#import + +@implementation MosaicCollectionLayoutInfo + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns + headerHeight:(CGFloat)headerHeight + columnSpacing:(CGFloat)columnSpacing + sectionInsets:(UIEdgeInsets)sectionInsets + interItemSpacing:(UIEdgeInsets)interItemSpacing +{ + self = [super init]; + if (self) { + _numberOfColumns = numberOfColumns; + _headerHeight = headerHeight; + _columnSpacing = columnSpacing; + _sectionInsets = sectionInsets; + _interItemSpacing = interItemSpacing; + } + return self; +} + +- (BOOL)isEqualToInfo:(MosaicCollectionLayoutInfo *)info +{ + if (info == nil) { + return NO; + } + + return _numberOfColumns == info.numberOfColumns + && _headerHeight == info.headerHeight + && _columnSpacing == info.columnSpacing + && UIEdgeInsetsEqualToEdgeInsets(_sectionInsets, info.sectionInsets) + && UIEdgeInsetsEqualToEdgeInsets(_interItemSpacing, info.interItemSpacing); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[MosaicCollectionLayoutInfo class]]) { + return NO; + } + return [self isEqualToInfo:other]; +} + +- (NSUInteger)hash +{ + struct { + NSInteger numberOfColumns; + CGFloat headerHeight; + CGFloat columnSpacing; + UIEdgeInsets sectionInsets; + UIEdgeInsets interItemSpacing; + } data = { + _numberOfColumns, + _headerHeight, + _columnSpacing, + _sectionInsets, + _interItemSpacing, + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ViewController.h new file mode 100644 index 0000000000..da850f7446 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ViewController.h @@ -0,0 +1,15 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ViewController.m new file mode 100644 index 0000000000..7715fc8c07 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/ViewController.m @@ -0,0 +1,142 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import +#import "MosaicCollectionLayoutDelegate.h" +#import "ImageCellNode.h" +#import "ImageCollectionViewCell.h" + +// This option demonstrates that raw UIKit cells can still be used alongside native ASCellNodes. +static BOOL kShowUICollectionViewCells = YES; +static NSString *kReuseIdentifier = @"ImageCollectionViewCell"; +static NSUInteger kNumberOfImages = 14; + +@interface ViewController () +{ + NSMutableArray *_sections; + ASCollectionNode *_collectionNode; +} + +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController + +- (instancetype)init +{ + MosaicCollectionLayoutDelegate *layoutDelegate = [[MosaicCollectionLayoutDelegate alloc] initWithNumberOfColumns:2 headerHeight:44.0]; + _collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; + _collectionNode.dataSource = self; + _collectionNode.delegate = self; + _collectionNode.layoutInspector = self; + + if (!(self = [super initWithNode:_collectionNode])) + return nil; + + _sections = [NSMutableArray array]; + [_sections addObject:[NSMutableArray array]]; + for (NSUInteger idx = 0, section = 0; idx < kNumberOfImages; idx++) { + NSString *name = [NSString stringWithFormat:@"image_%lu.jpg", (unsigned long)idx]; + [_sections[section] addObject:[UIImage imageNamed:name]]; + if ((idx + 1) % 5 == 0 && idx < kNumberOfImages - 1) { + section++; + [_sections addObject:[NSMutableArray array]]; + } + } + + [_collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [_collectionNode.view registerClass:[ImageCollectionViewCell class] forCellWithReuseIdentifier:kReuseIdentifier]; +} + +- (void)reloadTapped +{ + [_collectionNode reloadData]; +} + +#pragma mark - ASCollectionNode data source. + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (kShowUICollectionViewCells && indexPath.item % 3 == 1) { + // When enabled, return nil for every third cell and then cellForItemAtIndexPath: will be called. + return nil; + } + + UIImage *image = _sections[indexPath.section][indexPath.item]; + return ^{ + return [[ImageCellNode alloc] initWithImage:image]; + }; +} + +// The below 2 methods are required by ASCollectionViewLayoutInspecting, but ASCollectionLayout and its layout delegate are the ones that really determine the size ranges and directions +// TODO Remove these methods once a layout inspector is no longer required under ASCollectionLayout mode +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + return ASSizeRangeZero; +} + +- (ASScrollDirection)scrollableDirections +{ + return ASScrollDirectionVerticalDirections; +} + +/** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + return [kind isEqualToString:UICollectionElementKindSectionHeader] ? 1 : 0; +} + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSDictionary *textAttributes = @{ + NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline], + NSForegroundColorAttributeName: [UIColor grayColor] + }; + UIEdgeInsets textInsets = UIEdgeInsetsMake(11.0, 0, 11.0, 0); + ASTextCellNode *textCellNode = [[ASTextCellNode alloc] initWithAttributes:textAttributes insets:textInsets]; + textCellNode.text = [NSString stringWithFormat:@"Section %zd", indexPath.section + 1]; + return textCellNode; +} + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ + return _sections.count; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return [_sections[section] count]; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [_collectionNode.view dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return nil; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/main.m b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/main.m new file mode 100644 index 0000000000..65850400e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/CustomCollectionView/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Podfile b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6472106a7b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,378 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFD19D4F94A00CBA93C /* HorizontalScrollCellNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* HorizontalScrollCellNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 18B5AC4A1550AC957426B54E /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9341ADE7BE83CA50F1FD55C1 /* libPods-Sample.a */; }; + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */; }; + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CFB19D4F94A00CBA93C /* HorizontalScrollCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HorizontalScrollCellNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* HorizontalScrollCellNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = HorizontalScrollCellNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RandomCoreGraphicsNode.h; sourceTree = ""; }; + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RandomCoreGraphicsNode.m; sourceTree = ""; }; + 367E401FD4A0E65C4C240050 /* 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 = ""; }; + 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; }; + 9341ADE7BE83CA50F1FD55C1 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9CEC783A48902CC0051FDE7E /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 18B5AC4A1550AC957426B54E /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* HorizontalScrollCellNode.h */, + 05561CFC19D4F94A00CBA93C /* HorizontalScrollCellNode.mm */, + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */, + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9341ADE7BE83CA50F1FD55C1 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 367E401FD4A0E65C4C240050 /* Pods-Sample.debug.xcconfig */, + 9CEC783A48902CC0051FDE7E /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + A79A9172A45D7C9595AA01CC /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A79A9172A45D7C9595AA01CC /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */, + 05561CFD19D4F94A00CBA93C /* HorizontalScrollCellNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 367E401FD4A0E65C4C240050 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9CEC783A48902CC0051FDE7E /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.m new file mode 100644 index 0000000000..f8437855b0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.h b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.h new file mode 100644 index 0000000000..49b6e48d09 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.h @@ -0,0 +1,20 @@ +// +// HorizontalScrollCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * This ASCellNode contains an ASCollectionNode. It intelligently interacts with a containing ASCollectionView or ASTableView, + * to preload and clean up contents as the user scrolls around both vertically and horizontally — in a way that minimizes memory usage. + */ +@interface HorizontalScrollCellNode : ASCellNode + +- (instancetype)initWithElementSize:(CGSize)size; + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.mm b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.mm new file mode 100644 index 0000000000..0794643893 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.mm @@ -0,0 +1,105 @@ +// +// HorizontalScrollCellNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "HorizontalScrollCellNode.h" +#import "RandomCoreGraphicsNode.h" +#import "AppDelegate.h" + +#import + +#import +#import + +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + +@interface HorizontalScrollCellNode () +{ + ASCollectionNode *_collectionNode; + CGSize _elementSize; + ASDisplayNode *_divider; +} + +@end + + +@implementation HorizontalScrollCellNode + +#pragma mark - Lifecycle + +- (instancetype)initWithElementSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _elementSize = size; + + // the containing table uses -nodeForRowAtIndexPath (rather than -nodeBlockForRowAtIndexPath), + // so this init method will always be run on the main thread (thus it is safe to do UIKit things). + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + flowLayout.itemSize = _elementSize; + flowLayout.minimumInteritemSpacing = kInnerPadding; + + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; + _collectionNode.delegate = self; + _collectionNode.dataSource = self; + [self addSubnode:_collectionNode]; + + // hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + _divider.backgroundColor = [UIColor lightGrayColor]; + [self addSubnode:_divider]; + + return self; +} + +// With box model, you don't need to override this method, unless you want to add custom logic. +- (void)layout +{ + [super layout]; + + _collectionNode.view.contentInset = UIEdgeInsetsMake(0.0, kOuterPadding, 0.0, kOuterPadding); + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGSize collectionNodeSize = CGSizeMake(constrainedSize.max.width, _elementSize.height); + _collectionNode.style.preferredSize = collectionNodeSize; + + ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, 0.0, kOuterPadding, 0.0); + insetSpec.child = _collectionNode; + + return insetSpec; +} + +#pragma mark - ASCollectionNode + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return 5; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGSize elementSize = _elementSize; + + return ^{ + RandomCoreGraphicsNode *elementNode = [[RandomCoreGraphicsNode alloc] init]; + elementNode.style.preferredSize = elementSize; + return elementNode; + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.h b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.h new file mode 100644 index 0000000000..85b53290cc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.h @@ -0,0 +1,14 @@ +// +// RandomCoreGraphicsNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface RandomCoreGraphicsNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.m b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.m new file mode 100644 index 0000000000..83d9ed6795 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.m @@ -0,0 +1,45 @@ +// +// RandomCoreGraphicsNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "RandomCoreGraphicsNode.h" +#import + +@implementation RandomCoreGraphicsNode + ++ (UIColor *)randomColor +{ + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + CGFloat locations[3]; + NSMutableArray *colors = [NSMutableArray arrayWithCapacity:3]; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[0] = 0.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[1] = 1.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[2] = ( arc4random() % 256 / 256.0 ); + + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)colors, locations); + + CGContextDrawLinearGradient(ctx, gradient, CGPointZero, CGPointMake(bounds.size.width, bounds.size.height), 0); + + CGGradientRelease(gradient); + CGColorSpaceRelease(colorSpace); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.h new file mode 100644 index 0000000000..560b6a2d03 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.m new file mode 100644 index 0000000000..f1b1858825 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.m @@ -0,0 +1,68 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "ViewController.h" +#import "HorizontalScrollCellNode.h" + +@interface ViewController () +{ + ASTableNode *_tableNode; +} + +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + _tableNode.dataSource = self; + _tableNode.delegate = self; + + if (!(self = [super initWithNode:_tableNode])) + return nil; + + self.title = @"Horizontal Scrolling Gradients"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRedo + target:self + action:@selector(reloadEverything)]; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; +} + +- (void)reloadEverything +{ + [_tableNode reloadData]; +} + +#pragma mark - ASTableNode + +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return [[HorizontalScrollCellNode alloc] initWithElementSize:CGSizeMake(100, 100)]; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return 100; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/main.m b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/HorizontalWithinVerticalScrolling/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/Kittens/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Kittens/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/Kittens/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Kittens/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/Kittens/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Kittens/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Podfile b/submodules/AsyncDisplayKit/examples/Kittens/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..3c2cb1a592 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 05561CF919D4E77700CBA93C /* BlurbNode.m */; }; + 05561CFD19D4F94A00CBA93C /* KittenNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* KittenNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 52C9A6CB87CA529C55C02E59 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E0D5A93FCFED54B93217FB08 /* 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CF819D4E77700CBA93C /* BlurbNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurbNode.h; sourceTree = ""; }; + 05561CF919D4E77700CBA93C /* BlurbNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurbNode.m; sourceTree = ""; }; + 05561CFB19D4F94A00CBA93C /* KittenNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KittenNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* KittenNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KittenNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 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; }; + 6DE7AC2C8D07948DFC1E9C93 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + DEE781281CCB21EB0053A711 /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; + DEE781291CCB21EB0053A711 /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; + E0D5A93FCFED54B93217FB08 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 52C9A6CB87CA529C55C02E59 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* KittenNode.h */, + 05561CFC19D4F94A00CBA93C /* KittenNode.mm */, + 05561CF819D4E77700CBA93C /* BlurbNode.h */, + 05561CF919D4E77700CBA93C /* BlurbNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6DE7AC2C8D07948DFC1E9C93 /* libPods.a */, + E0D5A93FCFED54B93217FB08 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + DEE781281CCB21EB0053A711 /* Pods-Sample.debug.xcconfig */, + DEE781291CCB21EB0053A711 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 9C2078E0C7EEEFF207C7F6A9 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9C2078E0C7EEEFF207C7F6A9 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05561CFD19D4F94A00CBA93C /* KittenNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DEE781281CCB21EB0053A711 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DEE781291CCB21EB0053A711 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..d41d58c5d8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/Kittens/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/Kittens/Sample/AppDelegate.m new file mode 100644 index 0000000000..2395642bd1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/AppDelegate.m @@ -0,0 +1,37 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" +#import + +@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 = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end + +@implementation ASConfiguration (UserProvided) + ++ (ASConfiguration *)textureConfiguration +{ + ASConfiguration *configuration = [ASConfiguration new]; + return configuration; + +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/BlurbNode.h b/submodules/AsyncDisplayKit/examples/Kittens/Sample/BlurbNode.h new file mode 100644 index 0000000000..5451fb39f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/BlurbNode.h @@ -0,0 +1,17 @@ +// +// BlurbNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Simple node that displays a placekitten.com attribution. + */ +@interface BlurbNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/BlurbNode.m b/submodules/AsyncDisplayKit/examples/Kittens/Sample/BlurbNode.m new file mode 100644 index 0000000000..6014711d33 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/BlurbNode.m @@ -0,0 +1,120 @@ +// +// BlurbNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "BlurbNode.h" +#import "AppDelegate.h" + +#import +#import + +#import +#import + +static CGFloat kTextPadding = 10.0f; +static NSString *kLinkAttributeName = @"PlaceKittenNodeLinkAttributeName"; + +@interface BlurbNode () +{ + ASTextNode *_textNode; +} + +@end + + +@implementation BlurbNode + +#pragma mark - +#pragma mark ASCellNode. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // create a text node + _textNode = [[ASTextNode alloc] init]; + + // configure the node to support tappable links + _textNode.delegate = self; + _textNode.userInteractionEnabled = YES; + _textNode.linkAttributeNames = @[ kLinkAttributeName ]; + + // generate an attributed string using the custom link attribute specified above + NSString *blurb = @"kittens courtesy placekitten.com \U0001F638"; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)]; + [string addAttributes:@{ + kLinkAttributeName: [NSURL URLWithString:@"http://placekitten.com/"], + NSForegroundColorAttributeName: [UIColor grayColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } + range:[blurb rangeOfString:@"placekitten.com"]]; + _textNode.attributedText = string; + + // add it as a subnode, and we're done + [self addSubnode:_textNode]; + + return self; +} + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // called on a background thread. custom nodes must call -measure: on their subnodes in -calculateSizeThatFits: + CGSize measuredSize = [_textNode measure:CGSizeMake(constrainedSize.width - 2 * kTextPadding, + constrainedSize.height - 2 * kTextPadding)]; + return CGSizeMake(constrainedSize.width, measuredSize.height + 2 * kTextPadding); +} + +- (void)layout +{ + // called on the main thread. we'll use the stashed size from above, instead of blocking on text sizing + CGSize textNodeSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(roundf((self.calculatedSize.width - textNodeSize.width) / 2.0f), + kTextPadding, + textNodeSize.width, + textNodeSize.height); +} +#endif + +#pragma mark - +#pragma mark ASTextNodeDelegate methods. + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // the node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/Kittens/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/KittenNode.h b/submodules/AsyncDisplayKit/examples/Kittens/Sample/KittenNode.h new file mode 100644 index 0000000000..5b0b04203e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/KittenNode.h @@ -0,0 +1,22 @@ +// +// KittenNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Social media-style node that displays a kitten picture and a random length + * of lorem ipsum text. Uses a placekitten.com kitten of the specified size. + */ +@interface KittenNode : ASCellNode + +- (instancetype)initWithKittenOfSize:(CGSize)size; + +- (void)toggleImageEnlargement; + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/KittenNode.mm b/submodules/AsyncDisplayKit/examples/Kittens/Sample/KittenNode.mm new file mode 100644 index 0000000000..f8ae2ef2e6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/KittenNode.mm @@ -0,0 +1,252 @@ +// +// KittenNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "KittenNode.h" +#import "AppDelegate.h" + +#import + +#import +#import +#import + +static const CGFloat kImageSize = 80.0f; +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + + +@interface KittenNode () +{ + CGSize _kittenSize; + + ASNetworkImageNode *_imageNode; + ASTextNode *_textNode; + ASDisplayNode *_divider; + BOOL _isImageEnlarged; + BOOL _swappedTextAndImage; +} + +@end + + +@implementation KittenNode + +// lorem ipsum text courtesy https://kittyipsum.com/ <3 ++ (NSArray *)placeholders +{ + static NSArray *placeholders = nil; + + static dispatch_once_t once; + dispatch_once(&once, ^{ + placeholders = @[ + @"Kitty ipsum dolor sit amet, purr sleep on your face lay down in your way biting, sniff tincidunt a etiam fluffy fur judging you stuck in a tree kittens.", + @"Lick tincidunt a biting eat the grass, egestas enim ut lick leap puking climb the curtains lick.", + @"Lick quis nunc toss the mousie vel, tortor pellentesque sunbathe orci turpis non tail flick suscipit sleep in the sink.", + @"Orci turpis litter box et stuck in a tree, egestas ac tempus et aliquam elit.", + @"Hairball iaculis dolor dolor neque, nibh adipiscing vehicula egestas dolor aliquam.", + @"Sunbathe fluffy fur tortor faucibus pharetra jump, enim jump on the table I don't like that food catnip toss the mousie scratched.", + @"Quis nunc nam sleep in the sink quis nunc purr faucibus, chase the red dot consectetur bat sagittis.", + @"Lick tail flick jump on the table stretching purr amet, rhoncus scratched jump on the table run.", + @"Suspendisse aliquam vulputate feed me sleep on your keyboard, rip the couch faucibus sleep on your keyboard tristique give me fish dolor.", + @"Rip the couch hiss attack your ankles biting pellentesque puking, enim suspendisse enim mauris a.", + @"Sollicitudin iaculis vestibulum toss the mousie biting attack your ankles, puking nunc jump adipiscing in viverra.", + @"Nam zzz amet neque, bat tincidunt a iaculis sniff hiss bibendum leap nibh.", + @"Chase the red dot enim puking chuf, tristique et egestas sniff sollicitudin pharetra enim ut mauris a.", + @"Sagittis scratched et lick, hairball leap attack adipiscing catnip tail flick iaculis lick.", + @"Neque neque sleep in the sink neque sleep on your face, climb the curtains chuf tail flick sniff tortor non.", + @"Ac etiam kittens claw toss the mousie jump, pellentesque rhoncus litter box give me fish adipiscing mauris a.", + @"Pharetra egestas sunbathe faucibus ac fluffy fur, hiss feed me give me fish accumsan.", + @"Tortor leap tristique accumsan rutrum sleep in the sink, amet sollicitudin adipiscing dolor chase the red dot.", + @"Knock over the lamp pharetra vehicula sleep on your face rhoncus, jump elit cras nec quis quis nunc nam.", + @"Sollicitudin feed me et ac in viverra catnip, nunc eat I don't like that food iaculis give me fish.", + ]; + }); + + return placeholders; +} + +- (instancetype)initWithKittenOfSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _kittenSize = size; + + // kitten image, with a solid background colour serving as placeholder + _imageNode = [[ASNetworkImageNode alloc] init]; + _imageNode.URL = [NSURL URLWithString:[NSString stringWithFormat:@"https://placekitten.com/%zd/%zd", + (NSInteger)roundl(_kittenSize.width), + (NSInteger)roundl(_kittenSize.height)]]; + _imageNode.placeholderFadeDuration = .5; + _imageNode.placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); + // _imageNode.contentMode = UIViewContentModeCenter; + [_imageNode addTarget:self action:@selector(toggleNodesSwap) forControlEvents:ASControlNodeEventTouchUpInside]; + [self addSubnode:_imageNode]; + + // lorem ipsum text, plus some nice styling + + _textNode = [[ASTextNode alloc] init]; + _textNode.shadowColor = [UIColor blackColor].CGColor; + _textNode.shadowRadius = 3; + _textNode.shadowOffset = CGSizeMake(-2, -2); + _textNode.shadowOpacity = 0.3; + if (_textNode.usingExperiment) { + _textNode.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:1 alpha:1]; + } else { + _textNode.backgroundColor = [UIColor colorWithRed:1 green:0.9 blue:0.9 alpha:1]; + } + _textNode.maximumNumberOfLines = 2; + _textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"…"]; + _textNode.additionalTruncationMessage = [[NSAttributedString alloc] initWithString:@"More"]; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:[self kittyIpsum] attributes:[self textStyle]]; + [self addSubnode:_textNode]; + + // hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + _divider.backgroundColor = [UIColor lightGrayColor]; + [self addSubnode:_divider]; + + return self; +} + +- (NSString *)kittyIpsum +{ + NSArray *placeholders = [KittenNode placeholders]; + u_int32_t ipsumCount = (u_int32_t)[placeholders count]; + u_int32_t location = arc4random_uniform(ipsumCount); + u_int32_t length = arc4random_uniform(ipsumCount - location); + + NSMutableString *string = [placeholders[location] mutableCopy]; + for (u_int32_t i = location + 1; i < location + length; i++) { + [string appendString:(i % 2 == 0) ? @"\n" : @" "]; + [string appendString:placeholders[i]]; + } + + return string; +} + +- (NSDictionary *)textStyle +{ + UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:12.0f]; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + style.paragraphSpacing = 0.5 * font.lineHeight; + style.hyphenationFactor = 1.0; + + return @{ + NSFontAttributeName: font, + NSParagraphStyleAttributeName: style, + ASTextNodeWordKerningAttributeName : @.5 + }; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // Set an intrinsic size for the image node + CGSize imageSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize) : CGSizeMake(kImageSize, kImageSize); + _imageNode.style.preferredSize = imageSize; + + // Shrink the text node in case the image + text gonna be too wide + _textNode.style.flexShrink = 1.0; + + // Configure stack + ASStackLayoutSpec *stackLayoutSpec = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:kInnerPadding + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:_swappedTextAndImage ? @[_textNode, _imageNode] : @[_imageNode, _textNode]]; + + // Add inset + return [ASInsetLayoutSpec + insetLayoutSpecWithInsets:UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding) + child:stackLayoutSpec]; +} + +// With box model, you don't need to override this method, unless you want to add custom logic. +- (void)layout +{ + [super layout]; + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize imageSize = CGSizeMake(kImageSize, kImageSize); + CGSize textSize = [_textNode measure:CGSizeMake(constrainedSize.width - kImageSize - 2 * kOuterPadding - kInnerPadding, + constrainedSize.height)]; + + // ensure there's room for the text + CGFloat requiredHeight = MAX(textSize.height, imageSize.height); + return CGSizeMake(constrainedSize.width, requiredHeight + 2 * kOuterPadding); +} + +- (void)layout +{ + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); + + _imageNode.frame = CGRectMake(kOuterPadding, kOuterPadding, kImageSize, kImageSize); + + CGSize textSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(kOuterPadding + kImageSize + kInnerPadding, kOuterPadding, textSize.width, textSize.height); +} +#endif + +- (void)toggleImageEnlargement +{ + _isImageEnlarged = !_isImageEnlarged; + [self setNeedsLayout]; +} + +- (void)toggleNodesSwap +{ + _swappedTextAndImage = !_swappedTextAndImage; + + [UIView animateWithDuration:0.15 animations:^{ + self.alpha = 0; + } completion:^(BOOL finished) { + [self setNeedsLayout]; + [self.view layoutIfNeeded]; + + [UIView animateWithDuration:0.15 animations:^{ + self.alpha = 1; + }]; + }]; +} + +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [UIColor lightGrayColor]; + } else if (self.selected) { + self.backgroundColor = [UIColor blueColor]; + } else { + self.backgroundColor = [UIColor whiteColor]; + } +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self updateBackgroundColor]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/Kittens/Sample/ViewController.h new file mode 100644 index 0000000000..560b6a2d03 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/Kittens/Sample/ViewController.m new file mode 100644 index 0000000000..552f482886 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/ViewController.m @@ -0,0 +1,176 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import + +#import "BlurbNode.h" +#import "KittenNode.h" + + +static const NSInteger kLitterSize = 20; // intial number of kitten cells in ASTableNode +static const NSInteger kLitterBatchSize = 10; // number of kitten cells to add to ASTableNode +static const NSInteger kMaxLitterSize = 100; // max number of kitten cells allowed in ASTableNode + +@interface ViewController () +{ + ASTableNode *_tableNode; + + // array of boxed CGSizes corresponding to placekitten.com kittens + NSMutableArray *_kittenDataSource; + + NSIndexPath *_blurbNodeIndexPath; +} + +@property (nonatomic, strong) NSMutableArray *kittenDataSource; + +@end + + +@implementation ViewController + +#pragma mark - Lifecycle + +- (instancetype)init +{ + _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + _tableNode.dataSource = self; + _tableNode.delegate = self; + + if (!(self = [super initWithNode:_tableNode])) + return nil; + + // populate our "data source" with some random kittens + _kittenDataSource = [self createLitterWithSize:kLitterSize]; + _blurbNodeIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + self.title = @"Kittens"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit + target:self + action:@selector(toggleEditingMode)]; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator + [self.node addSubnode:_tableNode]; +} + +#pragma mark - Data Model + +- (NSMutableArray *)createLitterWithSize:(NSInteger)litterSize +{ + NSMutableArray *kittens = [NSMutableArray arrayWithCapacity:litterSize]; + for (NSInteger i = 0; i < litterSize; i++) { + + // placekitten.com will return the same kitten picture if the same pixel height & width are requested, + // so generate kittens with different width & height values. + u_int32_t deltaX = arc4random_uniform(10) - 5; + u_int32_t deltaY = arc4random_uniform(10) - 5; + CGSize size = CGSizeMake(350 + 2 * deltaX, 350 + 4 * deltaY); + + [kittens addObject:[NSValue valueWithCGSize:size]]; + } + return kittens; +} + +- (void)toggleEditingMode +{ + [_tableNode.view setEditing:!_tableNode.view.editing animated:YES]; +} + + +#pragma mark - ASTableNode + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + // blurb node + kLitterSize kitties + return 1 + _kittenDataSource.count; +} + +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // special-case the first row + if ([_blurbNodeIndexPath compare:indexPath] == NSOrderedSame) { + BlurbNode *node = [[BlurbNode alloc] init]; + return node; + } + + NSValue *size = _kittenDataSource[indexPath.row - 1]; + KittenNode *node = [[KittenNode alloc] initWithKittenOfSize:size.CGSizeValue]; + return node; +} + +- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [_tableNode deselectRowAtIndexPath:indexPath animated:YES]; + + // Assume only kitten nodes are selectable (see -tableNode:shouldHighlightRowAtIndexPath:). + KittenNode *node = (KittenNode *)[_tableNode nodeForRowAtIndexPath:indexPath]; + + [node toggleImageEnlargement]; +} + +- (BOOL)tableNode:(ASTableNode *)tableNode shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable selection for kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(nonnull ASBatchContext *)context +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // populate a new array of random-sized kittens + NSArray *moarKittens = [self createLitterWithSize:kLitterBatchSize]; + + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + + // find number of kittens in the data source and create their indexPaths + NSInteger existingRows = _kittenDataSource.count + 1; + + for (NSInteger i = 0; i < moarKittens.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:existingRows + i inSection:0]]; + } + + // add new kittens to the data source & notify table of new indexpaths + [_kittenDataSource addObjectsFromArray:moarKittens]; + [tableNode insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + + [context completeBatchFetching:YES]; + }); +} + +- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode +{ + return _kittenDataSource.count < kMaxLitterSize; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable editing for Kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle + forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Assume only kitten nodes are editable (see -tableView:canEditRowAtIndexPath:). + [_kittenDataSource removeObjectAtIndex:indexPath.row - 1]; + [_tableNode deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Kittens/Sample/main.m b/submodules/AsyncDisplayKit/examples/Kittens/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Kittens/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Podfile b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Podfile new file mode 100644 index 0000000000..83d2cae8bf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' + +use_frameworks! + +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..5ac0a8718a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,416 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7319D22E19004363C2 /* AppDelegate.swift */; }; + 161FE6897BB33A570A663F90 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6141C83F216FCA6D7EAEE /* Pods_Sample.framework */; }; + 5E2932011DD1E07900026492 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5E2931FF1DD1E07900026492 /* LaunchScreen.storyboard */; }; + 5E2932031DD1E0B600026492 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2932021DD1E0B600026492 /* Utilities.swift */; }; + 5E2932071DD1F06D00026492 /* OverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2932061DD1F06D00026492 /* OverviewViewController.swift */; }; + 5E2932091DD1F0F600026492 /* LayoutExampleNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2932081DD1F0F600026492 /* LayoutExampleNode.swift */; }; + 5E29320B1DD1F46500026492 /* OverviewCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E29320A1DD1F46500026492 /* OverviewCellNode.swift */; }; + 5EBAB9511DD1FA8C00536D28 /* LayoutExampleNode+Layouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EBAB9501DD1FA8C00536D28 /* LayoutExampleNode+Layouts.swift */; }; + 5EBAB9531DD1FFA600536D28 /* LayoutExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EBAB9521DD1FFA600536D28 /* LayoutExampleViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 050E7C6E19D22E19004363C2 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 050E7C7219D22E19004363C2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 050E7C7319D22E19004363C2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 08B9AAEC0A03243C3516AA96 /* 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 = ""; }; + 27E6141C83F216FCA6D7EAEE /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E2932001DD1E07900026492 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 5E2932021DD1E0B600026492 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; + 5E2932061DD1F06D00026492 /* OverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewViewController.swift; sourceTree = ""; }; + 5E2932081DD1F0F600026492 /* LayoutExampleNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutExampleNode.swift; sourceTree = ""; }; + 5E29320A1DD1F46500026492 /* OverviewCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewCellNode.swift; sourceTree = ""; }; + 5EBAB9501DD1FA8C00536D28 /* LayoutExampleNode+Layouts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LayoutExampleNode+Layouts.swift"; sourceTree = ""; }; + 5EBAB9521DD1FFA600536D28 /* LayoutExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutExampleViewController.swift; sourceTree = ""; }; + 9B402DAAE8C6A8B9BE1A506B /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 050E7C6B19D22E19004363C2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 161FE6897BB33A570A663F90 /* Pods_Sample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 050E7C6519D22E19004363C2 = { + isa = PBXGroup; + children = ( + 050E7C7019D22E19004363C2 /* Sample */, + 050E7C6F19D22E19004363C2 /* Products */, + 092C2001FE124604891D6E90 /* Frameworks */, + 655F2ABBD991CBDE7140FACE /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 050E7C6F19D22E19004363C2 /* Products */ = { + isa = PBXGroup; + children = ( + 050E7C6E19D22E19004363C2 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 050E7C7019D22E19004363C2 /* Sample */ = { + isa = PBXGroup; + children = ( + 050E7C7319D22E19004363C2 /* AppDelegate.swift */, + 5E2932061DD1F06D00026492 /* OverviewViewController.swift */, + 5E29320A1DD1F46500026492 /* OverviewCellNode.swift */, + 5EBAB9521DD1FFA600536D28 /* LayoutExampleViewController.swift */, + 5E2932081DD1F0F600026492 /* LayoutExampleNode.swift */, + 5EBAB9501DD1FA8C00536D28 /* LayoutExampleNode+Layouts.swift */, + 5E2932021DD1E0B600026492 /* Utilities.swift */, + 050E7C7119D22E19004363C2 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 050E7C7119D22E19004363C2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 5E2931FF1DD1E07900026492 /* LaunchScreen.storyboard */, + 050E7C7219D22E19004363C2 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 092C2001FE124604891D6E90 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 27E6141C83F216FCA6D7EAEE /* Pods_Sample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 655F2ABBD991CBDE7140FACE /* Pods */ = { + isa = PBXGroup; + children = ( + 9B402DAAE8C6A8B9BE1A506B /* Pods-Sample.debug.xcconfig */, + 08B9AAEC0A03243C3516AA96 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 050E7C6D19D22E19004363C2 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 050E7C8D19D22E1A004363C2 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + B8824BD0ED824BAD8268EC35 /* [CP] Check Pods Manifest.lock */, + 050E7C6A19D22E19004363C2 /* Sources */, + 050E7C6B19D22E19004363C2 /* Frameworks */, + 050E7C6C19D22E19004363C2 /* Resources */, + 941C5E41C54B4613A2D3B760 /* [CP] Copy Pods Resources */, + 1F5A9F09F5875F61862D0783 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 050E7C6E19D22E19004363C2 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 050E7C6619D22E19004363C2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 050E7C6D19D22E19004363C2 = { + CreatedOnToolsVersion = 6.0.1; + LastSwiftMigration = 0810; + }; + }; + }; + buildConfigurationList = 050E7C6919D22E19004363C2 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 050E7C6519D22E19004363C2; + productRefGroup = 050E7C6F19D22E19004363C2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 050E7C6D19D22E19004363C2 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 050E7C6C19D22E19004363C2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5E2932011DD1E07900026492 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1F5A9F09F5875F61862D0783 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 941C5E41C54B4613A2D3B760 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + B8824BD0ED824BAD8268EC35 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 050E7C6A19D22E19004363C2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EBAB9511DD1FA8C00536D28 /* LayoutExampleNode+Layouts.swift in Sources */, + 5E2932031DD1E0B600026492 /* Utilities.swift in Sources */, + 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */, + 5E2932091DD1F0F600026492 /* LayoutExampleNode.swift in Sources */, + 5EBAB9531DD1FFA600536D28 /* LayoutExampleViewController.swift in Sources */, + 5E29320B1DD1F46500026492 /* OverviewCellNode.swift in Sources */, + 5E2932071DD1F06D00026492 /* OverviewViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 5E2931FF1DD1E07900026492 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5E2932001DD1E07900026492 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 050E7C8B19D22E1A004363C2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 050E7C8C19D22E1A004363C2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 050E7C8E19D22E1A004363C2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9B402DAAE8C6A8B9BE1A506B /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 050E7C8F19D22E1A004363C2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 08B9AAEC0A03243C3516AA96 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 050E7C6919D22E19004363C2 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050E7C8B19D22E1A004363C2 /* Debug */, + 050E7C8C19D22E1A004363C2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 050E7C8D19D22E1A004363C2 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050E7C8E19D22E1A004363C2 /* Debug */, + 050E7C8F19D22E1A004363C2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 050E7C6619D22E19004363C2 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..05f94265c3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift new file mode 100644 index 0000000000..9a465c6de8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift @@ -0,0 +1,31 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = UIColor.white + window.rootViewController = UINavigationController(rootViewController: OverviewViewController()) + window.makeKeyAndVisible() + self.window = window + + UINavigationBar.appearance().barTintColor = UIColor(red: 47/255.0, green: 184/255.0, blue: 253/255.0, alpha: 1.0) + UINavigationBar.appearance().tintColor = .white + UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white] + + return true + } + +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..f4fc7f7736 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Info.plist new file mode 100644 index 0000000000..6105445463 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode+Layouts.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode+Layouts.swift new file mode 100644 index 0000000000..cf6bfbf1a6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode+Layouts.swift @@ -0,0 +1,85 @@ +// +// LayoutExampleNode+Layouts.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +extension HeaderWithRightAndLeftItems { + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let nameLocationStack = ASStackLayoutSpec.vertical() + nameLocationStack.style.flexShrink = 1.0 + nameLocationStack.style.flexGrow = 1.0 + + if postLocationNode.attributedText != nil { + nameLocationStack.children = [userNameNode, postLocationNode] + } else { + nameLocationStack.children = [userNameNode] + } + + let headerStackSpec = ASStackLayoutSpec(direction: .horizontal, + spacing: 40, + justifyContent: .start, + alignItems: .center, + children: [nameLocationStack, postTimeNode]) + + return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10), child: headerStackSpec) + } + +} + +extension PhotoWithInsetTextOverlay { + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let photoDimension: CGFloat = constrainedSize.max.width / 4.0 + photoNode.style.preferredSize = CGSize(width: photoDimension, height: photoDimension) + + // INFINITY is used to make the inset unbounded + let insets = UIEdgeInsets(top: CGFloat.infinity, left: 12, bottom: 12, right: 12) + let textInsetSpec = ASInsetLayoutSpec(insets: insets, child: titleNode) + + return ASOverlayLayoutSpec(child: photoNode, overlay: textInsetSpec) + } + +} + +extension PhotoWithOutsetIconOverlay { + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + iconNode.style.preferredSize = CGSize(width: 40, height: 40); + iconNode.style.layoutPosition = CGPoint(x: 150, y: 0); + + photoNode.style.preferredSize = CGSize(width: 150, height: 150); + photoNode.style.layoutPosition = CGPoint(x: 40 / 2.0, y: 40 / 2.0); + + let absoluteSpec = ASAbsoluteLayoutSpec(children: [photoNode, iconNode]) + + // ASAbsoluteLayoutSpec's .sizing property recreates the behavior of ASDK Layout API 1.0's "ASStaticLayoutSpec" + absoluteSpec.sizing = .sizeToFit + + return absoluteSpec; + } + +} + +extension FlexibleSeparatorSurroundingContent { + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + topSeparator.style.flexGrow = 1.0 + bottomSeparator.style.flexGrow = 1.0 + textNode.style.alignSelf = .center + + let verticalStackSpec = ASStackLayoutSpec.vertical() + verticalStackSpec.spacing = 20 + verticalStackSpec.justifyContent = .center + verticalStackSpec.children = [topSeparator, textNode, bottomSeparator] + + return ASInsetLayoutSpec(insets:UIEdgeInsets(top: 60, left: 0, bottom: 60, right: 0), child: verticalStackSpec) + } + +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift new file mode 100644 index 0000000000..c113e07d37 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift @@ -0,0 +1,274 @@ +// +// LayoutExampleNode.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +class LayoutExampleNode: ASDisplayNode { + override required init() { + super.init() + automaticallyManagesSubnodes = true + backgroundColor = .white + } + + class func title() -> String { + assertionFailure("All layout example nodes must provide a title!") + return "" + } + + class func descriptionTitle() -> String? { + return nil + } +} + +class HeaderWithRightAndLeftItems : LayoutExampleNode { + let userNameNode = ASTextNode() + let postLocationNode = ASTextNode() + let postTimeNode = ASTextNode() + + required init() { + super.init() + + userNameNode.attributedText = NSAttributedString.attributedString(string: "hannahmbanana", fontSize: 20, color: .darkBlueColor()) + userNameNode.maximumNumberOfLines = 1 + userNameNode.truncationMode = .byTruncatingTail + + postLocationNode.attributedText = NSAttributedString.attributedString(string: "Sunset Beach, San Fransisco, CA", fontSize: 20, color: .lightBlueColor()) + postLocationNode.maximumNumberOfLines = 1 + postLocationNode.truncationMode = .byTruncatingTail + + postTimeNode.attributedText = NSAttributedString.attributedString(string: "30m", fontSize: 20, color: .lightGray) + postTimeNode.maximumNumberOfLines = 1 + postTimeNode.truncationMode = .byTruncatingTail + } + + override class func title() -> String { + return "Header with left and right justified text" + } + + override class func descriptionTitle() -> String? { + return "try rotating me!" + } +} + +class PhotoWithInsetTextOverlay : LayoutExampleNode { + let photoNode = ASNetworkImageNode() + let titleNode = ASTextNode() + + required init() { + super.init() + + backgroundColor = .clear + + photoNode.url = URL(string: "http://texturegroup.org/static/images/layout-examples-photo-with-inset-text-overlay-photo.png") + photoNode.willDisplayNodeContentWithRenderingContext = { context, drawParameters in + let bounds = context.boundingBoxOfClipPath + UIBezierPath(roundedRect: bounds, cornerRadius: 10).addClip() + } + + titleNode.attributedText = NSAttributedString.attributedString(string: "family fall hikes", fontSize: 16, color: .white) + titleNode.truncationAttributedText = NSAttributedString.attributedString(string: "...", fontSize: 16, color: .white) + titleNode.maximumNumberOfLines = 2 + titleNode.truncationMode = .byTruncatingTail + } + + override class func title() -> String { + return "Photo with inset text overlay" + } + + override class func descriptionTitle() -> String? { + return "try rotating me!" + } +} + +class PhotoWithOutsetIconOverlay : LayoutExampleNode { + let photoNode = ASNetworkImageNode() + let iconNode = ASNetworkImageNode() + + required init() { + super.init() + + photoNode.url = URL(string: "http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png") + + iconNode.url = URL(string: "http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png") + + iconNode.imageModificationBlock = { image in + let profileImageSize = CGSize(width: 60, height: 60) + return image.makeCircularImage(size: profileImageSize, borderWidth: 10) + } + } + + override class func title() -> String { + return "Photo with outset icon overlay" + } + + override class func descriptionTitle() -> String? { + return nil + } +} + +class FlexibleSeparatorSurroundingContent : LayoutExampleNode { + let topSeparator = ASImageNode() + let bottomSeparator = ASImageNode() + let textNode = ASTextNode() + + required init() { + super.init() + + topSeparator.image = UIImage.as_resizableRoundedImage(withCornerRadius: 1.0, cornerColor: .black, fill: .black) + + textNode.attributedText = NSAttributedString.attributedString(string: "this is a long text node", fontSize: 16, color: .black) + + bottomSeparator.image = UIImage.as_resizableRoundedImage(withCornerRadius: 1.0, cornerColor: .black, fill: .black) + } + + override class func title() -> String { + return "Top and bottom cell separator lines" + } + + override class func descriptionTitle() -> String? { + return "try rotating me!" + } +} + +class CornerLayoutSample : PhotoWithOutsetIconOverlay { + let photoNode1 = ASImageNode() + let photoNode2 = ASImageNode() + let dotNode = ASImageNode() + let badgeTextNode = ASTextNode() + let badgeImageNode = ASImageNode() + + struct ImageSize { + static let avatar = CGSize(width: 100, height: 100) + static let icon = CGSize(width: 26, height: 26) + } + + struct ImageColor { + static let avatar = UIColor.lightGray + static let icon = UIColor.red + } + + required init() { + super.init() + + let avatarImage = UIImage.draw(size: ImageSize.avatar, fillColor: ImageColor.avatar) { () -> UIBezierPath in + return UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: ImageSize.avatar), cornerRadius: ImageSize.avatar.width / 20) + } + + let iconImage = UIImage.draw(size: ImageSize.icon, fillColor: ImageColor.icon) { () -> UIBezierPath in + return UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: ImageSize.icon)) + } + + photoNode1.image = avatarImage + photoNode2.image = avatarImage + dotNode.image = iconImage + + badgeTextNode.attributedText = NSAttributedString.attributedString(string: " 999+ ", fontSize: 20, color: .white) + + badgeImageNode.image = UIImage.as_resizableRoundedImage(withCornerRadius: 12, cornerColor: .clear, fill: .red) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + photoNode.style.preferredSize = ImageSize.avatar + iconNode.style.preferredSize = ImageSize.icon + + let badgeSpec = ASBackgroundLayoutSpec(child: badgeTextNode, background: badgeImageNode) + let cornerSpec1 = ASCornerLayoutSpec(child: photoNode1, corner: dotNode, location: .topRight) + let cornerSpec2 = ASCornerLayoutSpec(child: photoNode2, corner: badgeSpec, location: .topRight) + let cornerSpec3 = ASCornerLayoutSpec(child: photoNode, corner: iconNode, location: .topRight) + + cornerSpec1.offset = CGPoint(x: -3, y: 3) + + let stackSpec = ASStackLayoutSpec.vertical() + stackSpec.spacing = 40 + stackSpec.children = [cornerSpec1, cornerSpec2, cornerSpec3] + + return stackSpec + } + + override class func title() -> String { + return "Declarative way for Corner image Layout" + } + + override class func descriptionTitle() -> String? { + return nil + } +} + +class UserProfileSample : LayoutExampleNode { + + let badgeNode = ASImageNode() + let avatarNode = ASImageNode() + let usernameNode = ASTextNode() + let subtitleNode = ASTextNode() + + struct ImageSize { + static let avatar = CGSize(width: 44, height: 44) + static let badge = CGSize(width: 15, height: 15) + } + + struct ImageColor { + static let avatar = UIColor.lightGray + static let badge = UIColor.red + } + + required init() { + super.init() + + avatarNode.image = UIImage.draw(size: ImageSize.avatar, fillColor: ImageColor.avatar) { () -> UIBezierPath in + return UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: ImageSize.avatar)) + } + + badgeNode.image = UIImage.draw(size: ImageSize.badge, fillColor: ImageColor.badge) { () -> UIBezierPath in + return UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: ImageSize.badge)) + } + + makeSingleLine(for: usernameNode, with: "Hello world", fontSize: 17, textColor: .black) + makeSingleLine(for: subtitleNode, with: "This is a long long subtitle, with a long long appended string.", fontSize: 14, textColor: .lightGray) + } + + private func makeSingleLine(for node: ASTextNode, with text: String, fontSize: CGFloat, textColor: UIColor) { + node.attributedText = NSAttributedString.attributedString(string: text, fontSize: fontSize, color: textColor) + node.maximumNumberOfLines = 1 + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let avatarBox = ASCornerLayoutSpec(child: avatarNode, corner: badgeNode, location: .bottomRight) + avatarBox.offset = CGPoint(x: -6, y: -6) + + let textBox = ASStackLayoutSpec.vertical() + textBox.justifyContent = .spaceAround + textBox.children = [usernameNode, subtitleNode] + + let profileBox = ASStackLayoutSpec.horizontal() + profileBox.spacing = 10 + profileBox.children = [avatarBox, textBox] + + // Apply text truncation + let elems: [ASLayoutElement] = [usernameNode, subtitleNode, textBox, profileBox] + for elem in elems { + elem.style.flexShrink = 1 + } + + let insetBox = ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 120, left: 20, bottom: CGFloat.infinity, right: 20), + child: profileBox + ) + + return insetBox + } + + override class func title() -> String { + return "Common user profile layout." + } + + override class func descriptionTitle() -> String? { + return "For corner image layout and text truncation." + } + +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleViewController.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleViewController.swift new file mode 100644 index 0000000000..25a391694f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleViewController.swift @@ -0,0 +1,38 @@ +// +// LayoutExampleViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +class LayoutExampleViewController: ASViewController { + + let customNode: LayoutExampleNode + + init(layoutExampleType: LayoutExampleNode.Type) { + customNode = layoutExampleType.init() + + super.init(node: ASDisplayNode()) + self.title = "Layout Example" + + self.node.addSubnode(customNode) + let needsOnlyYCentering = (layoutExampleType.isEqual(HeaderWithRightAndLeftItems.self) || layoutExampleType.isEqual(FlexibleSeparatorSurroundingContent.self)) + + self.node.backgroundColor = needsOnlyYCentering ? .lightGray : .white + + self.node.layoutSpecBlock = { [weak self] node, constrainedSize in + guard let customNode = self?.customNode else { return ASLayoutSpec() } + return ASCenterLayoutSpec(centeringOptions: needsOnlyYCentering ? .Y : .XY, + sizingOptions: .minimumXY, + child: customNode) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/OverviewCellNode.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/OverviewCellNode.swift new file mode 100644 index 0000000000..e6e511b62d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/OverviewCellNode.swift @@ -0,0 +1,38 @@ +// +// OverviewCellNode.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +class OverviewCellNode: ASCellNode { + + let layoutExampleType: LayoutExampleNode.Type + + fileprivate let titleNode = ASTextNode() + fileprivate let descriptionNode = ASTextNode() + + init(layoutExampleType le: LayoutExampleNode.Type) { + layoutExampleType = le + + super.init() + self.automaticallyManagesSubnodes = true + + titleNode.attributedText = NSAttributedString.attributedString(string: layoutExampleType.title(), fontSize: 16, color: .black) + descriptionNode.attributedText = NSAttributedString.attributedString(string: layoutExampleType.descriptionTitle(), fontSize: 12, color: .lightGray) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let verticalStackSpec = ASStackLayoutSpec.vertical() + verticalStackSpec.alignItems = .start + verticalStackSpec.spacing = 5.0 + verticalStackSpec.children = [titleNode, descriptionNode] + + return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 10), child: verticalStackSpec) + } + +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift new file mode 100644 index 0000000000..c43856f11a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift @@ -0,0 +1,64 @@ +// +// OverviewViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +class OverviewViewController: ASViewController { + let tableNode = ASTableNode() + let layoutExamples: [LayoutExampleNode.Type] + + init() { + layoutExamples = [ + HeaderWithRightAndLeftItems.self, + PhotoWithInsetTextOverlay.self, + PhotoWithOutsetIconOverlay.self, + FlexibleSeparatorSurroundingContent.self, + CornerLayoutSample.self, + UserProfileSample.self + ] + + super.init(node: tableNode) + + self.title = "Layout Examples" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) + tableNode.delegate = self + tableNode.dataSource = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let indexPath = tableNode.indexPathForSelectedRow { + tableNode.deselectRow(at: indexPath, animated: true) + } + } + +} + +extension OverviewViewController: ASTableDataSource { + func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { + return layoutExamples.count + } + + func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { + return OverviewCellNode(layoutExampleType: layoutExamples[indexPath.row]) + } +} + +extension OverviewViewController: ASTableDelegate { + func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { + let layoutExampleType = (tableNode.nodeForRow(at: indexPath) as! OverviewCellNode).layoutExampleType + let detail = LayoutExampleViewController(layoutExampleType: layoutExampleType) + self.navigationController?.pushViewController(detail, animated: true) + } +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift new file mode 100644 index 0000000000..f235be8166 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift @@ -0,0 +1,99 @@ +// +// Utilities.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import Foundation + +extension UIColor { + + static func darkBlueColor() -> UIColor { + return UIColor(red: 18.0/255.0, green: 86.0/255.0, blue: 136.0/255.0, alpha: 1.0) + } + + static func lightBlueColor() -> UIColor { + return UIColor(red: 0.0, green: 122.0/255.0, blue: 1.0, alpha: 1.0) + } + + static func duskColor() -> UIColor { + return UIColor(red: 255/255.0, green: 181/255.0, blue: 68/255.0, alpha: 1.0) + } + + static func customOrangeColor() -> UIColor { + return UIColor(red: 40/255.0, green: 43/255.0, blue: 53/255.0, alpha: 1.0) + } + +} + +extension UIImage { + + func makeCircularImage(size: CGSize, borderWidth width: CGFloat) -> UIImage { + // make a CGRect with the image's size + let circleRect = CGRect(origin: .zero, size: size) + + // begin the image context since we're not in a drawRect: + UIGraphicsBeginImageContextWithOptions(circleRect.size, false, 0) + + // create a UIBezierPath circle + let circle = UIBezierPath(roundedRect: circleRect, cornerRadius: circleRect.size.width * 0.5) + + // clip to the circle + circle.addClip() + + UIColor.white.set() + circle.fill() + + // draw the image in the circleRect *AFTER* the context is clipped + self.draw(in: circleRect) + + // create a border (for white background pictures) + if width > 0 { + circle.lineWidth = width; + UIColor.white.set() + circle.stroke() + } + + // get an image from the image context + let roundedImage = UIGraphicsGetImageFromCurrentImageContext(); + + // end the image context since we're not in a drawRect: + UIGraphicsEndImageContext(); + + return roundedImage ?? self + } + + class func draw(size: CGSize, fillColor: UIColor, shapeClosure: () -> UIBezierPath) -> UIImage { + UIGraphicsBeginImageContext(size) + + let path = shapeClosure() + path.addClip() + + fillColor.setFill() + path.fill() + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image! + } +} + +extension NSAttributedString { + + static func attributedString(string: String?, fontSize size: CGFloat, color: UIColor?) -> NSAttributedString? { + guard let string = string else { return nil } + + let attributes = [NSForegroundColorAttributeName: color ?? UIColor.black, + NSFontAttributeName: UIFont.boldSystemFont(ofSize: size)] + + let attributedString = NSMutableAttributedString(string: string, attributes: attributes) + + return attributedString + } + +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Podfile b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Podfile new file mode 100644 index 0000000000..08d1b7add6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c065fcab3f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,394 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 694993D21C8B334F00491CA5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D11C8B334F00491CA5 /* main.m */; }; + 694993D51C8B334F00491CA5 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D41C8B334F00491CA5 /* AppDelegate.m */; }; + 694993D81C8B334F00491CA5 /* OverviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 694993D71C8B334F00491CA5 /* OverviewViewController.m */; }; + 694993E01C8B334F00491CA5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */; }; + 77E904A7F1CA1E967CBECF1B /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19C07F7EA804F418514FC00F /* libPods-Sample.a */; }; + 8032A2FC1DCD07580083C469 /* Utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8032A2FB1DCD07580083C469 /* Utilities.m */; }; + 8032A2FF1DCD076E0083C469 /* LayoutExampleNodes.m in Sources */ = {isa = PBXBuildFile; fileRef = 8032A2FE1DCD076E0083C469 /* LayoutExampleNodes.m */; }; + 8092664B1DCD9CE1008373C2 /* OverviewCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 8092664A1DCD9CE1008373C2 /* OverviewCellNode.m */; }; + 8092664E1DCDA64A008373C2 /* LayoutExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8092664D1DCDA64A008373C2 /* LayoutExampleViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 19C07F7EA804F418514FC00F /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 49ABEBD1ABCEC54CD6585C1B /* 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 = ""; }; + 694993CD1C8B334F00491CA5 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 694993D11C8B334F00491CA5 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 694993D31C8B334F00491CA5 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 694993D41C8B334F00491CA5 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 694993D61C8B334F00491CA5 /* OverviewViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OverviewViewController.h; sourceTree = ""; }; + 694993D71C8B334F00491CA5 /* OverviewViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OverviewViewController.m; sourceTree = ""; }; + 694993DF1C8B334F00491CA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 694993E11C8B334F00491CA5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8032A2FA1DCD07580083C469 /* Utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utilities.h; sourceTree = ""; }; + 8032A2FB1DCD07580083C469 /* Utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utilities.m; sourceTree = ""; }; + 8032A2FD1DCD076E0083C469 /* LayoutExampleNodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutExampleNodes.h; sourceTree = ""; }; + 8032A2FE1DCD076E0083C469 /* LayoutExampleNodes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutExampleNodes.m; sourceTree = ""; }; + 809266491DCD9CE1008373C2 /* OverviewCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverviewCellNode.h; sourceTree = ""; }; + 8092664A1DCD9CE1008373C2 /* OverviewCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverviewCellNode.m; sourceTree = ""; }; + 8092664C1DCDA64A008373C2 /* LayoutExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutExampleViewController.h; sourceTree = ""; }; + 8092664D1DCDA64A008373C2 /* LayoutExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutExampleViewController.m; sourceTree = ""; }; + C2FE4C0B2EE2F08E2186823E /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 694993CA1C8B334F00491CA5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 77E904A7F1CA1E967CBECF1B /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 694993C41C8B334F00491CA5 = { + isa = PBXGroup; + children = ( + 694993CF1C8B334F00491CA5 /* Sample */, + 694993CE1C8B334F00491CA5 /* Products */, + B4D85F4D35E1EEC1C00CAFE8 /* Pods */, + E5CE41038A61211B6B519735 /* Frameworks */, + ); + sourceTree = ""; + }; + 694993CE1C8B334F00491CA5 /* Products */ = { + isa = PBXGroup; + children = ( + 694993CD1C8B334F00491CA5 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 694993CF1C8B334F00491CA5 /* Sample */ = { + isa = PBXGroup; + children = ( + 694993D31C8B334F00491CA5 /* AppDelegate.h */, + 694993D41C8B334F00491CA5 /* AppDelegate.m */, + 694993D61C8B334F00491CA5 /* OverviewViewController.h */, + 694993D71C8B334F00491CA5 /* OverviewViewController.m */, + 809266491DCD9CE1008373C2 /* OverviewCellNode.h */, + 8092664A1DCD9CE1008373C2 /* OverviewCellNode.m */, + 8092664C1DCDA64A008373C2 /* LayoutExampleViewController.h */, + 8092664D1DCDA64A008373C2 /* LayoutExampleViewController.m */, + 8032A2FD1DCD076E0083C469 /* LayoutExampleNodes.h */, + 8032A2FE1DCD076E0083C469 /* LayoutExampleNodes.m */, + 8032A2FA1DCD07580083C469 /* Utilities.h */, + 8032A2FB1DCD07580083C469 /* Utilities.m */, + 694993D01C8B334F00491CA5 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 694993D01C8B334F00491CA5 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 694993E11C8B334F00491CA5 /* Info.plist */, + 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */, + 694993D11C8B334F00491CA5 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + B4D85F4D35E1EEC1C00CAFE8 /* Pods */ = { + isa = PBXGroup; + children = ( + C2FE4C0B2EE2F08E2186823E /* Pods-Sample.debug.xcconfig */, + 49ABEBD1ABCEC54CD6585C1B /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + E5CE41038A61211B6B519735 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 19C07F7EA804F418514FC00F /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 694993CC1C8B334F00491CA5 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 694993E41C8B334F00491CA5 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 339DA21AF893915BAA46F6A1 /* [CP] Check Pods Manifest.lock */, + 694993C91C8B334F00491CA5 /* Sources */, + 694993CA1C8B334F00491CA5 /* Frameworks */, + 694993CB1C8B334F00491CA5 /* Resources */, + 06EE2E0ABEB6289D4775A867 /* [CP] Copy Pods Resources */, + 23FC03B282CBD9014D868DF6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 694993CD1C8B334F00491CA5 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 694993C51C8B334F00491CA5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = AsyncDisplayKit; + TargetAttributes = { + 694993CC1C8B334F00491CA5 = { + CreatedOnToolsVersion = 7.2.1; + }; + }; + }; + buildConfigurationList = 694993C81C8B334F00491CA5 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 694993C41C8B334F00491CA5; + productRefGroup = 694993CE1C8B334F00491CA5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 694993CC1C8B334F00491CA5 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 694993CB1C8B334F00491CA5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 694993E01C8B334F00491CA5 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 06EE2E0ABEB6289D4775A867 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 23FC03B282CBD9014D868DF6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 339DA21AF893915BAA46F6A1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 694993C91C8B334F00491CA5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8092664B1DCD9CE1008373C2 /* OverviewCellNode.m in Sources */, + 694993D81C8B334F00491CA5 /* OverviewViewController.m in Sources */, + 694993D51C8B334F00491CA5 /* AppDelegate.m in Sources */, + 694993D21C8B334F00491CA5 /* main.m in Sources */, + 8032A2FF1DCD076E0083C469 /* LayoutExampleNodes.m in Sources */, + 8032A2FC1DCD07580083C469 /* Utilities.m in Sources */, + 8092664E1DCDA64A008373C2 /* LayoutExampleViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 694993DE1C8B334F00491CA5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 694993DF1C8B334F00491CA5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 694993E21C8B334F00491CA5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 694993E31C8B334F00491CA5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 694993E51C8B334F00491CA5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2FE4C0B2EE2F08E2186823E /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 694993E61C8B334F00491CA5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 49ABEBD1ABCEC54CD6585C1B /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 694993C81C8B334F00491CA5 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 694993E21C8B334F00491CA5 /* Debug */, + 694993E31C8B334F00491CA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 694993E41C8B334F00491CA5 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 694993E51C8B334F00491CA5 /* Debug */, + 694993E61C8B334F00491CA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 694993C51C8B334F00491CA5 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..10e9c51e22 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/AppDelegate.h new file mode 100644 index 0000000000..d5c7194563 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/AppDelegate.h @@ -0,0 +1,15 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder +@property (strong, nonatomic) UIWindow *window; +@end + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/AppDelegate.m new file mode 100644 index 0000000000..73fc27af6e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/AppDelegate.m @@ -0,0 +1,29 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "OverviewViewController.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 = [[UINavigationController alloc] initWithRootViewController:[OverviewViewController new]]; + [self.window makeKeyAndVisible]; + + [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:47/255.0 green:184/255.0 blue:253/255.0 alpha:1.0]]; + [[UINavigationBar appearance] setTintColor:[UIColor whiteColor]]; + [[UINavigationBar appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor whiteColor]}];; + + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..f4fc7f7736 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Info.plist new file mode 100644 index 0000000000..6105445463 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h new file mode 100644 index 0000000000..484ba5d418 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h @@ -0,0 +1,33 @@ +// +// LayoutExampleNodes.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface LayoutExampleNode : ASDisplayNode ++ (NSString *)title; ++ (NSString *)descriptionTitle; +@end + +@interface HeaderWithRightAndLeftItems : LayoutExampleNode +@end + +@interface PhotoWithInsetTextOverlay : LayoutExampleNode +@end + +@interface PhotoWithOutsetIconOverlay : LayoutExampleNode +@end + +@interface FlexibleSeparatorSurroundingContent : LayoutExampleNode +@end + +@interface CornerLayoutExample : PhotoWithOutsetIconOverlay +@end + +@interface UserProfileSample : LayoutExampleNode +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m new file mode 100644 index 0000000000..5d9d63663f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m @@ -0,0 +1,461 @@ +// +// LayoutExampleNodes.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "LayoutExampleNodes.h" + +#import + +#import "Utilities.h" + +@interface HeaderWithRightAndLeftItems () +@property (nonatomic, strong) ASTextNode *usernameNode; +@property (nonatomic, strong) ASTextNode *postLocationNode; +@property (nonatomic, strong) ASTextNode *postTimeNode; +@end + +@interface PhotoWithInsetTextOverlay () +@property (nonatomic, strong) ASNetworkImageNode *photoNode; +@property (nonatomic, strong) ASTextNode *titleNode; +@end + +@interface PhotoWithOutsetIconOverlay () +@property (nonatomic, strong) ASNetworkImageNode *photoNode; +@property (nonatomic, strong) ASNetworkImageNode *iconNode; +@end + +@interface FlexibleSeparatorSurroundingContent () +@property (nonatomic, strong) ASImageNode *topSeparator; +@property (nonatomic, strong) ASImageNode *bottomSeparator; +@property (nonatomic, strong) ASTextNode *textNode; +@end + +@implementation HeaderWithRightAndLeftItems + ++ (NSString *)title +{ + return @"Header with left and right justified text"; +} + ++ (NSString *)descriptionTitle +{ + return @"try rotating me!"; +} + +- (instancetype)init +{ + self = [super init]; + + if (self) { + _usernameNode = [[ASTextNode alloc] init]; + _usernameNode.attributedText = [NSAttributedString attributedStringWithString:@"hannahmbanana" + fontSize:20 + color:[UIColor darkBlueColor]]; + _usernameNode.maximumNumberOfLines = 1; + _usernameNode.truncationMode = NSLineBreakByTruncatingTail; + + _postLocationNode = [[ASTextNode alloc] init]; + _postLocationNode.maximumNumberOfLines = 1; + _postLocationNode.attributedText = [NSAttributedString attributedStringWithString:@"Sunset Beach, San Fransisco, CA" + fontSize:20 + color:[UIColor lightBlueColor]]; + _postLocationNode.maximumNumberOfLines = 1; + _postLocationNode.truncationMode = NSLineBreakByTruncatingTail; + + _postTimeNode = [[ASTextNode alloc] init]; + _postTimeNode.attributedText = [NSAttributedString attributedStringWithString:@"30m" + fontSize:20 + color:[UIColor lightGrayColor]]; + _postLocationNode.maximumNumberOfLines = 1; + _postLocationNode.truncationMode = NSLineBreakByTruncatingTail; + } + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + + ASStackLayoutSpec *nameLocationStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + nameLocationStack.style.flexShrink = 1.0; + nameLocationStack.style.flexGrow = 1.0; + + if (_postLocationNode.attributedText) { + nameLocationStack.children = @[_usernameNode, _postLocationNode]; + } else { + nameLocationStack.children = @[_usernameNode]; + } + + ASStackLayoutSpec *headerStackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:40 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[nameLocationStack, _postTimeNode]]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 10, 0, 10) child:headerStackSpec]; +} + +@end + + +@implementation PhotoWithInsetTextOverlay + ++ (NSString *)title +{ + return @"Photo with inset text overlay"; +} + ++ (NSString *)descriptionTitle +{ + return @"try rotating me!"; +} + +- (instancetype)init +{ + self = [super init]; + + if (self) { + self.backgroundColor = [UIColor clearColor]; + + _photoNode = [[ASNetworkImageNode alloc] init]; + _photoNode.URL = [NSURL URLWithString:@"http://texturegroup.org/static/images/layout-examples-photo-with-inset-text-overlay-photo.png"]; + _photoNode.willDisplayNodeContentWithRenderingContext = ^(CGContextRef context, id drawParameters) { + CGRect bounds = CGContextGetClipBoundingBox(context); + [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:10] addClip]; + }; + + _titleNode = [[ASTextNode alloc] init]; + _titleNode.maximumNumberOfLines = 2; + _titleNode.truncationMode = NSLineBreakByTruncatingTail; + _titleNode.truncationAttributedText = [NSAttributedString attributedStringWithString:@"..." fontSize:16 color:[UIColor whiteColor]]; + _titleNode.attributedText = [NSAttributedString attributedStringWithString:@"family fall hikes" fontSize:16 color:[UIColor whiteColor]]; + } + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGFloat photoDimension = constrainedSize.max.width / 4.0; + _photoNode.style.preferredSize = CGSizeMake(photoDimension, photoDimension); + + // INFINITY is used to make the inset unbounded + UIEdgeInsets insets = UIEdgeInsetsMake(INFINITY, 12, 12, 12); + ASInsetLayoutSpec *textInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:_titleNode]; + + return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:_photoNode overlay:textInsetSpec];; +} + +@end + + +@implementation PhotoWithOutsetIconOverlay + ++ (NSString *)title +{ + return @"Photo with outset icon overlay"; +} + +- (instancetype)init +{ + self = [super init]; + + if (self) { + _photoNode = [[ASNetworkImageNode alloc] init]; + _photoNode.URL = [NSURL URLWithString:@"http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png"]; + + _iconNode = [[ASNetworkImageNode alloc] init]; + _iconNode.URL = [NSURL URLWithString:@"http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png"]; + + [_iconNode setImageModificationBlock:^UIImage *(UIImage *image) { // FIXME: in framework autocomplete for setImageModificationBlock line seems broken + CGSize profileImageSize = CGSizeMake(60, 60); + return [image makeCircularImageWithSize:profileImageSize withBorderWidth:10]; + }]; + } + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + _iconNode.style.preferredSize = CGSizeMake(40, 40); + _iconNode.style.layoutPosition = CGPointMake(150, 0); + + _photoNode.style.preferredSize = CGSizeMake(150, 150); + _photoNode.style.layoutPosition = CGPointMake(40 / 2.0, 40 / 2.0); + + ASAbsoluteLayoutSpec *absoluteSpec = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[_photoNode, _iconNode]]; + + // ASAbsoluteLayoutSpec's .sizing property recreates the behavior of ASDK Layout API 1.0's "ASStaticLayoutSpec" + absoluteSpec.sizing = ASAbsoluteLayoutSpecSizingSizeToFit; + + return absoluteSpec; +} + + + +@end + + +@implementation FlexibleSeparatorSurroundingContent + ++ (NSString *)title +{ + return @"Top and bottom cell separator lines"; +} + ++ (NSString *)descriptionTitle +{ + return @"try rotating me!"; +} + +- (instancetype)init +{ + self = [super init]; + + if (self) { + self.backgroundColor = [UIColor whiteColor]; + + _topSeparator = [[ASImageNode alloc] init]; + _topSeparator.image = [UIImage as_resizableRoundedImageWithCornerRadius:1.0 cornerColor:[UIColor blackColor] fillColor:[UIColor blackColor]]; + + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [NSAttributedString attributedStringWithString:@"this is a long text node" + fontSize:16 + color:[UIColor blackColor]]; + + _bottomSeparator = [[ASImageNode alloc] init]; + _bottomSeparator.image = [UIImage as_resizableRoundedImageWithCornerRadius:1.0 cornerColor:[UIColor blackColor] fillColor:[UIColor blackColor]]; + } + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + _topSeparator.style.flexGrow = 1.0; + _bottomSeparator.style.flexGrow = 1.0; + _textNode.style.alignSelf = ASStackLayoutAlignSelfCenter; + + ASStackLayoutSpec *verticalStackSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackSpec.spacing = 20; + verticalStackSpec.justifyContent = ASStackLayoutJustifyContentCenter; + verticalStackSpec.children = @[_topSeparator, _textNode, _bottomSeparator]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(60, 0, 60, 0) child:verticalStackSpec]; +} + +@end + +@interface CornerLayoutExample () +@property (nonatomic, strong) ASImageNode *dotNode; +@property (nonatomic, strong) ASImageNode *photoNode1; +@property (nonatomic, strong) ASTextNode *badgeTextNode; +@property (nonatomic, strong) ASImageNode *badgeImageNode; +@property (nonatomic, strong) ASImageNode *photoNode2; +@end + +@implementation CornerLayoutExample + +static CGFloat const kSampleAvatarSize = 100; +static CGFloat const kSampleIconSize = 26; +static CGFloat const kSampleBadgeCornerRadius = 12; + ++ (NSString *)title +{ + return @"Declarative way for Corner image Layout"; +} + ++ (NSString *)descriptionTitle +{ + return nil; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + UIImage *avatarImage = [self avatarImageWithSize:CGSizeMake(kSampleAvatarSize, kSampleAvatarSize)]; + UIImage *cornerImage = [self cornerImageWithSize:CGSizeMake(kSampleIconSize, kSampleIconSize)]; + + NSAttributedString *numberText = [NSAttributedString attributedStringWithString:@" 999+ " fontSize:20 color:UIColor.whiteColor]; + + _dotNode = [ASImageNode new]; + _dotNode.image = cornerImage; + + _photoNode1 = [ASImageNode new]; + _photoNode1.image = avatarImage; + + _badgeTextNode = [ASTextNode new]; + _badgeTextNode.attributedText = numberText; + + _badgeImageNode = [ASImageNode new]; + _badgeImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:kSampleBadgeCornerRadius + cornerColor:UIColor.clearColor + fillColor:UIColor.redColor]; + + _photoNode2 = [ASImageNode new]; + _photoNode2.image = avatarImage; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + + ASBackgroundLayoutSpec *badgeSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:_badgeTextNode + background:_badgeImageNode]; + + ASCornerLayoutSpec *cornerSpec1 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:_photoNode1 corner:_dotNode location:ASCornerLayoutLocationTopRight]; + cornerSpec1.offset = CGPointMake(-3, 3); + + ASCornerLayoutSpec *cornerSpec2 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:_photoNode2 corner:badgeSpec location:ASCornerLayoutLocationTopRight]; + + self.photoNode.style.preferredSize = CGSizeMake(kSampleAvatarSize, kSampleAvatarSize); + self.iconNode.style.preferredSize = CGSizeMake(kSampleIconSize, kSampleIconSize); + + ASCornerLayoutSpec *cornerSpec3 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:self.photoNode corner:self.iconNode location:ASCornerLayoutLocationTopRight]; + + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + stackSpec.spacing = 40; + stackSpec.children = @[cornerSpec1, cornerSpec2, cornerSpec3]; + + return stackSpec; +} + +- (UIImage *)avatarImageWithSize:(CGSize)size +{ + return [UIImage imageWithSize:size fillColor:UIColor.lightGrayColor shapeBlock:^UIBezierPath *{ + CGRect rect = (CGRect){ CGPointZero, size }; + return [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:MIN(size.width, size.height) / 20]; + }]; +} + +- (UIImage *)cornerImageWithSize:(CGSize)size +{ + return [UIImage imageWithSize:size fillColor:UIColor.redColor shapeBlock:^UIBezierPath *{ + return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, size }]; + }]; +} + +@end + + +@interface UserProfileSample () +@property (nonatomic, strong) ASImageNode *badgeNode; +@property (nonatomic, strong) ASImageNode *avatarNode; +@property (nonatomic, strong) ASTextNode *usernameNode; +@property (nonatomic, strong) ASTextNode *subtitleNode; +@property (nonatomic, assign) CGFloat photoSizeValue; +@property (nonatomic, assign) CGFloat iconSizeValue; +@end + +@implementation UserProfileSample + ++ (NSString *)title +{ + return @"Common user profile layout."; +} + ++ (NSString *)descriptionTitle +{ + return @"For corner image layout and text truncation."; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _photoSizeValue = 44; + _iconSizeValue = 15; + + CGSize iconSize = CGSizeMake(_iconSizeValue, _iconSizeValue); + CGSize photoSize = CGSizeMake(_photoSizeValue, _photoSizeValue); + + _badgeNode = [ASImageNode new]; + _badgeNode.style.preferredSize = iconSize; + _badgeNode.image = [UIImage imageWithSize:iconSize fillColor:UIColor.redColor shapeBlock:^UIBezierPath *{ + return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, iconSize }]; + }]; + + _avatarNode = [ASImageNode new]; + _avatarNode.style.preferredSize = photoSize; + _avatarNode.image = [UIImage imageWithSize:photoSize fillColor:UIColor.lightGrayColor shapeBlock:^UIBezierPath *{ + return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, photoSize }]; + }]; + + _usernameNode = [ASTextNode new]; + _usernameNode.attributedText = [NSAttributedString attributedStringWithString:@"Hello World" fontSize:17 color:UIColor.blackColor]; + _usernameNode.maximumNumberOfLines = 1; + + _subtitleNode = [ASTextNode new]; + _subtitleNode.attributedText = [NSAttributedString attributedStringWithString:@"This is a long long subtitle, with a long long appended string." fontSize:14 color:UIColor.lightGrayColor]; + _subtitleNode.maximumNumberOfLines = 1; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // Apply avatar with badge + // Normally, avatar's box size is the only photo size and it will not include the badge size. + // Otherwise, use includeCornerForSizeCalculation property to increase the box's size if needed. + ASCornerLayoutSpec *avatarBox = [ASCornerLayoutSpec new]; + avatarBox.child = _avatarNode; + avatarBox.corner = _badgeNode; + avatarBox.cornerLocation = ASCornerLayoutLocationBottomRight; + avatarBox.offset = CGPointMake(-6, -6); + + ASStackLayoutSpec *textBox = [ASStackLayoutSpec verticalStackLayoutSpec]; + textBox.justifyContent = ASStackLayoutJustifyContentSpaceAround; + textBox.children = @[_usernameNode, _subtitleNode]; + + ASStackLayoutSpec *profileBox = [ASStackLayoutSpec horizontalStackLayoutSpec]; + profileBox.spacing = 10; + profileBox.children = @[avatarBox, textBox]; + + // Apply text truncation. + NSArray *elems = @[_usernameNode, _subtitleNode, textBox, profileBox]; + for (id elem in elems) { + elem.style.flexShrink = 1; + } + + ASInsetLayoutSpec *profileInsetBox = [ASInsetLayoutSpec new]; + profileInsetBox.insets = UIEdgeInsetsMake(120, 20, INFINITY, 20); + profileInsetBox.child = profileBox; + + return profileInsetBox; +} + +@end + +@implementation LayoutExampleNode + ++ (NSString *)title +{ + NSAssert(NO, @"All layout example nodes must provide a title!"); + return nil; +} + ++ (NSString *)descriptionTitle +{ + return nil; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + self.automaticallyManagesSubnodes = YES; + self.backgroundColor = [UIColor whiteColor]; + } + return self; +} + +@end + diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleViewController.h b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleViewController.h new file mode 100644 index 0000000000..d087bcc77f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleViewController.h @@ -0,0 +1,15 @@ +// +// LayoutExampleViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface LayoutExampleViewController : ASViewController +- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithNode:(ASDisplayNode *)node NS_UNAVAILABLE; +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleViewController.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleViewController.m new file mode 100644 index 0000000000..71607a6f87 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/LayoutExampleViewController.m @@ -0,0 +1,47 @@ +// +// LayoutExampleViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "LayoutExampleViewController.h" +#import "LayoutExampleNodes.h" + +@interface LayoutExampleViewController () +@property (nonatomic, strong) LayoutExampleNode *customNode; +@end + +@implementation LayoutExampleViewController + +- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass +{ + NSAssert([layoutExampleClass isSubclassOfClass:[LayoutExampleNode class]], @"Must pass a subclass of LayoutExampleNode."); + + self = [super initWithNode:[ASDisplayNode new]]; + + if (self) { + self.title = @"Layout Example"; + + _customNode = [layoutExampleClass new]; + [self.node addSubnode:_customNode]; + + BOOL needsOnlyYCentering = [layoutExampleClass isEqual:[HeaderWithRightAndLeftItems class]] || + [layoutExampleClass isEqual:[FlexibleSeparatorSurroundingContent class]]; + + self.node.backgroundColor = needsOnlyYCentering ? [UIColor lightGrayColor] : [UIColor whiteColor]; + + __weak __typeof(self) weakself = self; + self.node.layoutSpecBlock = ^ASLayoutSpec*(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:needsOnlyYCentering ? ASCenterLayoutSpecCenteringY : ASCenterLayoutSpecCenteringXY + sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY + child:weakself.customNode]; + }; + } + + return self; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewCellNode.h b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewCellNode.h new file mode 100644 index 0000000000..7a98c031cd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewCellNode.h @@ -0,0 +1,19 @@ +// +// OverviewCellNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface OverviewCellNode : ASCellNode + +@property (nonatomic, strong) Class layoutExampleClass; + +- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewCellNode.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewCellNode.m new file mode 100644 index 0000000000..6cd4eb845f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewCellNode.m @@ -0,0 +1,52 @@ +// +// OverviewCellNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewCellNode.h" +#import "LayoutExampleNodes.h" +#import "Utilities.h" + +@interface OverviewCellNode () +@property (nonatomic, strong) ASTextNode *titleNode; +@property (nonatomic, strong) ASTextNode *descriptionNode; +@end + +@implementation OverviewCellNode + +- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass +{ + self = [super init]; + if (self) { + self.automaticallyManagesSubnodes = YES; + + _layoutExampleClass = layoutExampleClass; + + _titleNode = [[ASTextNode alloc] init]; + _titleNode.attributedText = [NSAttributedString attributedStringWithString:[layoutExampleClass title] + fontSize:16 + color:[UIColor blackColor]]; + + _descriptionNode = [[ASTextNode alloc] init]; + _descriptionNode.attributedText = [NSAttributedString attributedStringWithString:[layoutExampleClass descriptionTitle] + fontSize:12 + color:[UIColor lightGrayColor]]; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *verticalStackSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackSpec.alignItems = ASStackLayoutAlignItemsStart; + verticalStackSpec.spacing = 5.0; + verticalStackSpec.children = @[self.titleNode, self.descriptionNode]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 16, 10, 10) child:verticalStackSpec]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewViewController.h b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewViewController.h new file mode 100644 index 0000000000..83381429a3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewViewController.h @@ -0,0 +1,15 @@ +// +// OverviewViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + + +@interface OverviewViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewViewController.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewViewController.m new file mode 100644 index 0000000000..9d9700f5e2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/OverviewViewController.m @@ -0,0 +1,77 @@ +// +// OverviewViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverviewViewController.h" +#import "LayoutExampleNodes.h" +#import "LayoutExampleViewController.h" +#import "OverviewCellNode.h" + +@interface OverviewViewController () +@property (nonatomic, strong) NSArray *layoutExamples; +@property (nonatomic, strong) ASTableNode *tableNode; +@end + +@implementation OverviewViewController + +#pragma mark - Lifecycle Methods + +- (instancetype)init +{ + _tableNode = [ASTableNode new]; + self = [super initWithNode:_tableNode]; + + if (self) { + self.title = @"Layout Examples"; + self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; + + _tableNode.delegate = self; + _tableNode.dataSource = self; + + _layoutExamples = @[[HeaderWithRightAndLeftItems class], + [PhotoWithInsetTextOverlay class], + [PhotoWithOutsetIconOverlay class], + [FlexibleSeparatorSurroundingContent class], + [CornerLayoutExample class], + [UserProfileSample class] + ]; + } + + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + NSIndexPath *indexPath = _tableNode.indexPathForSelectedRow; + if (indexPath != nil) { + [_tableNode deselectRowAtIndexPath:indexPath animated:YES]; + } +} + +#pragma mark - ASTableDelegate, ASTableDataSource + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return [_layoutExamples count]; +} + +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return [[OverviewCellNode alloc] initWithLayoutExampleClass:_layoutExamples[indexPath.row]]; +} + +- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + Class layoutExampleClass = [[tableNode nodeForRowAtIndexPath:indexPath] layoutExampleClass]; + LayoutExampleViewController *detail = [[LayoutExampleViewController alloc] initWithLayoutExampleClass:layoutExampleClass]; + [self.navigationController pushViewController:detail animated:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Utilities.h b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Utilities.h new file mode 100644 index 0000000000..5719a4ab85 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Utilities.h @@ -0,0 +1,25 @@ +// +// Utilities.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface UIColor (Additions) ++ (UIColor *)darkBlueColor; ++ (UIColor *)lightBlueColor; +@end + +@interface UIImage (Additions) +- (UIImage *)makeCircularImageWithSize:(CGSize)size withBorderWidth:(CGFloat)width; ++ (UIImage *)imageWithSize:(CGSize)size fillColor:(UIColor *)fillColor shapeBlock:(UIBezierPath *(^)(void))shapeBlock; +@end + +@interface NSAttributedString (Additions) ++ (NSAttributedString *)attributedStringWithString:(NSString *)string fontSize:(CGFloat)size color:(UIColor *)color; +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Utilities.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Utilities.m new file mode 100644 index 0000000000..d999ddd32d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/Utilities.m @@ -0,0 +1,99 @@ +// +// Utilities.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "Utilities.h" + +#define StrokeRoundedImages 0 + +@implementation UIColor (Additions) + ++ (UIColor *)darkBlueColor +{ + return [UIColor colorWithRed:18.0/255.0 green:86.0/255.0 blue:136.0/255.0 alpha:1.0]; +} + ++ (UIColor *)lightBlueColor +{ + return [UIColor colorWithRed:0.0 green:122.0/255.0 blue:1.0 alpha:1.0]; +} + +@end + +@implementation UIImage (Additions) + +- (UIImage *)makeCircularImageWithSize:(CGSize)size withBorderWidth:(CGFloat)width +{ + // 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]; + + [[UIColor whiteColor] set]; + [circle fill]; + + // draw the image in the circleRect *AFTER* the context is clipped + [self drawInRect:circleRect]; + + // create a border (for white background pictures) + if (width > 0) { + circle.lineWidth = width; + [[UIColor whiteColor] set]; + [circle stroke]; + } + + // get an image from the image context + UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext(); + + // end the image context since we're not in a drawRect: + UIGraphicsEndImageContext(); + + return roundedImage; +} + ++ (UIImage *)imageWithSize:(CGSize)size fillColor:(UIColor *)fillColor shapeBlock:(UIBezierPath *(^)(void))shapeBlock +{ + UIGraphicsBeginImageContext(size); + [fillColor setFill]; + + UIBezierPath *path = shapeBlock(); + [path addClip]; + [path fill]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +@end + +@implementation NSAttributedString (Additions) + ++ (NSAttributedString *)attributedStringWithString:(NSString *)string fontSize:(CGFloat)size color:(nullable UIColor *)color +{ + if (string == nil) { + return nil; + } + + NSDictionary *attributes = @{NSForegroundColorAttributeName: color ? : [UIColor blackColor], + NSFontAttributeName: [UIFont boldSystemFontOfSize:size]}; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string]; + [attributedString addAttributes:attributes range:NSMakeRange(0, string.length)]; + + return attributedString; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/main.m b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/LayoutSpecExamples/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/PagerNode/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/PagerNode/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/PagerNode/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/PagerNode/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/PagerNode/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/PagerNode/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Podfile b/submodules/AsyncDisplayKit/examples/PagerNode/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..febfa871c5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,375 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 252041E21C167DFC00E264C8 /* PageNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 252041E11C167DFC00E264C8 /* PageNode.m */; }; + 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 */; }; + DFE855DDBC731242D3515B58 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C284F7E957985CA251284B05 /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 1C47DEC3F9D2BD9AD5F5CD67 /* 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 = ""; }; + 252041E01C167DFC00E264C8 /* PageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PageNode.h; sourceTree = ""; }; + 252041E11C167DFC00E264C8 /* PageNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PageNode.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; }; + 79ED4D85CC60068C341CFD77 /* 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 = ""; }; + C284F7E957985CA251284B05 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DFE855DDBC731242D3515B58 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 252041E01C167DFC00E264C8 /* PageNode.h */, + 252041E11C167DFC00E264C8 /* PageNode.m */, + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + C284F7E957985CA251284B05 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */, + 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0710; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + 252041E21C167DFC00E264C8 /* PageNode.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..0b71c455d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/AppDelegate.m new file mode 100644 index 0000000000..f8437855b0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/PageNode.h b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/PageNode.h new file mode 100644 index 0000000000..f4346289c5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/PageNode.h @@ -0,0 +1,14 @@ +// +// PageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface PageNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/PageNode.m b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/PageNode.m new file mode 100644 index 0000000000..bedd46f0c0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/PageNode.m @@ -0,0 +1,25 @@ +// +// PageNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PageNode.h" + +@implementation PageNode + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + return constrainedSize; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + NSLog(@"didEnterPreloadState for node: %@", self); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/ViewController.h new file mode 100644 index 0000000000..3af731c848 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/ViewController.m new file mode 100644 index 0000000000..b55fa343f9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/ViewController.m @@ -0,0 +1,73 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import + +#import "PageNode.h" + +static UIColor *randomColor() { + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; +} + +@interface ViewController () + +@end + +@implementation ViewController + +- (instancetype)init +{ + self = [super initWithNode:[[ASPagerNode alloc] init]]; + if (self == nil) { + return self; + } + + self.title = @"Pages"; + self.node.dataSource = self; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Next" style:UIBarButtonItemStylePlain target:self action:@selector(scrollToNextPage:)]; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Previous" style:UIBarButtonItemStylePlain target:self action:@selector(scrollToPreviousPage:)]; + self.automaticallyAdjustsScrollViewInsets = NO; + return self; +} + +#pragma mark - Actions + +- (void)scrollToNextPage:(id)sender +{ + [self.node scrollToPageAtIndex:self.node.currentPageIndex+1 animated:YES]; +} + +- (void)scrollToPreviousPage:(id)sender +{ + [self.node scrollToPageAtIndex:self.node.currentPageIndex-1 animated:YES]; +} + +#pragma mark - ASPagerNodeDataSource + +- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode +{ + return 5; +} + +- (ASCellNodeBlock)pagerNode:(ASPagerNode *)pagerNode nodeBlockAtIndex:(NSInteger)index +{ + return ^{ + PageNode *page = [[PageNode alloc] init]; + page.backgroundColor = randomColor(); + return page; + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/PagerNode/Sample/main.m b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/PagerNode/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/README.md b/submodules/AsyncDisplayKit/examples/README.md new file mode 100644 index 0000000000..76c71a75d0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/README.md @@ -0,0 +1,232 @@ +# Sample projects + +## Building + +Run `pod install` in each sample project directory to set up their +dependencies. + +## Example Catalog + +### ASCollectionView [ObjC] + +![ASCollectionView Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASCollectionView.png) + +Featuring: +- ASCollectionView with header/footer supplementary node support +- ASCollectionView batch API +- ASDelegateProxy + +### ASDKgram [ObjC] + +![ASDKgram Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASDKgram.png) + +### ASDKLayoutTransition [ObjC] + +![ASDKLayoutTransition Example App](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASDKLayoutTransition.gif) + +### ASDKTube [ObjC] + +![ASDKTube Example App](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASDKTube.gif) + +### ASMapNode [ObjC] + +![ASMapNode Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASMapNode.png) + +### ASTableViewStressTest [ObjC] + +![ASTableViewStressTest Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASTableViewStressTest.png) + +### ASViewController [ObjC] + +![ASViewController Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASViewController.png) + +Featuring: +- ASViewController +- ASTableView +- ASMultiplexImageNode +- ASLayoutSpec + +### AsyncDisplayKitOverview [ObjC] + +![AsyncDisplayKitOverview Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/AsyncDisplayKitOverview.png) + +### BackgroundPropertySetting [Swift] + +![BackgroundPropertySetting Example App gif](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/BackgroundPropertySetting.gif) + +Featuring: +- ASDK Swift compatibility +- ASViewController +- ASCollectionView +- thread affinity +- ASLayoutSpec + +### CarthageBuildTest +### CatDealsCollectionView [ObjC] + +![CatDealsCollectionView Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/CatDealsCollectionView.png) + +Featuring: +- ASCollectionView +- ASRangeTuningParameters +- Placeholder Images +- ASLayoutSpec + +### CollectionViewWithViewControllerCells [ObjC] + +![CollectionViewWithViewControllerCells Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/CollectionViewWithViewControllerCells.png) + +Featuring: +- custom collection view layout +- ASLayoutSpec +- ASMultiplexImageNode + +### CustomCollectionView [ObjC+Swift] + +![CustomCollectionView Example App gif](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/CustomCollectionView.git) + +Featuring: +- custom collection view layout +- ASCollectionView with sections + +### EditableText [ObjC] + +![EditableText Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/EditableText.png) + +Featuring: +- ASEditableTextNode + +### HorizontalwithinVerticalScrolling [ObjC] + +![HorizontalwithinVerticalScrolling Example App gif](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/HorizontalwithinVerticalScrolling.gif) + +Featuring: +- UIViewController with ASTableView +- ASCollectionView +- ASCellNode + +### Kittens [ObjC] + +![Kittens Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/Kittens.png) + +Featuring: +- UIViewController with ASTableView +- ASCellNodes with ASNetworkImageNode and ASTextNode + +### LayoutSpecPlayground [ObjC] + +![LayoutSpecPlayground Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/LayoutSpecPlayground.png) + +### Multiplex [ObjC] + +![Multiplex Example App](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/Multiplex.gif) + +Featuring: +- ASMultiplexImageNode (with artificial delay inserted) +- ASLayoutSpec + +### PagerNode [ObjC] + +![PagerNode Example App](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/PagerNode.gif) + +Featuring: +- ASPagerNode + +### Placeholders [ObjC] + +Featuring: +- ASDisplayNodes now have an overidable method -placeholderImage that lets you provide a custom UIImage to display while a node is displaying asyncronously. The default implementation of this method returns nil and thus does nothing. A provided example project also demonstrates using the placeholder API. + +### SocialAppLayout [ObjC] + +![SocialAppLayout Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/SocialAppLayout.png) + +Featuring: +- ASLayoutSpec +- UIViewController with ASTableView + +### Swift [Swift] + +![Swift Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/Swift.png) + +Featuring: +- ASViewController with ASTableNode + +### SynchronousConcurrency [ObjC] + +![SynchronousConcurrency Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/SynchronousConcurrency.png) + +Implementation of Synchronous Concurrency features for AsyncDisplayKit 2.0 + +This provides internal features on _ASAsyncTransaction and ASDisplayNode to facilitate +implementing public API that allows clients to choose if they would prefer to block +on the completion of unfinished rendering, rather than allow a placeholder state to +become visible. + +The internal features are: +-[_ASAsyncTransaction waitUntilComplete] +-[ASDisplayNode recursivelyEnsureDisplay] + +Also provided are two such implementations: +-[ASCellNode setNeverShowPlaceholders:], which integrates with both Tables and Collections +-[ASViewController setNeverShowPlaceholders:], which should work with Nav and Tab controllers. + +Lastly, on ASDisplayNode, a new property .shouldBypassEnsureDisplay allows individual node types +to exempt themselves from blocking the main thread on their display. + +By implementing the feature at the ASCellNode level rather than ASTableView & ASCollectionView, +developers can retain fine-grained control on display characteristics. For example, certain +cell types may be appropriate to display to the user with placeholders, whereas others may not. + +### SynchronousKittens [ObjC] + +### VerticalWithinHorizontalScrolling [ObjC] + +![VerticalWithinHorizontalScrolling Example App](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/VerticalWithinHorizontalScrolling.gif) + +Features: +- UIViewController containing ASPagerNode containing ASTableNodes + +### Videos [ObjC] + +![VideoTableView Example App gif](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/Videos.gif) + +Featuring: +- ASVideoNode + +### VideoTableView [ObjC] + +![VideoTableView Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/VideoTableView.png) + +Featuring: +- ASVideoNode +- ASTableView +- ASCellNode + +### LayoutSpecExamples [ObjC] + +![Layout Spec Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png) + +Featuring: +- ASStackLayoutSpec +- ASInsetLayoutSpec +- ASOverlayLayoutSpec +- ASAbsoluteLayoutSpec +- ASBackgroundLayoutSpec +- ASCornerLayoutSpec + +There is an associated swift version app: LayoutSpecExamples-Swift with same logic implementation. + +## License + + This file provided by Facebook is for non-commercial testing and evaluation + purposes only. Facebook reserves all rights not expressly granted. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Podfile b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..3ad0bf8772 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,485 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3EEA4EE91BECC4A1008A7F35 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4EE81BECC4A1008A7F35 /* main.m */; }; + 3EEA4EEC1BECC4A1008A7F35 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4EEB1BECC4A1008A7F35 /* AppDelegate.m */; }; + 3EEA4EEF1BECC4A1008A7F35 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4EEE1BECC4A1008A7F35 /* ViewController.m */; }; + 3EEA4EF41BECC4A1008A7F35 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4EF31BECC4A1008A7F35 /* Assets.xcassets */; }; + 3EEA4F011BECC4E8008A7F35 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4EFE1BECC4E8008A7F35 /* Default-568h@2x.png */; }; + 3EEA4F021BECC4E8008A7F35 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4EFF1BECC4E8008A7F35 /* Default-667h@2x.png */; }; + 3EEA4F031BECC4E8008A7F35 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F001BECC4E8008A7F35 /* Default-736h@3x.png */; }; + 3EEA4F061BECC6C9008A7F35 /* Post.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F051BECC6C9008A7F35 /* Post.m */; }; + 3EEA4F091BECC855008A7F35 /* PostNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F081BECC855008A7F35 /* PostNode.m */; }; + 3EEA4F0C1BECCA0A008A7F35 /* TextStyles.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F0B1BECCA0A008A7F35 /* TextStyles.m */; }; + 3EEA4F141BECDCD6008A7F35 /* icon_android.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F0E1BECDCD6008A7F35 /* icon_android.png */; }; + 3EEA4F151BECDCD6008A7F35 /* icon_android@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F0F1BECDCD6008A7F35 /* icon_android@2x.png */; }; + 3EEA4F161BECDCD6008A7F35 /* icon_android@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F101BECDCD6008A7F35 /* icon_android@3x.png */; }; + 3EEA4F171BECDCD6008A7F35 /* icon_ios.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F111BECDCD6008A7F35 /* icon_ios.png */; }; + 3EEA4F181BECDCD6008A7F35 /* icon_ios@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F121BECDCD6008A7F35 /* icon_ios@2x.png */; }; + 3EEA4F191BECDCD6008A7F35 /* icon_ios@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F131BECDCD6008A7F35 /* icon_ios@3x.png */; }; + 3EEA4F1D1BECE358008A7F35 /* LikesNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F1C1BECE358008A7F35 /* LikesNode.m */; }; + 3EEA4F2A1BECE440008A7F35 /* icon_liked.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F1E1BECE440008A7F35 /* icon_liked.png */; }; + 3EEA4F2B1BECE440008A7F35 /* icon_liked@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F1F1BECE440008A7F35 /* icon_liked@2x.png */; }; + 3EEA4F2C1BECE440008A7F35 /* icon_liked@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F201BECE440008A7F35 /* icon_liked@3x.png */; }; + 3EEA4F301BECE440008A7F35 /* icon_like@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F241BECE440008A7F35 /* icon_like@3x.png */; }; + 3EEA4F311BECE440008A7F35 /* icon_like@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F251BECE440008A7F35 /* icon_like@2x.png */; }; + 3EEA4F321BECE440008A7F35 /* icon_like.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F261BECE440008A7F35 /* icon_like.png */; }; + 3EEA4F331BECE440008A7F35 /* icon_comment@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F271BECE440008A7F35 /* icon_comment@3x.png */; }; + 3EEA4F341BECE440008A7F35 /* icon_comment@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F281BECE440008A7F35 /* icon_comment@2x.png */; }; + 3EEA4F351BECE440008A7F35 /* icon_comment.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F291BECE440008A7F35 /* icon_comment.png */; }; + 3EEA4F381BECE775008A7F35 /* CommentsNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F371BECE775008A7F35 /* CommentsNode.m */; }; + 3EEA4F3C1BECE99F008A7F35 /* icon_more.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F391BECE99F008A7F35 /* icon_more.png */; }; + 3EEA4F3D1BECE99F008A7F35 /* icon_more@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F3A1BECE99F008A7F35 /* icon_more@2x.png */; }; + 3EEA4F3E1BECE99F008A7F35 /* icon_more@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F3B1BECE99F008A7F35 /* icon_more@3x.png */; }; + 93964C9FCC28D92625106430 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 193CE60FE6429EFEBF6EA52B /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 193CE60FE6429EFEBF6EA52B /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3EEA4EE41BECC4A1008A7F35 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3EEA4EE81BECC4A1008A7F35 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 3EEA4EEA1BECC4A1008A7F35 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 3EEA4EEB1BECC4A1008A7F35 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 3EEA4EED1BECC4A1008A7F35 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 3EEA4EEE1BECC4A1008A7F35 /* ViewController.m */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; tabWidth = 2; }; + 3EEA4EF31BECC4A1008A7F35 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3EEA4EF81BECC4A1008A7F35 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3EEA4EFE1BECC4E8008A7F35 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 3EEA4EFF1BECC4E8008A7F35 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = ""; }; + 3EEA4F001BECC4E8008A7F35 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = ""; }; + 3EEA4F041BECC6C9008A7F35 /* Post.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Post.h; sourceTree = ""; }; + 3EEA4F051BECC6C9008A7F35 /* Post.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Post.m; sourceTree = ""; }; + 3EEA4F071BECC855008A7F35 /* PostNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostNode.h; sourceTree = ""; }; + 3EEA4F081BECC855008A7F35 /* PostNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostNode.m; sourceTree = ""; }; + 3EEA4F0A1BECCA0A008A7F35 /* TextStyles.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextStyles.h; sourceTree = ""; }; + 3EEA4F0B1BECCA0A008A7F35 /* TextStyles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextStyles.m; sourceTree = ""; }; + 3EEA4F0E1BECDCD6008A7F35 /* icon_android.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_android.png; sourceTree = ""; }; + 3EEA4F0F1BECDCD6008A7F35 /* icon_android@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_android@2x.png"; sourceTree = ""; }; + 3EEA4F101BECDCD6008A7F35 /* icon_android@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_android@3x.png"; sourceTree = ""; }; + 3EEA4F111BECDCD6008A7F35 /* icon_ios.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_ios.png; sourceTree = ""; }; + 3EEA4F121BECDCD6008A7F35 /* icon_ios@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_ios@2x.png"; sourceTree = ""; }; + 3EEA4F131BECDCD6008A7F35 /* icon_ios@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_ios@3x.png"; sourceTree = ""; }; + 3EEA4F1B1BECE358008A7F35 /* LikesNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LikesNode.h; sourceTree = ""; }; + 3EEA4F1C1BECE358008A7F35 /* LikesNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LikesNode.m; sourceTree = ""; }; + 3EEA4F1E1BECE440008A7F35 /* icon_liked.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_liked.png; sourceTree = ""; }; + 3EEA4F1F1BECE440008A7F35 /* icon_liked@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_liked@2x.png"; sourceTree = ""; }; + 3EEA4F201BECE440008A7F35 /* icon_liked@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_liked@3x.png"; sourceTree = ""; }; + 3EEA4F241BECE440008A7F35 /* icon_like@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_like@3x.png"; sourceTree = ""; }; + 3EEA4F251BECE440008A7F35 /* icon_like@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_like@2x.png"; sourceTree = ""; }; + 3EEA4F261BECE440008A7F35 /* icon_like.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_like.png; sourceTree = ""; }; + 3EEA4F271BECE440008A7F35 /* icon_comment@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_comment@3x.png"; sourceTree = ""; }; + 3EEA4F281BECE440008A7F35 /* icon_comment@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_comment@2x.png"; sourceTree = ""; }; + 3EEA4F291BECE440008A7F35 /* icon_comment.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_comment.png; sourceTree = ""; }; + 3EEA4F361BECE775008A7F35 /* CommentsNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CommentsNode.h; sourceTree = ""; }; + 3EEA4F371BECE775008A7F35 /* CommentsNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommentsNode.m; sourceTree = ""; }; + 3EEA4F391BECE99F008A7F35 /* icon_more.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_more.png; sourceTree = ""; }; + 3EEA4F3A1BECE99F008A7F35 /* icon_more@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_more@2x.png"; sourceTree = ""; }; + 3EEA4F3B1BECE99F008A7F35 /* icon_more@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_more@3x.png"; sourceTree = ""; }; + CC6F2ABE8383FAB21802C734 /* 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 = ""; }; + FCCC1AD413FCA8603156ED15 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3EEA4EE11BECC4A1008A7F35 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 93964C9FCC28D92625106430 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3EEA4EDB1BECC4A1008A7F35 = { + isa = PBXGroup; + children = ( + 3EEA4EE61BECC4A1008A7F35 /* Sample */, + 3EEA4EE51BECC4A1008A7F35 /* Products */, + 842ADAFE88475D19B24183AC /* Pods */, + EED34FA6D8171DF44757C852 /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 3EEA4EE51BECC4A1008A7F35 /* Products */ = { + isa = PBXGroup; + children = ( + 3EEA4EE41BECC4A1008A7F35 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 3EEA4EE61BECC4A1008A7F35 /* Sample */ = { + isa = PBXGroup; + children = ( + 3EEA4F0D1BECDCA6008A7F35 /* Images */, + 3EEA4EEA1BECC4A1008A7F35 /* AppDelegate.h */, + 3EEA4EEB1BECC4A1008A7F35 /* AppDelegate.m */, + 3EEA4EED1BECC4A1008A7F35 /* ViewController.h */, + 3EEA4EEE1BECC4A1008A7F35 /* ViewController.m */, + 3EEA4EF31BECC4A1008A7F35 /* Assets.xcassets */, + 3EEA4EF81BECC4A1008A7F35 /* Info.plist */, + 3EEA4EE71BECC4A1008A7F35 /* Supporting Files */, + 3EEA4F041BECC6C9008A7F35 /* Post.h */, + 3EEA4F051BECC6C9008A7F35 /* Post.m */, + 3EEA4F071BECC855008A7F35 /* PostNode.h */, + 3EEA4F081BECC855008A7F35 /* PostNode.m */, + 3EEA4F0A1BECCA0A008A7F35 /* TextStyles.h */, + 3EEA4F0B1BECCA0A008A7F35 /* TextStyles.m */, + 3EEA4F1B1BECE358008A7F35 /* LikesNode.h */, + 3EEA4F1C1BECE358008A7F35 /* LikesNode.m */, + 3EEA4F361BECE775008A7F35 /* CommentsNode.h */, + 3EEA4F371BECE775008A7F35 /* CommentsNode.m */, + ); + path = Sample; + sourceTree = ""; + }; + 3EEA4EE71BECC4A1008A7F35 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 3EEA4EFE1BECC4E8008A7F35 /* Default-568h@2x.png */, + 3EEA4EFF1BECC4E8008A7F35 /* Default-667h@2x.png */, + 3EEA4F001BECC4E8008A7F35 /* Default-736h@3x.png */, + 3EEA4EE81BECC4A1008A7F35 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 3EEA4F0D1BECDCA6008A7F35 /* Images */ = { + isa = PBXGroup; + children = ( + 3EEA4F391BECE99F008A7F35 /* icon_more.png */, + 3EEA4F3A1BECE99F008A7F35 /* icon_more@2x.png */, + 3EEA4F3B1BECE99F008A7F35 /* icon_more@3x.png */, + 3EEA4F1E1BECE440008A7F35 /* icon_liked.png */, + 3EEA4F1F1BECE440008A7F35 /* icon_liked@2x.png */, + 3EEA4F201BECE440008A7F35 /* icon_liked@3x.png */, + 3EEA4F241BECE440008A7F35 /* icon_like@3x.png */, + 3EEA4F251BECE440008A7F35 /* icon_like@2x.png */, + 3EEA4F261BECE440008A7F35 /* icon_like.png */, + 3EEA4F271BECE440008A7F35 /* icon_comment@3x.png */, + 3EEA4F281BECE440008A7F35 /* icon_comment@2x.png */, + 3EEA4F291BECE440008A7F35 /* icon_comment.png */, + 3EEA4F0E1BECDCD6008A7F35 /* icon_android.png */, + 3EEA4F0F1BECDCD6008A7F35 /* icon_android@2x.png */, + 3EEA4F101BECDCD6008A7F35 /* icon_android@3x.png */, + 3EEA4F111BECDCD6008A7F35 /* icon_ios.png */, + 3EEA4F121BECDCD6008A7F35 /* icon_ios@2x.png */, + 3EEA4F131BECDCD6008A7F35 /* icon_ios@3x.png */, + ); + name = Images; + sourceTree = ""; + }; + 842ADAFE88475D19B24183AC /* Pods */ = { + isa = PBXGroup; + children = ( + CC6F2ABE8383FAB21802C734 /* Pods-Sample.debug.xcconfig */, + FCCC1AD413FCA8603156ED15 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + EED34FA6D8171DF44757C852 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 193CE60FE6429EFEBF6EA52B /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3EEA4EE31BECC4A1008A7F35 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3EEA4EFB1BECC4A1008A7F35 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + B5BD9E5609B2CB179EEE0CF4 /* [CP] Check Pods Manifest.lock */, + 3EEA4EE01BECC4A1008A7F35 /* Sources */, + 3EEA4EE11BECC4A1008A7F35 /* Frameworks */, + 3EEA4EE21BECC4A1008A7F35 /* Resources */, + 21F2C1D9B53F9468EAF1653F /* [CP] Copy Pods Resources */, + 852437589F1D53B9483A75DF /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 3EEA4EE41BECC4A1008A7F35 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3EEA4EDC1BECC4A1008A7F35 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0710; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 3EEA4EE31BECC4A1008A7F35 = { + CreatedOnToolsVersion = 7.1; + }; + }; + }; + buildConfigurationList = 3EEA4EDF1BECC4A1008A7F35 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3EEA4EDB1BECC4A1008A7F35; + productRefGroup = 3EEA4EE51BECC4A1008A7F35 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3EEA4EE31BECC4A1008A7F35 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3EEA4EE21BECC4A1008A7F35 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EEA4F2A1BECE440008A7F35 /* icon_liked.png in Resources */, + 3EEA4F181BECDCD6008A7F35 /* icon_ios@2x.png in Resources */, + 3EEA4F311BECE440008A7F35 /* icon_like@2x.png in Resources */, + 3EEA4F3D1BECE99F008A7F35 /* icon_more@2x.png in Resources */, + 3EEA4F141BECDCD6008A7F35 /* icon_android.png in Resources */, + 3EEA4F3E1BECE99F008A7F35 /* icon_more@3x.png in Resources */, + 3EEA4F2B1BECE440008A7F35 /* icon_liked@2x.png in Resources */, + 3EEA4F351BECE440008A7F35 /* icon_comment.png in Resources */, + 3EEA4EF41BECC4A1008A7F35 /* Assets.xcassets in Resources */, + 3EEA4F171BECDCD6008A7F35 /* icon_ios.png in Resources */, + 3EEA4F021BECC4E8008A7F35 /* Default-667h@2x.png in Resources */, + 3EEA4F161BECDCD6008A7F35 /* icon_android@3x.png in Resources */, + 3EEA4F191BECDCD6008A7F35 /* icon_ios@3x.png in Resources */, + 3EEA4F331BECE440008A7F35 /* icon_comment@3x.png in Resources */, + 3EEA4F341BECE440008A7F35 /* icon_comment@2x.png in Resources */, + 3EEA4F321BECE440008A7F35 /* icon_like.png in Resources */, + 3EEA4F151BECDCD6008A7F35 /* icon_android@2x.png in Resources */, + 3EEA4F301BECE440008A7F35 /* icon_like@3x.png in Resources */, + 3EEA4F2C1BECE440008A7F35 /* icon_liked@3x.png in Resources */, + 3EEA4F011BECC4E8008A7F35 /* Default-568h@2x.png in Resources */, + 3EEA4F031BECC4E8008A7F35 /* Default-736h@3x.png in Resources */, + 3EEA4F3C1BECE99F008A7F35 /* icon_more.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 21F2C1D9B53F9468EAF1653F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 852437589F1D53B9483A75DF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B5BD9E5609B2CB179EEE0CF4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3EEA4EE01BECC4A1008A7F35 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EEA4EEF1BECC4A1008A7F35 /* ViewController.m in Sources */, + 3EEA4EEC1BECC4A1008A7F35 /* AppDelegate.m in Sources */, + 3EEA4EE91BECC4A1008A7F35 /* main.m in Sources */, + 3EEA4F061BECC6C9008A7F35 /* Post.m in Sources */, + 3EEA4F0C1BECCA0A008A7F35 /* TextStyles.m in Sources */, + 3EEA4F381BECE775008A7F35 /* CommentsNode.m in Sources */, + 3EEA4F091BECC855008A7F35 /* PostNode.m in Sources */, + 3EEA4F1D1BECE358008A7F35 /* LikesNode.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3EEA4EF91BECC4A1008A7F35 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 3EEA4EFA1BECC4A1008A7F35 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3EEA4EFC1BECC4A1008A7F35 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CC6F2ABE8383FAB21802C734 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3EEA4EFD1BECC4A1008A7F35 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FCCC1AD413FCA8603156ED15 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3EEA4EDF1BECC4A1008A7F35 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3EEA4EF91BECC4A1008A7F35 /* Debug */, + 3EEA4EFA1BECC4A1008A7F35 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3EEA4EFB1BECC4A1008A7F35 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3EEA4EFC1BECC4A1008A7F35 /* Debug */, + 3EEA4EFD1BECC4A1008A7F35 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3EEA4EDC1BECC4A1008A7F35 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..64285ed126 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/AppDelegate.m new file mode 100644 index 0000000000..73663a6919 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/AppDelegate.m @@ -0,0 +1,24 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/CommentsNode.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/CommentsNode.h new file mode 100644 index 0000000000..422460fe9e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/CommentsNode.h @@ -0,0 +1,16 @@ +// +// CommentsNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface CommentsNode : ASControlNode + +- (instancetype)initWithCommentsCount:(NSInteger)comentsCount; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/CommentsNode.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/CommentsNode.m new file mode 100644 index 0000000000..96ba688881 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/CommentsNode.m @@ -0,0 +1,62 @@ +// +// CommentsNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "CommentsNode.h" +#import "TextStyles.h" + +@interface CommentsNode () +@property (nonatomic, strong) ASImageNode *iconNode; +@property (nonatomic, strong) ASTextNode *countNode; +@property (nonatomic, assign) NSInteger commentsCount; +@end + +@implementation CommentsNode + +- (instancetype)initWithCommentsCount:(NSInteger)comentsCount +{ + self = [super init]; + if (self) { + _commentsCount = comentsCount; + + _iconNode = [[ASImageNode alloc] init]; + _iconNode.image = [UIImage imageNamed:@"icon_comment.png"]; + [self addSubnode:_iconNode]; + + _countNode = [[ASTextNode alloc] init]; + if (_commentsCount > 0) { + _countNode.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%zd", _commentsCount] attributes:[TextStyles cellControlStyle]]; + } + [self addSubnode:_countNode]; + + // make it tappable easily + self.hitTestSlop = UIEdgeInsetsMake(-10, -10, -10, -10); + } + + return self; + +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *mainStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:6.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_iconNode, _countNode]]; + + // Adjust size + mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0); + mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0); + + return mainStack; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Info.plist new file mode 100644 index 0000000000..ed1c9acf9b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/LikesNode.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/LikesNode.h new file mode 100644 index 0000000000..1dbbc191e1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/LikesNode.h @@ -0,0 +1,16 @@ +// +// LikesNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface LikesNode : ASControlNode + +- (instancetype)initWithLikesCount:(NSInteger)likesCount; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/LikesNode.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/LikesNode.m new file mode 100644 index 0000000000..cd5ade1db4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/LikesNode.m @@ -0,0 +1,75 @@ +// +// LikesNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "LikesNode.h" +#import "TextStyles.h" + +@interface LikesNode () +@property (nonatomic, strong) ASImageNode *iconNode; +@property (nonatomic, strong) ASTextNode *countNode; +@property (nonatomic, assign) NSInteger likesCount; +@property (nonatomic, assign) BOOL liked; +@end + +@implementation LikesNode + +- (instancetype)initWithLikesCount:(NSInteger)likesCount +{ + self = [super init]; + if (self) { + _likesCount = likesCount; + _liked = (_likesCount > 0) ? [LikesNode getYesOrNo] : NO; + + _iconNode = [[ASImageNode alloc] init]; + _iconNode.image = (_liked) ? [UIImage imageNamed:@"icon_liked.png"] : [UIImage imageNamed:@"icon_like.png"]; + [self addSubnode:_iconNode]; + + _countNode = [[ASTextNode alloc] init]; + if (_likesCount > 0) { + + NSDictionary *attributes = _liked ? [TextStyles cellControlColoredStyle] : [TextStyles cellControlStyle]; + _countNode.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%ld", (long)_likesCount] attributes:attributes]; + + } + [self addSubnode:_countNode]; + + // make it tappable easily + self.hitTestSlop = UIEdgeInsetsMake(-10, -10, -10, -10); + } + + return self; + +} + ++ (BOOL)getYesOrNo +{ + int tmp = (arc4random() % 30)+1; + if (tmp % 5 == 0) { + return YES; + } + return NO; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *mainStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:6.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_iconNode, _countNode]]; + + mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0); + mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0); + + return mainStack; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Post.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Post.h new file mode 100644 index 0000000000..c8259237b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Post.h @@ -0,0 +1,25 @@ +// +// Post.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface Post : NSObject + +@property (nonatomic, copy) NSString *username; +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *photo; +@property (nonatomic, copy) NSString *post; +@property (nonatomic, copy) NSString *time; +@property (nonatomic, copy) NSString *media; +@property (nonatomic, assign) NSInteger via; + +@property (nonatomic, assign) NSInteger likes; +@property (nonatomic, assign) NSInteger comments; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Post.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Post.m new file mode 100644 index 0000000000..fc61c5bf82 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/Post.m @@ -0,0 +1,13 @@ +// +// Post.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "Post.h" + +@implementation Post +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/PostNode.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/PostNode.h new file mode 100644 index 0000000000..b558158e31 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/PostNode.h @@ -0,0 +1,18 @@ +// +// PostNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class Post; + +@interface PostNode : ASCellNode + +- (instancetype)initWithPost:(Post *)post; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/PostNode.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/PostNode.m new file mode 100644 index 0000000000..defafc0061 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/PostNode.m @@ -0,0 +1,337 @@ +// +// PostNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PostNode.h" +#import "Post.h" +#import "TextStyles.h" +#import "LikesNode.h" +#import "CommentsNode.h" + +#define PostNodeDividerColor [UIColor lightGrayColor] + +@interface PostNode() + +@property (strong, nonatomic) Post *post; +@property (strong, nonatomic) ASDisplayNode *divider; +@property (strong, nonatomic) ASTextNode *nameNode; +@property (strong, nonatomic) ASTextNode *usernameNode; +@property (strong, nonatomic) ASTextNode *timeNode; +@property (strong, nonatomic) ASTextNode *postNode; +@property (strong, nonatomic) ASImageNode *viaNode; +@property (strong, nonatomic) ASNetworkImageNode *avatarNode; +@property (strong, nonatomic) ASNetworkImageNode *mediaNode; +@property (strong, nonatomic) LikesNode *likesNode; +@property (strong, nonatomic) CommentsNode *commentsNode; +@property (strong, nonatomic) ASImageNode *optionsNode; + +@end + +@implementation PostNode + +#pragma mark - Lifecycle + +- (instancetype)initWithPost:(Post *)post +{ + self = [super init]; + if (self) { + _post = post; + + self.selectionStyle = UITableViewCellSelectionStyleNone; + + // Name node + _nameNode = [[ASTextNode alloc] init]; + _nameNode.attributedText = [[NSAttributedString alloc] initWithString:_post.name attributes:[TextStyles nameStyle]]; + _nameNode.maximumNumberOfLines = 1; + [self addSubnode:_nameNode]; + + // Username node + _usernameNode = [[ASTextNode alloc] init]; + _usernameNode.attributedText = [[NSAttributedString alloc] initWithString:_post.username attributes:[TextStyles usernameStyle]]; + _usernameNode.style.flexShrink = 1.0; //if name and username don't fit to cell width, allow username shrink + _usernameNode.truncationMode = NSLineBreakByTruncatingTail; + _usernameNode.maximumNumberOfLines = 1; + [self addSubnode:_usernameNode]; + + // Time node + _timeNode = [[ASTextNode alloc] init]; + _timeNode.attributedText = [[NSAttributedString alloc] initWithString:_post.time attributes:[TextStyles timeStyle]]; + [self addSubnode:_timeNode]; + + // Post node + _postNode = [[ASTextNode alloc] init]; + + // Processing URLs in post + NSString *kLinkAttributeName = @"TextLinkAttributeName"; + + if (![_post.post isEqualToString:@""]) { + + NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:_post.post attributes:[TextStyles postStyle]]; + + NSDataDetector *urlDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil]; + + [urlDetector enumerateMatchesInString:attrString.string options:kNilOptions range:NSMakeRange(0, attrString.string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){ + + if (result.resultType == NSTextCheckingTypeLink) { + + NSMutableDictionary *linkAttributes = [[NSMutableDictionary alloc] initWithDictionary:[TextStyles postLinkStyle]]; + linkAttributes[kLinkAttributeName] = [NSURL URLWithString:result.URL.absoluteString]; + + [attrString addAttributes:linkAttributes range:result.range]; + + } + + }]; + + // Configure node to support tappable links + _postNode.delegate = self; + _postNode.userInteractionEnabled = YES; + _postNode.linkAttributeNames = @[ kLinkAttributeName ]; + _postNode.attributedText = attrString; + _postNode.passthroughNonlinkTouches = YES; // passes touches through when they aren't on a link + + } + + [self addSubnode:_postNode]; + + + // Media + if (![_post.media isEqualToString:@""]) { + + _mediaNode = [[ASNetworkImageNode alloc] init]; + _mediaNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _mediaNode.cornerRadius = 4.0; + _mediaNode.URL = [NSURL URLWithString:_post.media]; + _mediaNode.delegate = self; + _mediaNode.imageModificationBlock = ^UIImage *(UIImage *image) { + + UIImage *modifiedImage; + CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height); + + UIGraphicsBeginImageContextWithOptions(image.size, false, [[UIScreen mainScreen] scale]); + + [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip]; + [image drawInRect:rect]; + modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); + + return modifiedImage; + + }; + [self addSubnode:_mediaNode]; + } + + // User pic + _avatarNode = [[ASNetworkImageNode alloc] init]; + _avatarNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _avatarNode.style.width = ASDimensionMakeWithPoints(44); + _avatarNode.style.height = ASDimensionMakeWithPoints(44); + _avatarNode.cornerRadius = 22.0; + _avatarNode.URL = [NSURL URLWithString:_post.photo]; + _avatarNode.imageModificationBlock = ^UIImage *(UIImage *image) { + + UIImage *modifiedImage; + CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height); + + UIGraphicsBeginImageContextWithOptions(image.size, false, [[UIScreen mainScreen] scale]); + + [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:44.0] addClip]; + [image drawInRect:rect]; + modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); + + return modifiedImage; + + }; + [self addSubnode:_avatarNode]; + + // Hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + [self updateDividerColor]; + [self addSubnode:_divider]; + + // Via + if (_post.via != 0) { + _viaNode = [[ASImageNode alloc] init]; + _viaNode.image = (_post.via == 1) ? [UIImage imageNamed:@"icon_ios.png"] : [UIImage imageNamed:@"icon_android.png"]; + [self addSubnode:_viaNode]; + } + + // Bottom controls + _likesNode = [[LikesNode alloc] initWithLikesCount:_post.likes]; + [self addSubnode:_likesNode]; + + _commentsNode = [[CommentsNode alloc] initWithCommentsCount:_post.comments]; + [self addSubnode:_commentsNode]; + + _optionsNode = [[ASImageNode alloc] init]; + _optionsNode.image = [UIImage imageNamed:@"icon_more"]; + [self addSubnode:_optionsNode]; + + for (ASDisplayNode *node in self.subnodes) { + // ASTextNode with embedded links doesn't support layer backing + if (node.supportsLayerBacking) { + node.layerBacked = YES; + } + } + } + return self; +} + +- (void)updateDividerColor +{ + /* + * UITableViewCell traverses through all its descendant views and adjusts their background color accordingly + * either to [UIColor clearColor], although potentially it could use the same color as the selection highlight itself. + * After selection, the same trick is performed again in reverse, putting all the backgrounds back as they used to be. + * But in our case, we don't want to have the background color disappearing so we reset it after highlighting or + * selection is done. + */ + _divider.backgroundColor = PostNodeDividerColor; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // Flexible spacer between username and time + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.style.flexGrow = 1.0; + + // Horizontal stack for name, username, via icon and time + NSMutableArray *layoutSpecChildren = [@[_nameNode, _usernameNode, spacer] mutableCopy]; + if (_post.via != 0) { + [layoutSpecChildren addObject:_viaNode]; + } + [layoutSpecChildren addObject:_timeNode]; + + ASStackLayoutSpec *nameStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:5.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:layoutSpecChildren]; + nameStack.style.alignSelf = ASStackLayoutAlignSelfStretch; + + // bottom controls horizontal stack + ASStackLayoutSpec *controlsStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_likesNode, _commentsNode, _optionsNode]]; + + // Add more gaps for control line + controlsStack.style.spacingAfter = 3.0; + controlsStack.style.spacingBefore = 3.0; + + NSMutableArray *mainStackContent = [[NSMutableArray alloc] init]; + [mainStackContent addObject:nameStack]; + [mainStackContent addObject:_postNode]; + + + if (![_post.media isEqualToString:@""]){ + + // Only add the media node if an image is present + if (_mediaNode.image != nil) { + ASRatioLayoutSpec *imagePlace = + [ASRatioLayoutSpec + ratioLayoutSpecWithRatio:0.5 + child:_mediaNode]; + imagePlace.style.spacingAfter = 3.0; + imagePlace.style.spacingBefore = 3.0; + + [mainStackContent addObject:imagePlace]; + } + } + [mainStackContent addObject:controlsStack]; + + // Vertical spec of cell main content + ASStackLayoutSpec *contentSpec = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:8.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:mainStackContent]; + contentSpec.style.flexShrink = 1.0; + + // Horizontal spec for avatar + ASStackLayoutSpec *avatarContentSpec = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:8.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[_avatarNode, contentSpec]]; + + return [ASInsetLayoutSpec + insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) + child:avatarContentSpec]; + +} + +- (void)layout +{ + [super layout]; + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} + +#pragma mark - ASCellNode + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + + [self updateDividerColor]; +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + + [self updateDividerColor]; +} + +#pragma mark - + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // Opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // The node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +#pragma mark - ASNetworkImageNodeDelegate methods. + +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image +{ + [self setNeedsLayout]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/TextStyles.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/TextStyles.h new file mode 100644 index 0000000000..2a975bdea3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/TextStyles.h @@ -0,0 +1,23 @@ +// +// TextStyles.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface TextStyles : NSObject + ++ (NSDictionary *)nameStyle; ++ (NSDictionary *)usernameStyle; ++ (NSDictionary *)timeStyle; ++ (NSDictionary *)postStyle; ++ (NSDictionary *)postLinkStyle; ++ (NSDictionary *)cellControlStyle; ++ (NSDictionary *)cellControlColoredStyle; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/TextStyles.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/TextStyles.m new file mode 100644 index 0000000000..ad7798c445 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/TextStyles.m @@ -0,0 +1,71 @@ +// +// TextStyles.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "TextStyles.h" + +@implementation TextStyles + ++ (NSDictionary *)nameStyle +{ + return @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor blackColor] + }; +} + ++ (NSDictionary *)usernameStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor lightGrayColor] + }; +} + ++ (NSDictionary *)timeStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor grayColor] + }; +} + ++ (NSDictionary *)postStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor blackColor] + }; +} + ++ (NSDictionary *)postLinkStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor colorWithRed:59.0/255.0 green:89.0/255.0 blue:152.0/255.0 alpha:1.0], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) + }; +} + ++ (NSDictionary *)cellControlStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor lightGrayColor] + }; +} + ++ (NSDictionary *)cellControlColoredStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor colorWithRed:59.0/255.0 green:89.0/255.0 blue:152.0/255.0 alpha:1.0] + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/ViewController.h new file mode 100644 index 0000000000..6416242247 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/ViewController.h @@ -0,0 +1,13 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/ViewController.m new file mode 100644 index 0000000000..efc9d06e4a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/ViewController.m @@ -0,0 +1,144 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import "Post.h" +#import "PostNode.h" + +#import +#import + +#include + +@interface ViewController () + +@property (nonatomic, strong) ASTableNode *tableNode; +@property (nonatomic, strong) NSMutableArray *socialAppDataSource; + +@end + +#pragma mark - Lifecycle + +@implementation ViewController + +- (instancetype)init +{ + _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + _tableNode.inverted = YES; + self = [super initWithNode:_tableNode]; + + + if (self) { + + _tableNode.delegate = self; + _tableNode.dataSource = self; + self.title = @"Timeline"; + + [self createSocialAppDataSource]; + } + + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + CGFloat inset = [self topBarsHeight]; + self.tableNode.view.contentInset = UIEdgeInsetsMake(-inset, 0, inset, 0); + self.tableNode.view.scrollIndicatorInsets = UIEdgeInsetsMake(-inset, 0, inset, 0); +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // SocialAppNode has its own separator + self.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; +} + +- (CGFloat)topBarsHeight +{ + // No need to adjust if the edge isn't available + if ((self.edgesForExtendedLayout & UIRectEdgeTop) == 0) { + return 0.0; + } + return CGRectGetHeight(self.navigationController.navigationBar.frame) + CGRectGetHeight([UIApplication sharedApplication].statusBarFrame); +} + + +#pragma mark - Data Model + +- (void)createSocialAppDataSource +{ + _socialAppDataSource = [[NSMutableArray alloc] init]; + + Post *newPost = [[Post alloc] init]; + newPost.name = @"Apple Guy"; + newPost.username = @"@appleguy"; + newPost.photo = @"https://avatars1.githubusercontent.com/u/565251?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; + newPost.time = @"3s"; + newPost.media = @""; + newPost.via = 0; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; + + newPost = [[Post alloc] init]; + newPost.name = @"Huy Nguyen"; + newPost.username = @"@nguyenhuy"; + newPost.photo = @"https://avatars2.githubusercontent.com/u/587874?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + newPost.time = @"1m"; + newPost.media = @""; + newPost.via = 1; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; + + newPost = [[Post alloc] init]; + newPost.name = @"Alex Long Name"; + newPost.username = @"@veryyyylongusername"; + newPost.photo = @"https://avatars1.githubusercontent.com/u/8086633?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + newPost.time = @"3:02"; + newPost.media = @"http://www.ngmag.ru/upload/iblock/f93/f9390efc34151456598077c1ba44a94d.jpg"; + newPost.via = 2; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; + + newPost = [[Post alloc] init]; + newPost.name = @"Vitaly Baev"; + newPost.username = @"@vitalybaev"; + newPost.photo = @"https://avatars0.githubusercontent.com/u/724423?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. https://github.com/facebook/AsyncDisplayKit Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + newPost.time = @"yesterday"; + newPost.media = @""; + newPost.via = 1; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; +} + +#pragma mark - ASTableNode + +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + Post *post = self.socialAppDataSource[indexPath.row]; + return ^{ + return [[PostNode alloc] initWithPost:post]; + }; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return self.socialAppDataSource.count; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android.png new file mode 100644 index 0000000000..6d30985339 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android@2x.png new file mode 100644 index 0000000000..c0dd2f5977 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android@3x.png new file mode 100644 index 0000000000..d3e83e9334 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_android@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment.png new file mode 100644 index 0000000000..59ccfe43c1 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment@2x.png new file mode 100644 index 0000000000..bedd0593c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment@3x.png new file mode 100644 index 0000000000..eb8a0d7660 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_comment@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios.png new file mode 100644 index 0000000000..0cd417d446 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios@2x.png new file mode 100644 index 0000000000..f73561fd09 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios@3x.png new file mode 100644 index 0000000000..35d2bb2b37 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_ios@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like.png new file mode 100644 index 0000000000..43110b9d59 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like@2x.png new file mode 100644 index 0000000000..1b535d748b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like@3x.png new file mode 100644 index 0000000000..8c80507335 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_like@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked.png new file mode 100644 index 0000000000..b1c1ade901 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked@2x.png new file mode 100644 index 0000000000..d9dc5988ea Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked@3x.png new file mode 100644 index 0000000000..00578ac63e Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_liked@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more.png new file mode 100644 index 0000000000..013126d291 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more@2x.png new file mode 100644 index 0000000000..3d183df436 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more@3x.png new file mode 100644 index 0000000000..d5f829ab11 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/icon_more@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/main.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout-Inverted/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Podfile b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..b187eec74a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,483 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3EEA4EE91BECC4A1008A7F35 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4EE81BECC4A1008A7F35 /* main.m */; }; + 3EEA4EEC1BECC4A1008A7F35 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4EEB1BECC4A1008A7F35 /* AppDelegate.m */; }; + 3EEA4EEF1BECC4A1008A7F35 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4EEE1BECC4A1008A7F35 /* ViewController.m */; }; + 3EEA4EF41BECC4A1008A7F35 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4EF31BECC4A1008A7F35 /* Assets.xcassets */; }; + 3EEA4F011BECC4E8008A7F35 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4EFE1BECC4E8008A7F35 /* Default-568h@2x.png */; }; + 3EEA4F021BECC4E8008A7F35 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4EFF1BECC4E8008A7F35 /* Default-667h@2x.png */; }; + 3EEA4F031BECC4E8008A7F35 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F001BECC4E8008A7F35 /* Default-736h@3x.png */; }; + 3EEA4F061BECC6C9008A7F35 /* Post.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F051BECC6C9008A7F35 /* Post.m */; }; + 3EEA4F091BECC855008A7F35 /* PostNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F081BECC855008A7F35 /* PostNode.m */; }; + 3EEA4F0C1BECCA0A008A7F35 /* TextStyles.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F0B1BECCA0A008A7F35 /* TextStyles.m */; }; + 3EEA4F141BECDCD6008A7F35 /* icon_android.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F0E1BECDCD6008A7F35 /* icon_android.png */; }; + 3EEA4F151BECDCD6008A7F35 /* icon_android@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F0F1BECDCD6008A7F35 /* icon_android@2x.png */; }; + 3EEA4F161BECDCD6008A7F35 /* icon_android@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F101BECDCD6008A7F35 /* icon_android@3x.png */; }; + 3EEA4F171BECDCD6008A7F35 /* icon_ios.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F111BECDCD6008A7F35 /* icon_ios.png */; }; + 3EEA4F181BECDCD6008A7F35 /* icon_ios@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F121BECDCD6008A7F35 /* icon_ios@2x.png */; }; + 3EEA4F191BECDCD6008A7F35 /* icon_ios@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F131BECDCD6008A7F35 /* icon_ios@3x.png */; }; + 3EEA4F1D1BECE358008A7F35 /* LikesNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F1C1BECE358008A7F35 /* LikesNode.m */; }; + 3EEA4F2A1BECE440008A7F35 /* icon_liked.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F1E1BECE440008A7F35 /* icon_liked.png */; }; + 3EEA4F2B1BECE440008A7F35 /* icon_liked@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F1F1BECE440008A7F35 /* icon_liked@2x.png */; }; + 3EEA4F2C1BECE440008A7F35 /* icon_liked@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F201BECE440008A7F35 /* icon_liked@3x.png */; }; + 3EEA4F301BECE440008A7F35 /* icon_like@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F241BECE440008A7F35 /* icon_like@3x.png */; }; + 3EEA4F311BECE440008A7F35 /* icon_like@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F251BECE440008A7F35 /* icon_like@2x.png */; }; + 3EEA4F321BECE440008A7F35 /* icon_like.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F261BECE440008A7F35 /* icon_like.png */; }; + 3EEA4F331BECE440008A7F35 /* icon_comment@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F271BECE440008A7F35 /* icon_comment@3x.png */; }; + 3EEA4F341BECE440008A7F35 /* icon_comment@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F281BECE440008A7F35 /* icon_comment@2x.png */; }; + 3EEA4F351BECE440008A7F35 /* icon_comment.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F291BECE440008A7F35 /* icon_comment.png */; }; + 3EEA4F381BECE775008A7F35 /* CommentsNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EEA4F371BECE775008A7F35 /* CommentsNode.m */; }; + 3EEA4F3C1BECE99F008A7F35 /* icon_more.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F391BECE99F008A7F35 /* icon_more.png */; }; + 3EEA4F3D1BECE99F008A7F35 /* icon_more@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F3A1BECE99F008A7F35 /* icon_more@2x.png */; }; + 3EEA4F3E1BECE99F008A7F35 /* icon_more@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3EEA4F3B1BECE99F008A7F35 /* icon_more@3x.png */; }; + 93964C9FCC28D92625106430 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 193CE60FE6429EFEBF6EA52B /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 193CE60FE6429EFEBF6EA52B /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3EEA4EE41BECC4A1008A7F35 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3EEA4EE81BECC4A1008A7F35 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 3EEA4EEA1BECC4A1008A7F35 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 3EEA4EEB1BECC4A1008A7F35 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 3EEA4EED1BECC4A1008A7F35 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 3EEA4EEE1BECC4A1008A7F35 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 3EEA4EF31BECC4A1008A7F35 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3EEA4EF81BECC4A1008A7F35 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3EEA4EFE1BECC4E8008A7F35 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 3EEA4EFF1BECC4E8008A7F35 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = ""; }; + 3EEA4F001BECC4E8008A7F35 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = ""; }; + 3EEA4F041BECC6C9008A7F35 /* Post.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Post.h; sourceTree = ""; }; + 3EEA4F051BECC6C9008A7F35 /* Post.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Post.m; sourceTree = ""; }; + 3EEA4F071BECC855008A7F35 /* PostNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostNode.h; sourceTree = ""; }; + 3EEA4F081BECC855008A7F35 /* PostNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostNode.m; sourceTree = ""; }; + 3EEA4F0A1BECCA0A008A7F35 /* TextStyles.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextStyles.h; sourceTree = ""; }; + 3EEA4F0B1BECCA0A008A7F35 /* TextStyles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextStyles.m; sourceTree = ""; }; + 3EEA4F0E1BECDCD6008A7F35 /* icon_android.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_android.png; sourceTree = ""; }; + 3EEA4F0F1BECDCD6008A7F35 /* icon_android@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_android@2x.png"; sourceTree = ""; }; + 3EEA4F101BECDCD6008A7F35 /* icon_android@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_android@3x.png"; sourceTree = ""; }; + 3EEA4F111BECDCD6008A7F35 /* icon_ios.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_ios.png; sourceTree = ""; }; + 3EEA4F121BECDCD6008A7F35 /* icon_ios@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_ios@2x.png"; sourceTree = ""; }; + 3EEA4F131BECDCD6008A7F35 /* icon_ios@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_ios@3x.png"; sourceTree = ""; }; + 3EEA4F1B1BECE358008A7F35 /* LikesNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LikesNode.h; sourceTree = ""; }; + 3EEA4F1C1BECE358008A7F35 /* LikesNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LikesNode.m; sourceTree = ""; }; + 3EEA4F1E1BECE440008A7F35 /* icon_liked.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_liked.png; sourceTree = ""; }; + 3EEA4F1F1BECE440008A7F35 /* icon_liked@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_liked@2x.png"; sourceTree = ""; }; + 3EEA4F201BECE440008A7F35 /* icon_liked@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_liked@3x.png"; sourceTree = ""; }; + 3EEA4F241BECE440008A7F35 /* icon_like@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_like@3x.png"; sourceTree = ""; }; + 3EEA4F251BECE440008A7F35 /* icon_like@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_like@2x.png"; sourceTree = ""; }; + 3EEA4F261BECE440008A7F35 /* icon_like.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_like.png; sourceTree = ""; }; + 3EEA4F271BECE440008A7F35 /* icon_comment@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_comment@3x.png"; sourceTree = ""; }; + 3EEA4F281BECE440008A7F35 /* icon_comment@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_comment@2x.png"; sourceTree = ""; }; + 3EEA4F291BECE440008A7F35 /* icon_comment.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_comment.png; sourceTree = ""; }; + 3EEA4F361BECE775008A7F35 /* CommentsNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CommentsNode.h; sourceTree = ""; }; + 3EEA4F371BECE775008A7F35 /* CommentsNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommentsNode.m; sourceTree = ""; }; + 3EEA4F391BECE99F008A7F35 /* icon_more.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_more.png; sourceTree = ""; }; + 3EEA4F3A1BECE99F008A7F35 /* icon_more@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_more@2x.png"; sourceTree = ""; }; + 3EEA4F3B1BECE99F008A7F35 /* icon_more@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_more@3x.png"; sourceTree = ""; }; + CC6F2ABE8383FAB21802C734 /* 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 = ""; }; + FCCC1AD413FCA8603156ED15 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3EEA4EE11BECC4A1008A7F35 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 93964C9FCC28D92625106430 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3EEA4EDB1BECC4A1008A7F35 = { + isa = PBXGroup; + children = ( + 3EEA4EE61BECC4A1008A7F35 /* Sample */, + 3EEA4EE51BECC4A1008A7F35 /* Products */, + 842ADAFE88475D19B24183AC /* Pods */, + EED34FA6D8171DF44757C852 /* Frameworks */, + ); + sourceTree = ""; + }; + 3EEA4EE51BECC4A1008A7F35 /* Products */ = { + isa = PBXGroup; + children = ( + 3EEA4EE41BECC4A1008A7F35 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 3EEA4EE61BECC4A1008A7F35 /* Sample */ = { + isa = PBXGroup; + children = ( + 3EEA4F0D1BECDCA6008A7F35 /* Images */, + 3EEA4EEA1BECC4A1008A7F35 /* AppDelegate.h */, + 3EEA4EEB1BECC4A1008A7F35 /* AppDelegate.m */, + 3EEA4EED1BECC4A1008A7F35 /* ViewController.h */, + 3EEA4EEE1BECC4A1008A7F35 /* ViewController.m */, + 3EEA4EF31BECC4A1008A7F35 /* Assets.xcassets */, + 3EEA4EF81BECC4A1008A7F35 /* Info.plist */, + 3EEA4EE71BECC4A1008A7F35 /* Supporting Files */, + 3EEA4F041BECC6C9008A7F35 /* Post.h */, + 3EEA4F051BECC6C9008A7F35 /* Post.m */, + 3EEA4F071BECC855008A7F35 /* PostNode.h */, + 3EEA4F081BECC855008A7F35 /* PostNode.m */, + 3EEA4F0A1BECCA0A008A7F35 /* TextStyles.h */, + 3EEA4F0B1BECCA0A008A7F35 /* TextStyles.m */, + 3EEA4F1B1BECE358008A7F35 /* LikesNode.h */, + 3EEA4F1C1BECE358008A7F35 /* LikesNode.m */, + 3EEA4F361BECE775008A7F35 /* CommentsNode.h */, + 3EEA4F371BECE775008A7F35 /* CommentsNode.m */, + ); + path = Sample; + sourceTree = ""; + }; + 3EEA4EE71BECC4A1008A7F35 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 3EEA4EFE1BECC4E8008A7F35 /* Default-568h@2x.png */, + 3EEA4EFF1BECC4E8008A7F35 /* Default-667h@2x.png */, + 3EEA4F001BECC4E8008A7F35 /* Default-736h@3x.png */, + 3EEA4EE81BECC4A1008A7F35 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 3EEA4F0D1BECDCA6008A7F35 /* Images */ = { + isa = PBXGroup; + children = ( + 3EEA4F391BECE99F008A7F35 /* icon_more.png */, + 3EEA4F3A1BECE99F008A7F35 /* icon_more@2x.png */, + 3EEA4F3B1BECE99F008A7F35 /* icon_more@3x.png */, + 3EEA4F1E1BECE440008A7F35 /* icon_liked.png */, + 3EEA4F1F1BECE440008A7F35 /* icon_liked@2x.png */, + 3EEA4F201BECE440008A7F35 /* icon_liked@3x.png */, + 3EEA4F241BECE440008A7F35 /* icon_like@3x.png */, + 3EEA4F251BECE440008A7F35 /* icon_like@2x.png */, + 3EEA4F261BECE440008A7F35 /* icon_like.png */, + 3EEA4F271BECE440008A7F35 /* icon_comment@3x.png */, + 3EEA4F281BECE440008A7F35 /* icon_comment@2x.png */, + 3EEA4F291BECE440008A7F35 /* icon_comment.png */, + 3EEA4F0E1BECDCD6008A7F35 /* icon_android.png */, + 3EEA4F0F1BECDCD6008A7F35 /* icon_android@2x.png */, + 3EEA4F101BECDCD6008A7F35 /* icon_android@3x.png */, + 3EEA4F111BECDCD6008A7F35 /* icon_ios.png */, + 3EEA4F121BECDCD6008A7F35 /* icon_ios@2x.png */, + 3EEA4F131BECDCD6008A7F35 /* icon_ios@3x.png */, + ); + name = Images; + sourceTree = ""; + }; + 842ADAFE88475D19B24183AC /* Pods */ = { + isa = PBXGroup; + children = ( + CC6F2ABE8383FAB21802C734 /* Pods-Sample.debug.xcconfig */, + FCCC1AD413FCA8603156ED15 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + EED34FA6D8171DF44757C852 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 193CE60FE6429EFEBF6EA52B /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3EEA4EE31BECC4A1008A7F35 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3EEA4EFB1BECC4A1008A7F35 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + B5BD9E5609B2CB179EEE0CF4 /* [CP] Check Pods Manifest.lock */, + 3EEA4EE01BECC4A1008A7F35 /* Sources */, + 3EEA4EE11BECC4A1008A7F35 /* Frameworks */, + 3EEA4EE21BECC4A1008A7F35 /* Resources */, + 21F2C1D9B53F9468EAF1653F /* [CP] Copy Pods Resources */, + 852437589F1D53B9483A75DF /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 3EEA4EE41BECC4A1008A7F35 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3EEA4EDC1BECC4A1008A7F35 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0710; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 3EEA4EE31BECC4A1008A7F35 = { + CreatedOnToolsVersion = 7.1; + }; + }; + }; + buildConfigurationList = 3EEA4EDF1BECC4A1008A7F35 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3EEA4EDB1BECC4A1008A7F35; + productRefGroup = 3EEA4EE51BECC4A1008A7F35 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3EEA4EE31BECC4A1008A7F35 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3EEA4EE21BECC4A1008A7F35 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EEA4F2A1BECE440008A7F35 /* icon_liked.png in Resources */, + 3EEA4F181BECDCD6008A7F35 /* icon_ios@2x.png in Resources */, + 3EEA4F311BECE440008A7F35 /* icon_like@2x.png in Resources */, + 3EEA4F3D1BECE99F008A7F35 /* icon_more@2x.png in Resources */, + 3EEA4F141BECDCD6008A7F35 /* icon_android.png in Resources */, + 3EEA4F3E1BECE99F008A7F35 /* icon_more@3x.png in Resources */, + 3EEA4F2B1BECE440008A7F35 /* icon_liked@2x.png in Resources */, + 3EEA4F351BECE440008A7F35 /* icon_comment.png in Resources */, + 3EEA4EF41BECC4A1008A7F35 /* Assets.xcassets in Resources */, + 3EEA4F171BECDCD6008A7F35 /* icon_ios.png in Resources */, + 3EEA4F021BECC4E8008A7F35 /* Default-667h@2x.png in Resources */, + 3EEA4F161BECDCD6008A7F35 /* icon_android@3x.png in Resources */, + 3EEA4F191BECDCD6008A7F35 /* icon_ios@3x.png in Resources */, + 3EEA4F331BECE440008A7F35 /* icon_comment@3x.png in Resources */, + 3EEA4F341BECE440008A7F35 /* icon_comment@2x.png in Resources */, + 3EEA4F321BECE440008A7F35 /* icon_like.png in Resources */, + 3EEA4F151BECDCD6008A7F35 /* icon_android@2x.png in Resources */, + 3EEA4F301BECE440008A7F35 /* icon_like@3x.png in Resources */, + 3EEA4F2C1BECE440008A7F35 /* icon_liked@3x.png in Resources */, + 3EEA4F011BECC4E8008A7F35 /* Default-568h@2x.png in Resources */, + 3EEA4F031BECC4E8008A7F35 /* Default-736h@3x.png in Resources */, + 3EEA4F3C1BECE99F008A7F35 /* icon_more.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 21F2C1D9B53F9468EAF1653F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 852437589F1D53B9483A75DF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B5BD9E5609B2CB179EEE0CF4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3EEA4EE01BECC4A1008A7F35 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EEA4EEF1BECC4A1008A7F35 /* ViewController.m in Sources */, + 3EEA4EEC1BECC4A1008A7F35 /* AppDelegate.m in Sources */, + 3EEA4EE91BECC4A1008A7F35 /* main.m in Sources */, + 3EEA4F061BECC6C9008A7F35 /* Post.m in Sources */, + 3EEA4F0C1BECCA0A008A7F35 /* TextStyles.m in Sources */, + 3EEA4F381BECE775008A7F35 /* CommentsNode.m in Sources */, + 3EEA4F091BECC855008A7F35 /* PostNode.m in Sources */, + 3EEA4F1D1BECE358008A7F35 /* LikesNode.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3EEA4EF91BECC4A1008A7F35 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 3EEA4EFA1BECC4A1008A7F35 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3EEA4EFC1BECC4A1008A7F35 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CC6F2ABE8383FAB21802C734 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3EEA4EFD1BECC4A1008A7F35 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FCCC1AD413FCA8603156ED15 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3EEA4EDF1BECC4A1008A7F35 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3EEA4EF91BECC4A1008A7F35 /* Debug */, + 3EEA4EFA1BECC4A1008A7F35 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3EEA4EFB1BECC4A1008A7F35 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3EEA4EFC1BECC4A1008A7F35 /* Debug */, + 3EEA4EFD1BECC4A1008A7F35 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3EEA4EDC1BECC4A1008A7F35 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..64285ed126 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/AppDelegate.m new file mode 100644 index 0000000000..73663a6919 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/AppDelegate.m @@ -0,0 +1,24 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/CommentsNode.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/CommentsNode.h new file mode 100644 index 0000000000..422460fe9e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/CommentsNode.h @@ -0,0 +1,16 @@ +// +// CommentsNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface CommentsNode : ASControlNode + +- (instancetype)initWithCommentsCount:(NSInteger)comentsCount; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/CommentsNode.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/CommentsNode.m new file mode 100644 index 0000000000..96ba688881 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/CommentsNode.m @@ -0,0 +1,62 @@ +// +// CommentsNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "CommentsNode.h" +#import "TextStyles.h" + +@interface CommentsNode () +@property (nonatomic, strong) ASImageNode *iconNode; +@property (nonatomic, strong) ASTextNode *countNode; +@property (nonatomic, assign) NSInteger commentsCount; +@end + +@implementation CommentsNode + +- (instancetype)initWithCommentsCount:(NSInteger)comentsCount +{ + self = [super init]; + if (self) { + _commentsCount = comentsCount; + + _iconNode = [[ASImageNode alloc] init]; + _iconNode.image = [UIImage imageNamed:@"icon_comment.png"]; + [self addSubnode:_iconNode]; + + _countNode = [[ASTextNode alloc] init]; + if (_commentsCount > 0) { + _countNode.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%zd", _commentsCount] attributes:[TextStyles cellControlStyle]]; + } + [self addSubnode:_countNode]; + + // make it tappable easily + self.hitTestSlop = UIEdgeInsetsMake(-10, -10, -10, -10); + } + + return self; + +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *mainStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:6.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_iconNode, _countNode]]; + + // Adjust size + mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0); + mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0); + + return mainStack; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Info.plist new file mode 100644 index 0000000000..ed1c9acf9b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/LikesNode.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/LikesNode.h new file mode 100644 index 0000000000..1dbbc191e1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/LikesNode.h @@ -0,0 +1,16 @@ +// +// LikesNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface LikesNode : ASControlNode + +- (instancetype)initWithLikesCount:(NSInteger)likesCount; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/LikesNode.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/LikesNode.m new file mode 100644 index 0000000000..cd5ade1db4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/LikesNode.m @@ -0,0 +1,75 @@ +// +// LikesNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "LikesNode.h" +#import "TextStyles.h" + +@interface LikesNode () +@property (nonatomic, strong) ASImageNode *iconNode; +@property (nonatomic, strong) ASTextNode *countNode; +@property (nonatomic, assign) NSInteger likesCount; +@property (nonatomic, assign) BOOL liked; +@end + +@implementation LikesNode + +- (instancetype)initWithLikesCount:(NSInteger)likesCount +{ + self = [super init]; + if (self) { + _likesCount = likesCount; + _liked = (_likesCount > 0) ? [LikesNode getYesOrNo] : NO; + + _iconNode = [[ASImageNode alloc] init]; + _iconNode.image = (_liked) ? [UIImage imageNamed:@"icon_liked.png"] : [UIImage imageNamed:@"icon_like.png"]; + [self addSubnode:_iconNode]; + + _countNode = [[ASTextNode alloc] init]; + if (_likesCount > 0) { + + NSDictionary *attributes = _liked ? [TextStyles cellControlColoredStyle] : [TextStyles cellControlStyle]; + _countNode.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%ld", (long)_likesCount] attributes:attributes]; + + } + [self addSubnode:_countNode]; + + // make it tappable easily + self.hitTestSlop = UIEdgeInsetsMake(-10, -10, -10, -10); + } + + return self; + +} + ++ (BOOL)getYesOrNo +{ + int tmp = (arc4random() % 30)+1; + if (tmp % 5 == 0) { + return YES; + } + return NO; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *mainStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:6.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_iconNode, _countNode]]; + + mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0); + mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0); + + return mainStack; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Post.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Post.h new file mode 100644 index 0000000000..c8259237b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Post.h @@ -0,0 +1,25 @@ +// +// Post.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface Post : NSObject + +@property (nonatomic, copy) NSString *username; +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *photo; +@property (nonatomic, copy) NSString *post; +@property (nonatomic, copy) NSString *time; +@property (nonatomic, copy) NSString *media; +@property (nonatomic, assign) NSInteger via; + +@property (nonatomic, assign) NSInteger likes; +@property (nonatomic, assign) NSInteger comments; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Post.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Post.m new file mode 100644 index 0000000000..fc61c5bf82 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/Post.m @@ -0,0 +1,13 @@ +// +// Post.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "Post.h" + +@implementation Post +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/PostNode.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/PostNode.h new file mode 100644 index 0000000000..b558158e31 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/PostNode.h @@ -0,0 +1,18 @@ +// +// PostNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class Post; + +@interface PostNode : ASCellNode + +- (instancetype)initWithPost:(Post *)post; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/PostNode.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/PostNode.m new file mode 100644 index 0000000000..defafc0061 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/PostNode.m @@ -0,0 +1,337 @@ +// +// PostNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PostNode.h" +#import "Post.h" +#import "TextStyles.h" +#import "LikesNode.h" +#import "CommentsNode.h" + +#define PostNodeDividerColor [UIColor lightGrayColor] + +@interface PostNode() + +@property (strong, nonatomic) Post *post; +@property (strong, nonatomic) ASDisplayNode *divider; +@property (strong, nonatomic) ASTextNode *nameNode; +@property (strong, nonatomic) ASTextNode *usernameNode; +@property (strong, nonatomic) ASTextNode *timeNode; +@property (strong, nonatomic) ASTextNode *postNode; +@property (strong, nonatomic) ASImageNode *viaNode; +@property (strong, nonatomic) ASNetworkImageNode *avatarNode; +@property (strong, nonatomic) ASNetworkImageNode *mediaNode; +@property (strong, nonatomic) LikesNode *likesNode; +@property (strong, nonatomic) CommentsNode *commentsNode; +@property (strong, nonatomic) ASImageNode *optionsNode; + +@end + +@implementation PostNode + +#pragma mark - Lifecycle + +- (instancetype)initWithPost:(Post *)post +{ + self = [super init]; + if (self) { + _post = post; + + self.selectionStyle = UITableViewCellSelectionStyleNone; + + // Name node + _nameNode = [[ASTextNode alloc] init]; + _nameNode.attributedText = [[NSAttributedString alloc] initWithString:_post.name attributes:[TextStyles nameStyle]]; + _nameNode.maximumNumberOfLines = 1; + [self addSubnode:_nameNode]; + + // Username node + _usernameNode = [[ASTextNode alloc] init]; + _usernameNode.attributedText = [[NSAttributedString alloc] initWithString:_post.username attributes:[TextStyles usernameStyle]]; + _usernameNode.style.flexShrink = 1.0; //if name and username don't fit to cell width, allow username shrink + _usernameNode.truncationMode = NSLineBreakByTruncatingTail; + _usernameNode.maximumNumberOfLines = 1; + [self addSubnode:_usernameNode]; + + // Time node + _timeNode = [[ASTextNode alloc] init]; + _timeNode.attributedText = [[NSAttributedString alloc] initWithString:_post.time attributes:[TextStyles timeStyle]]; + [self addSubnode:_timeNode]; + + // Post node + _postNode = [[ASTextNode alloc] init]; + + // Processing URLs in post + NSString *kLinkAttributeName = @"TextLinkAttributeName"; + + if (![_post.post isEqualToString:@""]) { + + NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:_post.post attributes:[TextStyles postStyle]]; + + NSDataDetector *urlDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil]; + + [urlDetector enumerateMatchesInString:attrString.string options:kNilOptions range:NSMakeRange(0, attrString.string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){ + + if (result.resultType == NSTextCheckingTypeLink) { + + NSMutableDictionary *linkAttributes = [[NSMutableDictionary alloc] initWithDictionary:[TextStyles postLinkStyle]]; + linkAttributes[kLinkAttributeName] = [NSURL URLWithString:result.URL.absoluteString]; + + [attrString addAttributes:linkAttributes range:result.range]; + + } + + }]; + + // Configure node to support tappable links + _postNode.delegate = self; + _postNode.userInteractionEnabled = YES; + _postNode.linkAttributeNames = @[ kLinkAttributeName ]; + _postNode.attributedText = attrString; + _postNode.passthroughNonlinkTouches = YES; // passes touches through when they aren't on a link + + } + + [self addSubnode:_postNode]; + + + // Media + if (![_post.media isEqualToString:@""]) { + + _mediaNode = [[ASNetworkImageNode alloc] init]; + _mediaNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _mediaNode.cornerRadius = 4.0; + _mediaNode.URL = [NSURL URLWithString:_post.media]; + _mediaNode.delegate = self; + _mediaNode.imageModificationBlock = ^UIImage *(UIImage *image) { + + UIImage *modifiedImage; + CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height); + + UIGraphicsBeginImageContextWithOptions(image.size, false, [[UIScreen mainScreen] scale]); + + [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip]; + [image drawInRect:rect]; + modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); + + return modifiedImage; + + }; + [self addSubnode:_mediaNode]; + } + + // User pic + _avatarNode = [[ASNetworkImageNode alloc] init]; + _avatarNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _avatarNode.style.width = ASDimensionMakeWithPoints(44); + _avatarNode.style.height = ASDimensionMakeWithPoints(44); + _avatarNode.cornerRadius = 22.0; + _avatarNode.URL = [NSURL URLWithString:_post.photo]; + _avatarNode.imageModificationBlock = ^UIImage *(UIImage *image) { + + UIImage *modifiedImage; + CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height); + + UIGraphicsBeginImageContextWithOptions(image.size, false, [[UIScreen mainScreen] scale]); + + [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:44.0] addClip]; + [image drawInRect:rect]; + modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); + + return modifiedImage; + + }; + [self addSubnode:_avatarNode]; + + // Hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + [self updateDividerColor]; + [self addSubnode:_divider]; + + // Via + if (_post.via != 0) { + _viaNode = [[ASImageNode alloc] init]; + _viaNode.image = (_post.via == 1) ? [UIImage imageNamed:@"icon_ios.png"] : [UIImage imageNamed:@"icon_android.png"]; + [self addSubnode:_viaNode]; + } + + // Bottom controls + _likesNode = [[LikesNode alloc] initWithLikesCount:_post.likes]; + [self addSubnode:_likesNode]; + + _commentsNode = [[CommentsNode alloc] initWithCommentsCount:_post.comments]; + [self addSubnode:_commentsNode]; + + _optionsNode = [[ASImageNode alloc] init]; + _optionsNode.image = [UIImage imageNamed:@"icon_more"]; + [self addSubnode:_optionsNode]; + + for (ASDisplayNode *node in self.subnodes) { + // ASTextNode with embedded links doesn't support layer backing + if (node.supportsLayerBacking) { + node.layerBacked = YES; + } + } + } + return self; +} + +- (void)updateDividerColor +{ + /* + * UITableViewCell traverses through all its descendant views and adjusts their background color accordingly + * either to [UIColor clearColor], although potentially it could use the same color as the selection highlight itself. + * After selection, the same trick is performed again in reverse, putting all the backgrounds back as they used to be. + * But in our case, we don't want to have the background color disappearing so we reset it after highlighting or + * selection is done. + */ + _divider.backgroundColor = PostNodeDividerColor; +} + +#pragma mark - ASDisplayNode + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // Flexible spacer between username and time + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.style.flexGrow = 1.0; + + // Horizontal stack for name, username, via icon and time + NSMutableArray *layoutSpecChildren = [@[_nameNode, _usernameNode, spacer] mutableCopy]; + if (_post.via != 0) { + [layoutSpecChildren addObject:_viaNode]; + } + [layoutSpecChildren addObject:_timeNode]; + + ASStackLayoutSpec *nameStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:5.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:layoutSpecChildren]; + nameStack.style.alignSelf = ASStackLayoutAlignSelfStretch; + + // bottom controls horizontal stack + ASStackLayoutSpec *controlsStack = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:@[_likesNode, _commentsNode, _optionsNode]]; + + // Add more gaps for control line + controlsStack.style.spacingAfter = 3.0; + controlsStack.style.spacingBefore = 3.0; + + NSMutableArray *mainStackContent = [[NSMutableArray alloc] init]; + [mainStackContent addObject:nameStack]; + [mainStackContent addObject:_postNode]; + + + if (![_post.media isEqualToString:@""]){ + + // Only add the media node if an image is present + if (_mediaNode.image != nil) { + ASRatioLayoutSpec *imagePlace = + [ASRatioLayoutSpec + ratioLayoutSpecWithRatio:0.5 + child:_mediaNode]; + imagePlace.style.spacingAfter = 3.0; + imagePlace.style.spacingBefore = 3.0; + + [mainStackContent addObject:imagePlace]; + } + } + [mainStackContent addObject:controlsStack]; + + // Vertical spec of cell main content + ASStackLayoutSpec *contentSpec = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:8.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:mainStackContent]; + contentSpec.style.flexShrink = 1.0; + + // Horizontal spec for avatar + ASStackLayoutSpec *avatarContentSpec = + [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:8.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[_avatarNode, contentSpec]]; + + return [ASInsetLayoutSpec + insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) + child:avatarContentSpec]; + +} + +- (void)layout +{ + [super layout]; + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} + +#pragma mark - ASCellNode + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + + [self updateDividerColor]; +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + + [self updateDividerColor]; +} + +#pragma mark - + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // Opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // The node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +#pragma mark - ASNetworkImageNodeDelegate methods. + +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image +{ + [self setNeedsLayout]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/TextStyles.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/TextStyles.h new file mode 100644 index 0000000000..2a975bdea3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/TextStyles.h @@ -0,0 +1,23 @@ +// +// TextStyles.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface TextStyles : NSObject + ++ (NSDictionary *)nameStyle; ++ (NSDictionary *)usernameStyle; ++ (NSDictionary *)timeStyle; ++ (NSDictionary *)postStyle; ++ (NSDictionary *)postLinkStyle; ++ (NSDictionary *)cellControlStyle; ++ (NSDictionary *)cellControlColoredStyle; + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/TextStyles.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/TextStyles.m new file mode 100644 index 0000000000..ad7798c445 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/TextStyles.m @@ -0,0 +1,71 @@ +// +// TextStyles.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "TextStyles.h" + +@implementation TextStyles + ++ (NSDictionary *)nameStyle +{ + return @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor blackColor] + }; +} + ++ (NSDictionary *)usernameStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor lightGrayColor] + }; +} + ++ (NSDictionary *)timeStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor grayColor] + }; +} + ++ (NSDictionary *)postStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor blackColor] + }; +} + ++ (NSDictionary *)postLinkStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor colorWithRed:59.0/255.0 green:89.0/255.0 blue:152.0/255.0 alpha:1.0], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) + }; +} + ++ (NSDictionary *)cellControlStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor lightGrayColor] + }; +} + ++ (NSDictionary *)cellControlColoredStyle +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:13.0], + NSForegroundColorAttributeName: [UIColor colorWithRed:59.0/255.0 green:89.0/255.0 blue:152.0/255.0 alpha:1.0] + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/ViewController.h new file mode 100644 index 0000000000..6416242247 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/ViewController.h @@ -0,0 +1,13 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/ViewController.m new file mode 100644 index 0000000000..1eebcb516c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/ViewController.m @@ -0,0 +1,128 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import "Post.h" +#import "PostNode.h" + +#import +#import + +#include + +@interface ViewController () + +@property (nonatomic, strong) ASTableNode *tableNode; +@property (nonatomic, strong) NSMutableArray *socialAppDataSource; + +@end + +#pragma mark - Lifecycle + +@implementation ViewController + +- (instancetype)init +{ + _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + + self = [super initWithNode:_tableNode]; + + if (self) { + + _tableNode.delegate = self; + _tableNode.dataSource = self; + _tableNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + self.title = @"Timeline"; + + [self createSocialAppDataSource]; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // SocialAppNode has its own separator + self.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; +} + +#pragma mark - Data Model + +- (void)createSocialAppDataSource +{ + _socialAppDataSource = [[NSMutableArray alloc] init]; + + Post *newPost = [[Post alloc] init]; + newPost.name = @"Apple Guy"; + newPost.username = @"@appleguy"; + newPost.photo = @"https://avatars1.githubusercontent.com/u/565251?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; + newPost.time = @"3s"; + newPost.media = @""; + newPost.via = 0; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; + + newPost = [[Post alloc] init]; + newPost.name = @"Huy Nguyen"; + newPost.username = @"@nguyenhuy"; + newPost.photo = @"https://avatars2.githubusercontent.com/u/587874?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + newPost.time = @"1m"; + newPost.media = @""; + newPost.via = 1; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; + + newPost = [[Post alloc] init]; + newPost.name = @"Alex Long Name"; + newPost.username = @"@veryyyylongusername"; + newPost.photo = @"https://avatars1.githubusercontent.com/u/8086633?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + newPost.time = @"3:02"; + newPost.media = @"http://www.ngmag.ru/upload/iblock/f93/f9390efc34151456598077c1ba44a94d.jpg"; + newPost.via = 2; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; + + newPost = [[Post alloc] init]; + newPost.name = @"Vitaly Baev"; + newPost.username = @"@vitalybaev"; + newPost.photo = @"https://avatars0.githubusercontent.com/u/724423?v=3&s=96"; + newPost.post = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. https://github.com/facebook/AsyncDisplayKit Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + newPost.time = @"yesterday"; + newPost.media = @""; + newPost.via = 1; + newPost.likes = arc4random_uniform(74); + newPost.comments = arc4random_uniform(40); + [_socialAppDataSource addObject:newPost]; +} + +#pragma mark - ASTableNode + +- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + Post *post = self.socialAppDataSource[indexPath.row]; + return ^{ + return [[PostNode alloc] initWithPost:post]; + }; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return self.socialAppDataSource.count; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android.png new file mode 100644 index 0000000000..6d30985339 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android@2x.png new file mode 100644 index 0000000000..c0dd2f5977 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android@3x.png new file mode 100644 index 0000000000..d3e83e9334 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_android@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment.png new file mode 100644 index 0000000000..59ccfe43c1 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment@2x.png new file mode 100644 index 0000000000..bedd0593c0 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment@3x.png new file mode 100644 index 0000000000..eb8a0d7660 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_comment@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios.png new file mode 100644 index 0000000000..0cd417d446 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios@2x.png new file mode 100644 index 0000000000..f73561fd09 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios@3x.png new file mode 100644 index 0000000000..35d2bb2b37 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_ios@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like.png new file mode 100644 index 0000000000..43110b9d59 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like@2x.png new file mode 100644 index 0000000000..1b535d748b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like@3x.png new file mode 100644 index 0000000000..8c80507335 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_like@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked.png new file mode 100644 index 0000000000..b1c1ade901 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked@2x.png new file mode 100644 index 0000000000..d9dc5988ea Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked@3x.png new file mode 100644 index 0000000000..00578ac63e Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_liked@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more.png new file mode 100644 index 0000000000..013126d291 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more@2x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more@2x.png new file mode 100644 index 0000000000..3d183df436 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more@3x.png b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more@3x.png new file mode 100644 index 0000000000..d5f829ab11 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/icon_more@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/main.m b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/SocialAppLayout/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/Swift/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/Swift/Default-568h@2x.png new file mode 100644 index 0000000000..1547a98454 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Swift/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Swift/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/Swift/Default-667h@2x.png new file mode 100644 index 0000000000..988ea56bab Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Swift/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Swift/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/Swift/Default-736h@3x.png new file mode 100644 index 0000000000..d19eb325a2 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Swift/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Swift/Podfile b/submodules/AsyncDisplayKit/examples/Swift/Podfile new file mode 100644 index 0000000000..83d2cae8bf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' + +use_frameworks! + +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..533a738ea5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,397 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7319D22E19004363C2 /* AppDelegate.swift */; }; + 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7519D22E19004363C2 /* ViewController.swift */; }; + 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */; }; + 161FE6897BB33A570A663F90 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E6141C83F216FCA6D7EAEE /* Pods_Sample.framework */; }; + 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053D919EE266A00E385DE /* Default-667h@2x.png */; }; + 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */; }; + CCB01CAB1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB01CAA1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 050E7C6E19D22E19004363C2 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 050E7C7219D22E19004363C2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 050E7C7319D22E19004363C2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 050E7C7519D22E19004363C2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 08B9AAEC0A03243C3516AA96 /* 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 = ""; }; + 27E6141C83F216FCA6D7EAEE /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C5053D919EE266A00E385DE /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + 9B402DAAE8C6A8B9BE1A506B /* 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 = ""; }; + CCB01CAA1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TailLoadingCellNode.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 050E7C6B19D22E19004363C2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 161FE6897BB33A570A663F90 /* Pods_Sample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 050E7C6519D22E19004363C2 = { + isa = PBXGroup; + children = ( + 050E7C7019D22E19004363C2 /* Sample */, + 050E7C6F19D22E19004363C2 /* Products */, + 092C2001FE124604891D6E90 /* Frameworks */, + 655F2ABBD991CBDE7140FACE /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 050E7C6F19D22E19004363C2 /* Products */ = { + isa = PBXGroup; + children = ( + 050E7C6E19D22E19004363C2 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 050E7C7019D22E19004363C2 /* Sample */ = { + isa = PBXGroup; + children = ( + CCB01CAA1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift */, + 050E7C7319D22E19004363C2 /* AppDelegate.swift */, + 050E7C7519D22E19004363C2 /* ViewController.swift */, + 050E7C7119D22E19004363C2 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 050E7C7119D22E19004363C2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 050E7C7219D22E19004363C2 /* Info.plist */, + 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */, + 6C5053D919EE266A00E385DE /* Default-667h@2x.png */, + 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 092C2001FE124604891D6E90 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 27E6141C83F216FCA6D7EAEE /* Pods_Sample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 655F2ABBD991CBDE7140FACE /* Pods */ = { + isa = PBXGroup; + children = ( + 9B402DAAE8C6A8B9BE1A506B /* Pods-Sample.debug.xcconfig */, + 08B9AAEC0A03243C3516AA96 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 050E7C6D19D22E19004363C2 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 050E7C8D19D22E1A004363C2 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + B8824BD0ED824BAD8268EC35 /* [CP] Check Pods Manifest.lock */, + 050E7C6A19D22E19004363C2 /* Sources */, + 050E7C6B19D22E19004363C2 /* Frameworks */, + 050E7C6C19D22E19004363C2 /* Resources */, + 941C5E41C54B4613A2D3B760 /* [CP] Copy Pods Resources */, + 1F5A9F09F5875F61862D0783 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 050E7C6E19D22E19004363C2 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 050E7C6619D22E19004363C2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 050E7C6D19D22E19004363C2 = { + CreatedOnToolsVersion = 6.0.1; + LastSwiftMigration = 0830; + }; + }; + }; + buildConfigurationList = 050E7C6919D22E19004363C2 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 050E7C6519D22E19004363C2; + productRefGroup = 050E7C6F19D22E19004363C2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 050E7C6D19D22E19004363C2 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 050E7C6C19D22E19004363C2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */, + 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */, + 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1F5A9F09F5875F61862D0783 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 941C5E41C54B4613A2D3B760 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + B8824BD0ED824BAD8268EC35 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 050E7C6A19D22E19004363C2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CCB01CAB1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift in Sources */, + 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */, + 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 050E7C8B19D22E1A004363C2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 050E7C8C19D22E1A004363C2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 050E7C8E19D22E1A004363C2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9B402DAAE8C6A8B9BE1A506B /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 050E7C8F19D22E1A004363C2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 08B9AAEC0A03243C3516AA96 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 050E7C6919D22E19004363C2 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050E7C8B19D22E1A004363C2 /* Debug */, + 050E7C8C19D22E1A004363C2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 050E7C8D19D22E1A004363C2 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050E7C8E19D22E1A004363C2 /* Debug */, + 050E7C8F19D22E1A004363C2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 050E7C6619D22E19004363C2 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..05f94265c3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample/AppDelegate.swift b/submodules/AsyncDisplayKit/examples/Swift/Sample/AppDelegate.swift new file mode 100644 index 0000000000..df48167298 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = UIColor.white + window.rootViewController = UINavigationController(rootViewController: ViewController()) + window.makeKeyAndVisible() + self.window = window + return true + } + +} diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/Swift/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample/TailLoadingCellNode.swift b/submodules/AsyncDisplayKit/examples/Swift/Sample/TailLoadingCellNode.swift new file mode 100644 index 0000000000..5b586e7086 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample/TailLoadingCellNode.swift @@ -0,0 +1,62 @@ +// +// TailLoadingCellNode.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit +import UIKit + +final class TailLoadingCellNode: ASCellNode { + let spinner = SpinnerNode() + let text = ASTextNode() + + override init() { + super.init() + + addSubnode(text) + text.attributedText = NSAttributedString( + string: "Loading…", + attributes: [ + NSFontAttributeName: UIFont.systemFont(ofSize: 12), + NSForegroundColorAttributeName: UIColor.lightGray, + NSKernAttributeName: -0.3 + ]) + addSubnode(spinner) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + + return ASStackLayoutSpec( + direction: .horizontal, + spacing: 16, + justifyContent: .center, + alignItems: .center, + children: [ text, spinner ]) + } +} + +final class SpinnerNode: ASDisplayNode { + var activityIndicatorView: UIActivityIndicatorView { + return view as! UIActivityIndicatorView + } + + override init() { + super.init() + setViewBlock { + UIActivityIndicatorView(activityIndicatorStyle: .gray) + } + + // Set spinner node to default size of the activitiy indicator view + self.style.preferredSize = CGSize(width: 20.0, height: 20.0) + } + + override func didLoad() { + super.didLoad() + + activityIndicatorView.startAnimating() + } +} diff --git a/submodules/AsyncDisplayKit/examples/Swift/Sample/ViewController.swift b/submodules/AsyncDisplayKit/examples/Swift/Sample/ViewController.swift new file mode 100644 index 0000000000..e68c178ad5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Swift/Sample/ViewController.swift @@ -0,0 +1,142 @@ +// +// ViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +final class ViewController: ASViewController, ASTableDataSource, ASTableDelegate { + + struct State { + var itemCount: Int + var fetchingMore: Bool + static let empty = State(itemCount: 20, fetchingMore: false) + } + + enum Action { + case beginBatchFetch + case endBatchFetch(resultCount: Int) + } + + var tableNode: ASTableNode { + return node as! ASTableNode + } + + fileprivate(set) var state: State = .empty + + init() { + super.init(node: ASTableNode()) + tableNode.delegate = self + tableNode.dataSource = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("storyboards are incompatible with truth and beauty") + } + + // MARK: ASTableNode data source and delegate. + + func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { + // Should read the row count directly from table view but + // https://github.com/facebook/AsyncDisplayKit/issues/1159 + let rowCount = self.tableNode(tableNode, numberOfRowsInSection: 0) + + if state.fetchingMore && indexPath.row == rowCount - 1 { + let node = TailLoadingCellNode() + node.style.height = ASDimensionMake(44.0) + return node; + } + + let node = ASTextCellNode() + node.text = String(format: "[%ld.%ld] says hello!", indexPath.section, indexPath.row) + + return node + } + + func numberOfSections(in tableNode: ASTableNode) -> Int { + return 1 + } + + func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { + var count = state.itemCount + if state.fetchingMore { + count += 1 + } + return count + } + + func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { + /// This call will come in on a background thread. Switch to main + /// to add our spinner, then fire off our fetch. + DispatchQueue.main.async { + let oldState = self.state + self.state = ViewController.handleAction(.beginBatchFetch, fromState: oldState) + self.renderDiff(oldState) + } + + ViewController.fetchDataWithCompletion { resultCount in + let action = Action.endBatchFetch(resultCount: resultCount) + let oldState = self.state + self.state = ViewController.handleAction(action, fromState: oldState) + self.renderDiff(oldState) + context.completeBatchFetching(true) + } + } + + fileprivate func renderDiff(_ oldState: State) { + + self.tableNode.performBatchUpdates({ + + // Add or remove items + let rowCountChange = state.itemCount - oldState.itemCount + if rowCountChange > 0 { + let indexPaths = (oldState.itemCount.. Void) { + let time = DispatchTime.now() + Double(Int64(TimeInterval(NSEC_PER_SEC) * 1.0)) / Double(NSEC_PER_SEC) + DispatchQueue.main.asyncAfter(deadline: time) { + let resultCount = Int(arc4random_uniform(20)) + completion(resultCount) + } + } + + fileprivate static func handleAction(_ action: Action, fromState state: State) -> State { + var state = state + switch action { + case .beginBatchFetch: + state.fetchingMore = true + case let .endBatchFetch(resultCount): + state.itemCount += resultCount + state.fetchingMore = false + } + return state + } +} diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Podfile b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..80493caefa --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,378 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFD19D4F94A00CBA93C /* GradientTableNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* GradientTableNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */; }; + 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 */; }; + C81806AD7AEA1CC3061C4742 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF6047FF8AB82FD183F47C3 /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CFB19D4F94A00CBA93C /* GradientTableNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GradientTableNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* GradientTableNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GradientTableNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RandomCoreGraphicsNode.h; sourceTree = ""; }; + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RandomCoreGraphicsNode.m; sourceTree = ""; }; + 19760F30C80D89FC055CF57A /* 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 = ""; }; + 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; }; + A5A1F5A0D2B4375F57D02F1A /* 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 = ""; }; + DBF6047FF8AB82FD183F47C3 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C81806AD7AEA1CC3061C4742 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* GradientTableNode.h */, + 05561CFC19D4F94A00CBA93C /* GradientTableNode.mm */, + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */, + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + DBF6047FF8AB82FD183F47C3 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 19760F30C80D89FC055CF57A /* Pods-Sample.debug.xcconfig */, + A5A1F5A0D2B4375F57D02F1A /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 6E94068CFAA7736333E7D960 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6E94068CFAA7736333E7D960 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */, + 05561CFD19D4F94A00CBA93C /* GradientTableNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 19760F30C80D89FC055CF57A /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A5A1F5A0D2B4375F57D02F1A /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/AppDelegate.m new file mode 100644 index 0000000000..faa44dc60b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/AppDelegate.m @@ -0,0 +1,28 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +#import +#import + +@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 = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/GradientTableNode.h b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/GradientTableNode.h new file mode 100644 index 0000000000..e679beb7d7 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/GradientTableNode.h @@ -0,0 +1,22 @@ +// +// GradientTableNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * This ASCellNode contains an ASTableNode. It intelligently interacts with a containing ASCollectionView, + * to preload and clean up contents as the user scrolls around both vertically and horizontally — in a way that minimizes memory usage. + */ +@interface GradientTableNode : ASCellNode + +- (instancetype)initWithElementSize:(CGSize)size; + +@property (nonatomic) NSInteger pageNumber; + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/GradientTableNode.mm b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/GradientTableNode.mm new file mode 100644 index 0000000000..af36ca637c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/GradientTableNode.mm @@ -0,0 +1,79 @@ +// +// GradientTableNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "GradientTableNode.h" +#import "RandomCoreGraphicsNode.h" +#import "AppDelegate.h" + +#import + +#import +#import + + +@interface GradientTableNode () +{ + ASTableNode *_tableNode; + CGSize _elementSize; +} + +@end + + +@implementation GradientTableNode + +- (instancetype)initWithElementSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _elementSize = size; + + _tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStylePlain]; + _tableNode.delegate = self; + _tableNode.dataSource = self; + + ASRangeTuningParameters rangeTuningParameters; + rangeTuningParameters.leadingBufferScreenfuls = 1.0; + rangeTuningParameters.trailingBufferScreenfuls = 0.5; + [_tableNode setTuningParameters:rangeTuningParameters forRangeType:ASLayoutRangeTypeDisplay]; + + [self addSubnode:_tableNode]; + + return self; +} + +- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section +{ + return 100; +} + +- (ASCellNode *)tableNode:(ASTableNode *)tableNode nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + RandomCoreGraphicsNode *elementNode = [[RandomCoreGraphicsNode alloc] init]; + elementNode.style.preferredSize = _elementSize; + elementNode.indexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:_pageNumber]; + + return elementNode; +} + +- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [tableNode deselectRowAtIndexPath:indexPath animated:NO]; + [_tableNode reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; +} + +- (void)layout +{ + [super layout]; + + _tableNode.frame = self.bounds; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/RandomCoreGraphicsNode.h b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/RandomCoreGraphicsNode.h new file mode 100644 index 0000000000..c07752ab1a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/RandomCoreGraphicsNode.h @@ -0,0 +1,19 @@ +// +// RandomCoreGraphicsNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface RandomCoreGraphicsNode : ASCellNode +{ + ASTextNode *_indexPathTextNode; +} + +@property NSIndexPath *indexPath; + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/RandomCoreGraphicsNode.m b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/RandomCoreGraphicsNode.m new file mode 100644 index 0000000000..bf114349e5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/RandomCoreGraphicsNode.m @@ -0,0 +1,103 @@ +// +// RandomCoreGraphicsNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "RandomCoreGraphicsNode.h" +#import + +@implementation RandomCoreGraphicsNode + +@synthesize indexPath = _indexPath; + ++ (UIColor *)randomColor +{ + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + CGFloat locations[3]; + NSMutableArray *colors = [NSMutableArray arrayWithCapacity:3]; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[0] = 0.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[1] = 1.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[2] = ( arc4random() % 256 / 256.0 ); + + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)colors, locations); + + CGContextDrawLinearGradient(ctx, gradient, CGPointZero, CGPointMake(bounds.size.width, bounds.size.height), 0); + + CGGradientRelease(gradient); + CGColorSpaceRelease(colorSpace); +} + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _indexPathTextNode = [[ASTextNode alloc] init]; + [self addSubnode:_indexPathTextNode]; + + return self; +} + +- (void)setIndexPath:(NSIndexPath *)indexPath +{ + @synchronized (self) { + _indexPath = indexPath; + _indexPathTextNode.attributedText = [[NSAttributedString alloc] initWithString:[indexPath description] attributes:nil]; + } +} + +- (NSIndexPath *)indexPath +{ + NSIndexPath *indexPath = nil; + @synchronized (self) { + indexPath = _indexPath; + } + return indexPath; +} + +- (void)layout +{ + [super layout]; + + _indexPathTextNode.frame = self.bounds; +} + +#if 0 +- (void)fetchData +{ + NSLog(@"fetchData - %@, %@", self, self.indexPath); + [super fetchData]; +} + +- (void)clearFetchedData +{ + NSLog(@"clearFetchedData - %@, %@", self, self.indexPath); + [super clearFetchedData]; +} + +- (void)visibilityDidChange:(BOOL)isVisible +{ + NSLog(@"visibilityDidChange:%d - %@, %@", isVisible, self, self.indexPath); + [super visibilityDidChange:isVisible]; +} +#endif + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m new file mode 100644 index 0000000000..7e852414c4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m @@ -0,0 +1,86 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ViewController.h" +#import "GradientTableNode.h" + +@interface ViewController () +{ + ASPagerNode *_pagerNode; +} + +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _pagerNode = [[ASPagerNode alloc] init]; + _pagerNode.dataSource = self; + _pagerNode.delegate = self; + ASDisplayNode.shouldShowRangeDebugOverlay = YES; + + // Could implement ASCollectionDelegate if we wanted extra callbacks, like from UIScrollView. + //_pagerNode.delegate = self; + + self.title = @"Paging Table Nodes"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRedo + target:self + action:@selector(reloadEverything)]; + + return self; +} + +- (void)reloadEverything +{ + [_pagerNode reloadData]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubnode:_pagerNode]; +} + +- (void)viewWillLayoutSubviews +{ + _pagerNode.frame = self.view.bounds; +} + +#pragma mark - +#pragma mark ASPagerNode. + +- (ASCellNode *)pagerNode:(ASPagerNode *)pagerNode nodeAtIndex:(NSInteger)index +{ + CGSize boundsSize = pagerNode.bounds.size; + CGSize gradientRowSize = CGSizeMake(boundsSize.width, 100); + GradientTableNode *node = [[GradientTableNode alloc] initWithElementSize:gradientRowSize]; + node.pageNumber = index; + return node; +} + +- (ASSizeRange)pagerNode:(ASPagerNode *)pagerNode constrainedSizeForNodeAtIndex:(NSInteger)index; +{ + return ASSizeRangeMake(pagerNode.bounds.size); +} + +- (NSInteger)numberOfPagesInPagerNode:(ASPagerNode *)pagerNode +{ + return 10; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/main.m b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/VerticalWithinHorizontalScrolling/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/Videos/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples/Videos/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Videos/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples/Videos/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Videos/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples/Videos/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Videos/Podfile b/submodules/AsyncDisplayKit/examples/Videos/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..8a04601003 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,380 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 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 */; }; + AE8E41191C228A4A00913AC4 /* bearacrat@2x.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AE8E41181C228A4A00913AC4 /* bearacrat@2x.jpg */; }; + AE8E411B1C23634C00913AC4 /* simon.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = AE8E411A1C235A6000913AC4 /* simon.mp4 */; }; + AED850671C22679200183ED3 /* playButton@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AED850661C22679200183ED3 /* playButton@2x.png */; }; + AEE1F2C01C293CF1005E0577 /* playButtonSelected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AEE1F2BF1C293CF1005E0577 /* playButtonSelected@2x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 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; }; + A2092CAF5607B3863A3700A2 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + AE8E41181C228A4A00913AC4 /* bearacrat@2x.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "bearacrat@2x.jpg"; sourceTree = ""; }; + AE8E411A1C235A6000913AC4 /* simon.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = simon.mp4; sourceTree = ""; }; + AED850661C22679200183ED3 /* playButton@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playButton@2x.png"; sourceTree = ""; }; + AEE1F2BF1C293CF1005E0577 /* playButtonSelected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playButtonSelected@2x.png"; sourceTree = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5791C5525B690FA54F26ACE8 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + AED850661C22679200183ED3 /* playButton@2x.png */, + AEE1F2BF1C293CF1005E0577 /* playButtonSelected@2x.png */, + AE8E41181C228A4A00913AC4 /* bearacrat@2x.jpg */, + AE8E411A1C235A6000913AC4 /* simon.mp4 */, + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + A2092CAF5607B3863A3700A2 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + CFD6AA1D30516C27DEE5602B /* Pods-Sample.debug.xcconfig */, + E51646FF8D3676A1D826A5AE /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 93B7780A33739EF25F20366B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AE8E41191C228A4A00913AC4 /* bearacrat@2x.jpg in Resources */, + AED850671C22679200183ED3 /* playButton@2x.png in Resources */, + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + AE8E411B1C23634C00913AC4 /* simon.mp4 in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + AEE1F2C01C293CF1005E0577 /* playButtonSelected@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 93B7780A33739EF25F20366B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CFD6AA1D30516C27DEE5602B /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E51646FF8D3676A1D826A5AE /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/ASVideoNode.h b/submodules/AsyncDisplayKit/examples/Videos/Sample/ASVideoNode.h new file mode 100644 index 0000000000..bec76ccc57 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/ASVideoNode.h @@ -0,0 +1,37 @@ +// +// ASVideoNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +typedef NS_ENUM(NSUInteger, ASVideoGravity) { + ASVideoGravityResizeAspect, + ASVideoGravityResizeAspectFill, + ASVideoGravityResize +}; + +// set up boolean to repeat video +// set up delegate methods to provide play button +// tapping should play and pause + +@interface ASVideoNode : ASDisplayNode +@property (nonatomic) NSURL *URL; +@property (nonatomic) BOOL shouldRepeat; +@property (nonatomic) ASVideoGravity gravity; + +- (instancetype)initWithURL:(NSURL *)URL; +- (instancetype)initWithURL:(NSURL *)URL videoGravity:(ASVideoGravity)gravity; + +- (void)play; +- (void)pause; + +@end + +@protocol ASVideoNodeDelegate + +@end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/ASVideoNode.m b/submodules/AsyncDisplayKit/examples/Videos/Sample/ASVideoNode.m new file mode 100644 index 0000000000..f7db712b5b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/ASVideoNode.m @@ -0,0 +1,80 @@ +// +// ASVideoNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import "ASVideoNode.h" + +@interface ASVideoNode () +@property (nonatomic) AVPlayer *player; +@end + +@implementation ASVideoNode + +- (instancetype)initWithURL:(NSURL *)URL; +{ + return [self initWithURL:URL videoGravity:ASVideoGravityResizeAspect]; +} + +- (instancetype)initWithURL:(NSURL *)URL videoGravity:(ASVideoGravity)gravity; +{ + if (!(self = [super initWithLayerBlock:^CALayer *{ + AVPlayerLayer *layer = [[AVPlayerLayer alloc] init]; + AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:URL]; + + layer.player = [[AVPlayer alloc] initWithPlayerItem:item]; + + return layer; + }])) { return nil; } + + self.gravity = gravity; + + return self; +} + +- (void)setGravity:(ASVideoGravity)gravity; +{ + switch (gravity) { + case ASVideoGravityResize: + ((AVPlayerLayer *)self.layer).videoGravity = AVLayerVideoGravityResize; + break; + case ASVideoGravityResizeAspect: + ((AVPlayerLayer *)self.layer).videoGravity = AVLayerVideoGravityResizeAspect; + break; + case ASVideoGravityResizeAspectFill: + ((AVPlayerLayer *)self.layer).videoGravity = AVLayerVideoGravityResizeAspectFill; + break; + default: + ((AVPlayerLayer *)self.layer).videoGravity = AVLayerVideoGravityResizeAspect; + break; + } +} + +- (ASVideoGravity)gravity; +{ + if ([((AVPlayerLayer *)self.layer).contentsGravity isEqualToString:AVLayerVideoGravityResize]) { + return ASVideoGravityResize; + } + if ([((AVPlayerLayer *)self.layer).contentsGravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) { + return ASVideoGravityResizeAspectFill; + } + + return ASVideoGravityResizeAspect; +} + +- (void)play; +{ + [[((AVPlayerLayer *)self.layer) player] play]; +} + +- (void)pause; +{ + [[((AVPlayerLayer *)self.layer) player] pause]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples/Videos/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples/Videos/Sample/AppDelegate.m new file mode 100644 index 0000000000..d0fd66f775 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/Info.plist b/submodules/AsyncDisplayKit/examples/Videos/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples/Videos/Sample/ViewController.h new file mode 100644 index 0000000000..40cc00f2fc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/ViewController.h @@ -0,0 +1,13 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples/Videos/Sample/ViewController.m new file mode 100644 index 0000000000..988b3579b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/ViewController.m @@ -0,0 +1,200 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import + +@interface ViewController() +@property (nonatomic, strong) ASDisplayNode *rootNode; +@property (nonatomic, strong) ASVideoNode *guitarVideoNode; +@end + +@implementation ViewController + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // Root node for the view controller + _rootNode = [ASDisplayNode new]; + _rootNode.frame = self.view.bounds; + _rootNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + ASVideoNode *guitarVideoNode = self.guitarVideoNode; + [_rootNode addSubnode:self.guitarVideoNode]; + + ASVideoNode *nicCageVideoNode = self.nicCageVideoNode; + [_rootNode addSubnode:nicCageVideoNode]; + + // Video node with custom play button + ASVideoNode *simonVideoNode = self.simonVideoNode; + [_rootNode addSubnode:simonVideoNode]; + + ASVideoNode *hlsVideoNode = self.hlsVideoNode; + [_rootNode addSubnode:hlsVideoNode]; + + CGSize mainScreenBoundsSize = [UIScreen mainScreen].bounds.size; + + _rootNode.layoutSpecBlock = ^ASLayoutSpec *(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + + // Layout all nodes absolute in a static layout spec + guitarVideoNode.style.preferredSize = CGSizeMake(mainScreenBoundsSize.width, mainScreenBoundsSize.height / 3.0); + guitarVideoNode.style.layoutPosition = CGPointMake(0, 0); + + nicCageVideoNode.style.preferredSize = CGSizeMake(mainScreenBoundsSize.width/2, mainScreenBoundsSize.height / 3.0); + nicCageVideoNode.style.layoutPosition = CGPointMake(mainScreenBoundsSize.width / 2.0, mainScreenBoundsSize.height / 3.0); + + simonVideoNode.style.preferredSize = CGSizeMake(mainScreenBoundsSize.width/2, mainScreenBoundsSize.height / 3.0); + simonVideoNode.style.layoutPosition = CGPointMake(0.0, mainScreenBoundsSize.height - (mainScreenBoundsSize.height / 3.0)); + + hlsVideoNode.style.preferredSize = CGSizeMake(mainScreenBoundsSize.width / 2.0, mainScreenBoundsSize.height / 3.0); + hlsVideoNode.style.layoutPosition = CGPointMake(0.0, mainScreenBoundsSize.height / 3.0); + + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[guitarVideoNode, nicCageVideoNode, simonVideoNode, hlsVideoNode]]; + }; + + // Delay setting video asset for testing that the transition between the placeholder and setting/playing the asset is seamless. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + hlsVideoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8"]]; + [hlsVideoNode play]; + }); + + [self.view addSubnode:_rootNode]; +} + +#pragma mark - Getter / Setter + +- (ASVideoNode *)guitarVideoNode; +{ + if (_guitarVideoNode) { + return _guitarVideoNode; + } + + _guitarVideoNode = [[ASVideoNode alloc] init]; + _guitarVideoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"]]; + _guitarVideoNode.gravity = AVLayerVideoGravityResizeAspectFill; + _guitarVideoNode.backgroundColor = [UIColor lightGrayColor]; + _guitarVideoNode.periodicTimeObserverTimescale = 1; //Default is 100 + _guitarVideoNode.delegate = self; + + return _guitarVideoNode; +} + +- (ASVideoNode *)nicCageVideoNode; +{ + ASVideoNode *nicCageVideoNode = [[ASVideoNode alloc] init]; + nicCageVideoNode.delegate = self; + nicCageVideoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-753fe655-86bb-46da-89b7-aa59c60e49c0-niccage.mp4"]]; + nicCageVideoNode.gravity = AVLayerVideoGravityResize; + nicCageVideoNode.backgroundColor = [UIColor lightGrayColor]; + nicCageVideoNode.shouldAutorepeat = YES; + nicCageVideoNode.shouldAutoplay = YES; + nicCageVideoNode.muted = YES; + + return nicCageVideoNode; +} + +- (ASVideoNode *)simonVideoNode +{ + ASVideoNode *simonVideoNode = [[ASVideoNode alloc] init]; + + NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"simon" ofType:@"mp4"]]; + simonVideoNode.asset = [AVAsset assetWithURL:url]; + simonVideoNode.gravity = AVLayerVideoGravityResizeAspect; + simonVideoNode.backgroundColor = [UIColor lightGrayColor]; + simonVideoNode.shouldAutorepeat = YES; + simonVideoNode.shouldAutoplay = YES; + simonVideoNode.muted = YES; + + return simonVideoNode; +} + +- (ASVideoNode *)hlsVideoNode; +{ + ASVideoNode *hlsVideoNode = [[ASVideoNode alloc] init]; + + hlsVideoNode.delegate = self; + hlsVideoNode.gravity = AVLayerVideoGravityResize; + hlsVideoNode.backgroundColor = [UIColor redColor]; // Should not be seen after placeholder image is loaded + hlsVideoNode.shouldAutorepeat = YES; + hlsVideoNode.shouldAutoplay = YES; + hlsVideoNode.muted = YES; + + // Placeholder image + hlsVideoNode.URL = [NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/en/5/52/Testcard_F.jpg"]; + + return hlsVideoNode; +} + +- (ASButtonNode *)playButton; +{ + ASButtonNode *playButtonNode = [[ASButtonNode alloc] init]; + + UIImage *image = [UIImage imageNamed:@"playButton@2x.png"]; + [playButtonNode setImage:image forState:UIControlStateNormal]; + [playButtonNode setImage:[UIImage imageNamed:@"playButtonSelected@2x.png"] forState:UIControlStateHighlighted]; + + // Change placement of play button if necessary + //playButtonNode.contentHorizontalAlignment = ASHorizontalAlignmentStart; + //playButtonNode.contentVerticalAlignment = ASVerticalAlignmentCenter; + + return playButtonNode; +} + +#pragma mark - Actions + +- (void)didTapVideoNode:(ASVideoNode *)videoNode +{ + if (videoNode == self.guitarVideoNode) { + if (videoNode.playerState == ASVideoNodePlayerStatePlaying) { + [videoNode pause]; + } else if(videoNode.playerState == ASVideoNodePlayerStateLoading) { + [videoNode pause]; + } else { + [videoNode play]; + } + return; + } + if (videoNode.player.muted == YES) { + videoNode.player.muted = NO; + } else { + videoNode.player.muted = YES; + } +} + +#pragma mark - ASVideoNodeDelegate + +- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState +{ + //Ignore nicCageVideo + if (videoNode != _guitarVideoNode) { + return; + } + + if (toState == ASVideoNodePlayerStatePlaying) { + NSLog(@"guitarVideoNode is playing"); + } else if (toState == ASVideoNodePlayerStateFinished) { + NSLog(@"guitarVideoNode finished"); + } else if (toState == ASVideoNodePlayerStateLoading) { + NSLog(@"guitarVideoNode is buffering"); + } +} + +- (void)videoNode:(ASVideoNode *)videoNode didPlayToTimeInterval:(NSTimeInterval)timeInterval +{ + if (videoNode != _guitarVideoNode) { + return; + } + + NSLog(@"guitarVideoNode playback time is: %f",timeInterval); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/bearacrat@2x.jpg b/submodules/AsyncDisplayKit/examples/Videos/Sample/bearacrat@2x.jpg new file mode 100644 index 0000000000..a083949d6f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Sample/bearacrat@2x.jpg differ diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/main.m b/submodules/AsyncDisplayKit/examples/Videos/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples/Videos/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/playButton@2x.png b/submodules/AsyncDisplayKit/examples/Videos/Sample/playButton@2x.png new file mode 100644 index 0000000000..9ac2481afe Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Sample/playButton@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/playButtonSelected@2x.png b/submodules/AsyncDisplayKit/examples/Videos/Sample/playButtonSelected@2x.png new file mode 100644 index 0000000000..f22ebc0f81 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Sample/playButtonSelected@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples/Videos/Sample/simon.mp4 b/submodules/AsyncDisplayKit/examples/Videos/Sample/simon.mp4 new file mode 100644 index 0000000000..95a4176c82 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples/Videos/Sample/simon.mp4 differ diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e073f01f5d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj @@ -0,0 +1,551 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2362FA1E2D33A0007E08F1 /* Date.swift */; }; + 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */; }; + 3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F641E1F94530039F711 /* Assets.xcassets */; }; + 3AB33F681E1F94530039F711 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */; }; + 3AB33F761E1F9C330039F711 /* PhotoFeedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F751E1F9C330039F711 /* PhotoFeedTableViewController.swift */; }; + 3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F771E1F9C400039F711 /* PhotoFeedTableNodeController.swift */; }; + 3AB33F7B1E1F9E630039F711 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F7A1E1F9E630039F711 /* UIColor.swift */; }; + 3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F801E1FDE100039F711 /* Webservice.swift */; }; + 3AB33F831E20E81E0039F711 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F821E20E81E0039F711 /* Constants.swift */; }; + 3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F851E20E9B10039F711 /* PhotoFeedModel.swift */; }; + 3AB33F881E20ED460039F711 /* PhotoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F871E20ED460039F711 /* PhotoModel.swift */; }; + 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F8B1E2106F30039F711 /* URL.swift */; }; + 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F951E2269D40039F711 /* PopularPageModel.swift */; }; + 3AB33F981E22A0080039F711 /* ParseResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F971E22A0080039F711 /* ParseResponse.swift */; }; + 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */; }; + 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA11E230A160039F711 /* NetworkImageView.swift */; }; + 3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */; }; + 692CD06E20E1A40D00D9B963 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */; }; + 7E438240D2C4026931D60594 /* Pods_ASDKgram_Swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */; }; + 9D4DFC5E20E1DF660067C960 /* OrderedDictionary+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */; }; + 9D4DFC5F20E1DF660067C960 /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */; }; + 9D4DFC6020E1DF660067C960 /* OrderedDictionary+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 019E984FADA258377FC6B2D8 /* Pods-ASDKgram-Swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKgram-Swift.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift.debug.xcconfig"; sourceTree = ""; }; + 3A2362FA1E2D33A0007E08F1 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ASDKgram-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 3AB33F641E1F94530039F711 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3AB33F671E1F94530039F711 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 3AB33F691E1F94530039F711 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3AB33F751E1F9C330039F711 /* PhotoFeedTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedTableViewController.swift; sourceTree = ""; }; + 3AB33F771E1F9C400039F711 /* PhotoFeedTableNodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedTableNodeController.swift; sourceTree = ""; }; + 3AB33F7A1E1F9E630039F711 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; + 3AB33F801E1FDE100039F711 /* Webservice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Webservice.swift; sourceTree = ""; }; + 3AB33F821E20E81E0039F711 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 3AB33F851E20E9B10039F711 /* PhotoFeedModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedModel.swift; sourceTree = ""; }; + 3AB33F871E20ED460039F711 /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = ""; }; + 3AB33F8B1E2106F30039F711 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + 3AB33F951E2269D40039F711 /* PopularPageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularPageModel.swift; sourceTree = ""; }; + 3AB33F971E22A0080039F711 /* ParseResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseResponse.swift; sourceTree = ""; }; + 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = ""; }; + 3AB33FA11E230A160039F711 /* NetworkImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = ""; }; + 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableNodeCell.swift; sourceTree = ""; }; + 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ASDKgram_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; + 9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Codable.swift"; sourceTree = ""; }; + 9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; + 9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Description.swift"; sourceTree = ""; }; + A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKgram-Swift.release.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3AB33F571E1F94520039F711 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7E438240D2C4026931D60594 /* Pods_ASDKgram_Swift.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3AB33F511E1F94520039F711 = { + isa = PBXGroup; + children = ( + 3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */, + 3AB33F5B1E1F94520039F711 /* Products */, + 78A64CA59A49BE1637214DF1 /* Pods */, + A7DD645D70CF34C7CA3B1A8B /* Frameworks */, + ); + sourceTree = ""; + }; + 3AB33F5B1E1F94520039F711 /* Products */ = { + isa = PBXGroup; + children = ( + 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */, + ); + name = Products; + sourceTree = ""; + }; + 3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */ = { + isa = PBXGroup; + children = ( + 9D4DFC5A20E1DF660067C960 /* OrderedDictionary */, + 3AB33F991E22CF160039F711 /* Views */, + 3AB33F841E20E98C0039F711 /* Model */, + 3AB33F7D1E1FDA890039F711 /* Client */, + 3AB33F791E1F9E4E0039F711 /* Extensions */, + 3AB33F721E1F9B650039F711 /* Controllers */, + 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */, + 3AB33F821E20E81E0039F711 /* Constants.swift */, + 3AB33F641E1F94530039F711 /* Assets.xcassets */, + 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */, + 3AB33F691E1F94530039F711 /* Info.plist */, + ); + path = "ASDKgram-Swift"; + sourceTree = ""; + }; + 3AB33F721E1F9B650039F711 /* Controllers */ = { + isa = PBXGroup; + children = ( + 3AB33F741E1F9B9F0039F711 /* ASDK */, + 3AB33F731E1F9B950039F711 /* UIKit */, + ); + name = Controllers; + sourceTree = ""; + }; + 3AB33F731E1F9B950039F711 /* UIKit */ = { + isa = PBXGroup; + children = ( + 3AB33F751E1F9C330039F711 /* PhotoFeedTableViewController.swift */, + ); + name = UIKit; + sourceTree = ""; + }; + 3AB33F741E1F9B9F0039F711 /* ASDK */ = { + isa = PBXGroup; + children = ( + 3AB33F771E1F9C400039F711 /* PhotoFeedTableNodeController.swift */, + ); + name = ASDK; + sourceTree = ""; + }; + 3AB33F791E1F9E4E0039F711 /* Extensions */ = { + isa = PBXGroup; + children = ( + 3A2362FA1E2D33A0007E08F1 /* Date.swift */, + 692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */, + 3AB33F7A1E1F9E630039F711 /* UIColor.swift */, + 3AB33F8B1E2106F30039F711 /* URL.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 3AB33F7D1E1FDA890039F711 /* Client */ = { + isa = PBXGroup; + children = ( + 3AB33F801E1FDE100039F711 /* Webservice.swift */, + 3AB33F971E22A0080039F711 /* ParseResponse.swift */, + ); + name = Client; + sourceTree = ""; + }; + 3AB33F841E20E98C0039F711 /* Model */ = { + isa = PBXGroup; + children = ( + 3AB33F851E20E9B10039F711 /* PhotoFeedModel.swift */, + 3AB33F871E20ED460039F711 /* PhotoModel.swift */, + 3AB33F951E2269D40039F711 /* PopularPageModel.swift */, + ); + name = Model; + sourceTree = ""; + }; + 3AB33F991E22CF160039F711 /* Views */ = { + isa = PBXGroup; + children = ( + 3AB33F9B1E22CF3C0039F711 /* UIKit */, + 3AB33F9C1E22CF5C0039F711 /* ASDK */, + ); + name = Views; + sourceTree = ""; + }; + 3AB33F9B1E22CF3C0039F711 /* UIKit */ = { + isa = PBXGroup; + children = ( + 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */, + 3AB33FA11E230A160039F711 /* NetworkImageView.swift */, + ); + name = UIKit; + sourceTree = ""; + }; + 3AB33F9C1E22CF5C0039F711 /* ASDK */ = { + isa = PBXGroup; + children = ( + 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */, + ); + name = ASDK; + sourceTree = ""; + }; + 78A64CA59A49BE1637214DF1 /* Pods */ = { + isa = PBXGroup; + children = ( + 019E984FADA258377FC6B2D8 /* Pods-ASDKgram-Swift.debug.xcconfig */, + A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9D4DFC5A20E1DF660067C960 /* OrderedDictionary */ = { + isa = PBXGroup; + children = ( + 9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */, + 9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */, + 9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */, + ); + path = OrderedDictionary; + sourceTree = ""; + }; + A7DD645D70CF34C7CA3B1A8B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3AB33F591E1F94520039F711 /* ASDKgram-Swift */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AB33F6C1E1F94530039F711 /* Build configuration list for PBXNativeTarget "ASDKgram-Swift" */; + buildPhases = ( + A5A729883237749EE5D2DB1C /* [CP] Check Pods Manifest.lock */, + 3AB33F561E1F94520039F711 /* Sources */, + 3AB33F571E1F94520039F711 /* Frameworks */, + 3AB33F581E1F94520039F711 /* Resources */, + 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */, + 3A7BEDD71E254278005769D4 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ASDKgram-Swift"; + productName = "ASDKgram-Swift"; + productReference = 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3AB33F521E1F94520039F711 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0820; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "Calum Harris"; + TargetAttributes = { + 3AB33F591E1F94520039F711 = { + CreatedOnToolsVersion = 8.2; + DevelopmentTeam = B3H446T9U7; + LastSwiftMigration = 0820; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 3AB33F551E1F94520039F711 /* Build configuration list for PBXProject "ASDKgram-Swift" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3AB33F511E1F94520039F711; + productRefGroup = 3AB33F5B1E1F94520039F711 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3AB33F591E1F94520039F711 /* ASDKgram-Swift */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3AB33F581E1F94520039F711 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AB33F681E1F94530039F711 /* LaunchScreen.storyboard in Resources */, + 3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3A7BEDD71E254278005769D4 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = ""; + }; + A5A729883237749EE5D2DB1C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ASDKgram-Swift-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3AB33F561E1F94520039F711 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */, + 3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */, + 3AB33F7B1E1F9E630039F711 /* UIColor.swift in Sources */, + 3AB33F981E22A0080039F711 /* ParseResponse.swift in Sources */, + 692CD06E20E1A40D00D9B963 /* NumberFormatter.swift in Sources */, + 3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */, + 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */, + 9D4DFC6020E1DF660067C960 /* OrderedDictionary+Description.swift in Sources */, + 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */, + 3AB33F831E20E81E0039F711 /* Constants.swift in Sources */, + 9D4DFC5F20E1DF660067C960 /* OrderedDictionary.swift in Sources */, + 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */, + 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */, + 9D4DFC5E20E1DF660067C960 /* OrderedDictionary+Codable.swift in Sources */, + 3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */, + 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */, + 3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */, + 3AB33F881E20ED460039F711 /* PhotoModel.swift in Sources */, + 3AB33F761E1F9C330039F711 /* PhotoFeedTableViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3AB33F671E1F94530039F711 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3AB33F6A1E1F94530039F711 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3AB33F6B1E1F94530039F711 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3AB33F6D1E1F94530039F711 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 019E984FADA258377FC6B2D8 /* Pods-ASDKgram-Swift.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = B3H446T9U7; + INFOPLIST_FILE = "ASDKgram-Swift/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.RenaldoMoon.ASDKgram-Swift"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 3AB33F6E1E1F94530039F711 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = B3H446T9U7; + INFOPLIST_FILE = "ASDKgram-Swift/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.RenaldoMoon.ASDKgram-Swift"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3AB33F551E1F94520039F711 /* Build configuration list for PBXProject "ASDKgram-Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AB33F6A1E1F94530039F711 /* Debug */, + 3AB33F6B1E1F94530039F711 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AB33F6C1E1F94530039F711 /* Build configuration list for PBXNativeTarget "ASDKgram-Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AB33F6D1E1F94530039F711 /* Debug */, + 3AB33F6E1E1F94530039F711 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3AB33F521E1F94520039F711 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..96a0fb6dec --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..102e5c6013 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift new file mode 100644 index 0000000000..990773f687 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift @@ -0,0 +1,51 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + + // UIKit Home Feed viewController & navController + + let UIKitNavController = UINavigationController(rootViewController: PhotoFeedTableViewController()) + UIKitNavController.tabBarItem.title = "UIKit" + + // ASDK Home Feed viewController & navController + + let ASDKNavController = UINavigationController(rootViewController: PhotoFeedTableNodeController()) + ASDKNavController.tabBarItem.title = "ASDK" + + // UITabBarController + + let tabBarController = UITabBarController() + tabBarController.viewControllers = [UIKitNavController, ASDKNavController] + tabBarController.selectedIndex = 1 + tabBarController.tabBar.tintColor = UIColor.mainBarTintColor + + // Nav Bar appearance + + UINavigationBar.appearance().barTintColor = UIColor.mainBarTintColor + + // UIWindow + + window = UIWindow() + window?.backgroundColor = .white + window?.rootViewController = tabBarController + window?.makeKeyAndVisible() + + return true + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..1d060ed288 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..c1191aaf8d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Base.lproj/Main.storyboard b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..273375fc70 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift new file mode 100644 index 0000000000..f4da709f26 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift @@ -0,0 +1,34 @@ +// +// Constants.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +struct Constants { + struct Unsplash { + struct URLS { + static let Host = "https://api.unsplash.com/" + static let PopularEndpoint = "photos?order_by=popular" + static let SearchEndpoint = "photos/search?geo=" //latitude,longitude,radius + static let UserEndpoint = "photos?user_id=" + static let ConsumerKey = "&client_id=3b99a69cee09770a4a0bbb870b437dbda53efb22f6f6de63714b71c4df7c9642" + static let ImagesPerPage = 30 + } + } + + struct CellLayout { + static let FontSize: CGFloat = 14 + static let HeaderHeight: CGFloat = 50 + static let UserImageHeight: CGFloat = 30 + static let HorizontalBuffer: CGFloat = 10 + static let VerticalBuffer: CGFloat = 5 + static let InsetForAvatar = UIEdgeInsets(top: HorizontalBuffer, left: 0, bottom: HorizontalBuffer, right: HorizontalBuffer) + static let InsetForHeader = UIEdgeInsets(top: 0, left: HorizontalBuffer, bottom: 0, right: HorizontalBuffer) + static let InsetForFooter = UIEdgeInsets(top: VerticalBuffer, left: HorizontalBuffer, bottom: VerticalBuffer, right: HorizontalBuffer) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift new file mode 100644 index 0000000000..d367584b10 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift @@ -0,0 +1,39 @@ +// +// Date.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +extension Date { + static let iso8601Formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .iso8601) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + return formatter + }() + + static func timeStringSince(fromConverted date: Date) -> String { + let diffDates = NSCalendar.current.dateComponents([.day, .hour, .second], from: date, to: Date()) + + if let week = diffDates.day, week > 7 { + return "\(week / 7)w" + } else if let day = diffDates.day, day > 0 { + return "\(day)d" + } else if let hour = diffDates.hour, hour > 0 { + return "\(hour)h" + } else if let second = diffDates.second, second > 0 { + return "\(second)s" + } else if let zero = diffDates.second, zero == 0 { + return "1s" + } else { + return "ERROR" + } + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Info.plist b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Info.plist new file mode 100644 index 0000000000..c12df3b8a9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift new file mode 100644 index 0000000000..937f87fadf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift @@ -0,0 +1,47 @@ +// +// NetworkImageView.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +let imageCache = NSCache() + +class NetworkImageView: UIImageView { + + var imageUrlString: String? + + func loadImageUsingUrlString(urlString: String) { + + imageUrlString = urlString + + let url = URL(string: urlString) + + image = nil + + if let imageFromCache = imageCache.object(forKey: urlString as NSString) { + self.image = imageFromCache + return + } + + URLSession.shared.dataTask(with: url!, completionHandler: { (data, respones, error) in + + if error != nil { + print(error!) + return + } + + DispatchQueue.main.async { + let imageToCache = UIImage(data: data!) + if self.imageUrlString == urlString { + self.image = imageToCache + } + imageCache.setObject(imageToCache!, forKey: urlString as NSString) + } + }).resume() + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift new file mode 100644 index 0000000000..246f398b94 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift @@ -0,0 +1,18 @@ +// +// NumberFormatter.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +extension NumberFormatter { + static let decimalNumberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift new file mode 100644 index 0000000000..45e5de48f7 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift @@ -0,0 +1,142 @@ +// +// OrderedDictionary+Codable.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#if swift(>=4.1) + +extension OrderedDictionary: Encodable where Key: Encodable, Value: Encodable { + + /// __inheritdoc__ + public func encode(to encoder: Encoder) throws { + // Encode the ordered dictionary as an array of alternating key-value pairs. + var container = encoder.unkeyedContainer() + + for (key, value) in self { + try container.encode(key) + try container.encode(value) + } + } + +} + +extension OrderedDictionary: Decodable where Key: Decodable, Value: Decodable { + + /// __inheritdoc__ + public init(from decoder: Decoder) throws { + // Decode the ordered dictionary from an array of alternating key-value pairs. + self.init() + + var container = try decoder.unkeyedContainer() + + while !container.isAtEnd { + let key = try container.decode(Key.self) + guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) } + let value = try container.decode(Value.self) + + self[key] = value + } + } + +} + +#else + +extension OrderedDictionary: Encodable { + + /// __inheritdoc__ + public func encode(to encoder: Encoder) throws { + // Since Swift 4.0 lacks the protocol conditional conformance support, we have to make the + // whole OrderedDictionary type conform to Encodable and assert that the key and value + // types conform to Encodable. Furthermore, we leverage a trick of super encoders to be + // able to encode objects without knowing their exact types. This trick was used in the + // standard library for encoding/decoding Dictionary before Swift 4.1. + + _assertTypeIsEncodable(Key.self, in: type(of: self)) + _assertTypeIsEncodable(Value.self, in: type(of: self)) + + var container = encoder.unkeyedContainer() + + for (key, value) in self { + let keyEncoder = container.superEncoder() + try (key as! Encodable).encode(to: keyEncoder) + + let valueEncoder = container.superEncoder() + try (value as! Encodable).encode(to: valueEncoder) + } + } + + private func _assertTypeIsEncodable(_ type: T.Type, in wrappingType: Any.Type) { + guard T.self is Encodable.Type else { + if T.self == Encodable.self || T.self == Codable.self { + preconditionFailure("\(wrappingType) does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.") + } else { + preconditionFailure("\(wrappingType) does not conform to Encodable because \(T.self) does not conform to Encodable.") + } + } + } + +} + +extension OrderedDictionary: Decodable { + + /// __inheritdoc__ + public init(from decoder: Decoder) throws { + // Since Swift 4.0 lacks the protocol conditional conformance support, we have to make the + // whole OrderedDictionary type conform to Decodable and assert that the key and value + // types conform to Decodable. Furthermore, we leverage a trick of super decoders to be + // able to decode objects without knowing their exact types. This trick was used in the + // standard library for encoding/decoding Dictionary before Swift 4.1. + + self.init() + + _assertTypeIsDecodable(Key.self, in: type(of: self)) + _assertTypeIsDecodable(Value.self, in: type(of: self)) + + var container = try decoder.unkeyedContainer() + + let keyMetaType = (Key.self as! Decodable.Type) + let valueMetaType = (Value.self as! Decodable.Type) + + while !container.isAtEnd { + let keyDecoder = try container.superDecoder() + let key = try keyMetaType.init(from: keyDecoder) as! Key + + guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) } + + let valueDecoder = try container.superDecoder() + let value = try valueMetaType.init(from: valueDecoder) as! Value + + self[key] = value + } + } + + private func _assertTypeIsDecodable(_ type: T.Type, in wrappingType: Any.Type) { + guard T.self is Decodable.Type else { + if T.self == Decodable.self || T.self == Codable.self { + preconditionFailure("\(wrappingType) does not conform to Decodable because Decodable does not conform to itself. You must use a concrete type to encode or decode.") + } else { + preconditionFailure("\(wrappingType) does not conform to Decodable because \(T.self) does not conform to Decodable.") + } + } + } + +} + +#endif + +fileprivate extension DecodingError { + + fileprivate static func unkeyedContainerReachedEndBeforeValue(_ codingPath: [CodingKey]) -> DecodingError { + return DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: codingPath, + debugDescription: "Unkeyed container reached end before value in key-value pair." + ) + ) + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift new file mode 100644 index 0000000000..604dbac119 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift @@ -0,0 +1,58 @@ +// +// OrderedDictionary+Description.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +extension OrderedDictionary: CustomStringConvertible { + + /// A textual representation of the ordered dictionary. + public var description: String { + return makeDescription(debug: false) + } + +} + +extension OrderedDictionary: CustomDebugStringConvertible { + + /// A textual representation of the ordered dictionary, suitable for debugging. + public var debugDescription: String { + return makeDescription(debug: true) + } + +} + +extension OrderedDictionary { + + fileprivate func makeDescription(debug: Bool) -> String { + // The implementation of the description is inspired by zwaldowski's implementation of the + // ordered dictionary. See http://bit.ly/2iqGhrb + + if isEmpty { return "[:]" } + + let printFunction: (Any, inout String) -> () = { + if debug { + return { debugPrint($0, separator: "", terminator: "", to: &$1) } + } else { + return { print($0, separator: "", terminator: "", to: &$1) } + } + }() + + let descriptionForItem: (Any) -> String = { item in + var description = "" + printFunction(item, &description) + return description + } + + let bodyComponents = map { element in + return descriptionForItem(element.key) + ": " + descriptionForItem(element.value) + } + + let body = bodyComponents.joined(separator: ", ") + + return "[\(body)]" + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift new file mode 100644 index 0000000000..38bb9853f2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift @@ -0,0 +1,618 @@ +// +// OrderedDictionary.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +/// A generic collection for storing key-value pairs in an ordered manner. +/// +/// Same as in a dictionary all keys in the collection are unique and have an associated value. +/// Same as in an array, all key-value pairs (elements) are kept sorted and accessible by +/// a zero-based integer index. +public struct OrderedDictionary: BidirectionalCollection { + + // ======================================================= // + // MARK: - Type Aliases + // ======================================================= // + + /// The type of the key-value pair stored in the ordered dictionary. + public typealias Element = (key: Key, value: Value) + + /// The type of the index. + public typealias Index = Int + + /// The type of the indices collection. + public typealias Indices = CountableRange + + /// The type of the contiguous subrange of the ordered dictionary's elements. + /// + /// - SeeAlso: OrderedDictionarySlice + public typealias SubSequence = OrderedDictionarySlice + + // ======================================================= // + // MARK: - Initialization + // ======================================================= // + + /// Creates an empty ordered dictionary. + public init() {} + + /// Creates an ordered dictionary from a sequence of values keyed by a key which gets extracted + /// from the value in the provided closure. + /// + /// - Parameter values: The sequence of values. + /// - Parameter getKey: The closure which provides a key for the given value from the values + /// sequence. + public init(values: Values, keyedBy getKey: (Value) -> Key) where Values.Element == Value { + self.init(values.map { (getKey($0), $0) }) + } + + /// Creates an ordered dictionary from a sequence of values keyed by a key loaded from the value + /// at the given key path. + /// + /// - Parameter values: The sequence of values. + /// - Parameter keyPath: The key path for the value to locate its key at. + public init(values: [Value], keyedBy keyPath: KeyPath) { + self.init(values.map { ($0[keyPath: keyPath], $0) }) + } + + /// Creates an ordered dictionary from a regular unsorted dictionary by sorting it using the + /// the given sort function. + /// + /// - Parameter unsorted: The unsorted dictionary. + /// - Parameter areInIncreasingOrder: The sort function which compares the key-value pairs. + public init(unsorted: Dictionary, areInIncreasingOrder: (Element, Element) -> Bool) { + let elements = unsorted + .map { (key: $0.key, value: $0.value) } + .sorted(by: areInIncreasingOrder) + + self.init(elements) + } + + /// Creates an ordered dictionary from a sequence of key-value pairs. + /// + /// - Parameter elements: The key-value pairs that will make up the new ordered dictionary. + /// Each key in `elements` must be unique. + public init(_ elements: S) where S.Element == Element { + for (key, value) in elements { + precondition(!containsKey(key), "Elements sequence contains duplicate keys") + self[key] = value + } + } + + // ======================================================= // + // MARK: - Ordered Keys & Values + // ======================================================= // + + /// A collection containing just the keys of the ordered dictionary in the correct order. + public var orderedKeys: OrderedDictionaryKeys { + return self.lazy.map { $0.key } + } + + /// A collection containing just the values of the ordered dictionary in the correct order. + public var orderedValues: OrderedDictionaryValues { + return self.lazy.map { $0.value } + } + + // ======================================================= // + // MARK: - Dictionary + // ======================================================= // + + /// Converts itself to a common unsorted dictionary. + public var unorderedDictionary: Dictionary { + return _keysToValues + } + + // ======================================================= // + // MARK: - Key-based Access + // ======================================================= // + + /// Accesses the value associated with the given key for reading and writing. + /// + /// This key-based subscript returns the value for the given key if the key is found in the + /// ordered dictionary, or `nil` if the key is not found. + /// + /// When you assign a value for a key and that key already exists, the ordered dictionary + /// overwrites the existing value and preservers the index of the key-value pair. If the ordered + /// dictionary does not contain the key, a new key-value pair is appended to the end of the + /// ordered dictionary. + /// + /// If you assign `nil` as the value for the given key, the ordered dictionary removes that key + /// and its associated value if it exists. + /// + /// - Parameter key: The key to find in the ordered dictionary. + /// - Returns: The value associated with `key` if `key` is in the ordered dictionary; otherwise, + /// `nil`. + public subscript(key: Key) -> Value? { + get { + return value(forKey: key) + } + set(newValue) { + if let newValue = newValue { + updateValue(newValue, forKey: key) + } else { + removeValue(forKey: key) + } + } + } + + /// Returns a Boolean value indicating whether the ordered dictionary contains the given key. + /// + /// - Parameter key: The key to be looked up. + /// - Returns: `true` if the ordered dictionary contains the given key; otherwise, `false`. + public func containsKey(_ key: Key) -> Bool { + return _keysToValues[key] != nil + } + + /// Returns the value associated with the given key if the key is found in the ordered + /// dictionary, or `nil` if the key is not found. + /// + /// - Parameter key: The key to find in the ordered dictionary. + /// - Returns: The value associated with `key` if `key` is in the ordered dictionary; otherwise, + /// `nil`. + public func value(forKey key: Key) -> Value? { + return _keysToValues[key] + } + + /// Updates the value stored in the ordered dictionary for the given key, or appends a new + /// key-value pair if the key does not exist. + /// + /// - Parameter value: The new value to add to the ordered dictionary. + /// - Parameter key: The key to associate with `value`. If `key` already exists in the ordered + /// dictionary, `value` replaces the existing associated value. If `key` is not already a key + /// of the ordered dictionary, the `(key, value)` pair is appended at the end of the ordered + /// dictionary. + @discardableResult + public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? { + if containsKey(key) { + let currentValue = _unsafeValue(forKey: key) + + _keysToValues[key] = value + + return currentValue + } else { + _orderedKeys.append(key) + _keysToValues[key] = value + + return nil + } + } + + /// Removes the given key and its associated value from the ordered dictionary. + /// + /// If the key is found in the ordered dictionary, this method returns the key's associated + /// value. On removal, the indices of the ordered dictionary are invalidated. If the key is + /// not found in the ordered dictionary, this method returns `nil`. + /// + /// - Parameter key: The key to remove along with its associated value. + /// - Returns: The value that was removed, or `nil` if the key was not present in the + /// ordered dictionary. + /// + /// - SeeAlso: remove(at:) + @discardableResult + public mutating func removeValue(forKey key: Key) -> Value? { + guard let index = index(forKey: key) else { return nil } + + let currentValue = self[index].value + + _orderedKeys.remove(at: index) + _keysToValues[key] = nil + + return currentValue + } + + /// Removes all key-value pairs from the ordered dictionary and invalidates all indices. + /// + /// - Parameter keepCapacity: Whether the ordered dictionary should keep its underlying storage. + /// If you pass `true`, the operation preserves the storage capacity that the collection has, + /// otherwise the underlying storage is released. The default is `false`. + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + _orderedKeys.removeAll(keepingCapacity: keepCapacity) + _keysToValues.removeAll(keepingCapacity: keepCapacity) + } + + private func _unsafeValue(forKey key: Key) -> Value { + let value = _keysToValues[key] + precondition(value != nil, "Inconsistency error occurred in OrderedDictionary") + return value! + } + + // ======================================================= // + // MARK: - Index-based Access + // ======================================================= // + + /// Accesses the key-value pair at the specified position. + /// + /// The specified position has to be a valid index of the ordered dictionary. The index-base + /// subscript returns the key-value pair corresponding to the index. + /// + /// - Parameter position: The position of the key-value pair to access. `position` must be + /// a valid index of the ordered dictionary and not equal to `endIndex`. + /// - Returns: A tuple containing the key-value pair corresponding to `position`. + /// + /// - SeeAlso: update(:at:) + public subscript(position: Index) -> Element { + precondition(indices.contains(position), "OrderedDictionary index is out of range") + + let key = _orderedKeys[position] + let value = _unsafeValue(forKey: key) + + return (key, value) + } + + /// Returns the index for the given key. + /// + /// - Parameter key: The key to find in the ordered dictionary. + /// - Returns: The index for `key` and its associated value if `key` is in the ordered + /// dictionary; otherwise, `nil`. + public func index(forKey key: Key) -> Index? { + return _orderedKeys.index(of: key) + } + + /// Returns the key-value pair at the specified index, or `nil` if there is no key-value pair + /// at that index. + /// + /// - Parameter index: The index of the key-value pair to be looked up. `index` does not have to + /// be a valid index. + /// - Returns: A tuple containing the key-value pair corresponding to `index` if the index is + /// valid; otherwise, `nil`. + public func elementAt(_ index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } + + /// Checks whether the given key-value pair can be inserted into to ordered dictionary by + /// validating the presence of the key. + /// + /// - Parameter newElement: The key-value pair to be inserted into the ordered dictionary. + /// - Returns: `true` if the key-value pair can be safely inserted; otherwise, `false`. + /// + /// - SeeAlso: canInsert(key:) + /// - SeeAlso: canInsert(at:) + @available(*, deprecated, message: "Use canInsert(key:) with the element's key instead") + public func canInsert(_ newElement: Element) -> Bool { + return canInsert(key: newElement.key) + } + + /// Checks whether a key-value pair with the given key can be inserted into the ordered + /// dictionary by validating its presence. + /// + /// - Parameter key: The key to be inserted into the ordered dictionary. + /// - Returns: `true` if the key can safely be inserted; ortherwise, `false`. + /// + /// - SeeAlso: canInsert(at:) + public func canInsert(key: Key) -> Bool { + return !containsKey(key) + } + + /// Checks whether a new key-value pair can be inserted into the ordered dictionary at the + /// given index. + /// + /// - Parameter index: The index the new key-value pair should be inserted at. + /// - Returns: `true` if a new key-value pair can be inserted at the specified index; otherwise, + /// `false`. + /// + /// - SeeAlso: canInsert(key:) + public func canInsert(at index: Index) -> Bool { + return index >= startIndex && index <= endIndex + } + + /// Inserts a new key-value pair at the specified position. + /// + /// If the key of the inserted pair already exists in the ordered dictionary, a runtime error + /// is triggered. Use `canInsert(_:)` for performing a check first, so that this method can + /// be executed safely. + /// + /// - Parameter newElement: The new key-value pair to insert into the ordered dictionary. The + /// key contained in the pair must not be already present in the ordered dictionary. + /// - Parameter index: The position at which to insert the new key-value pair. `index` must be + /// a valid index of the ordered dictionary or equal to `endIndex` property. + /// + /// - SeeAlso: canInsert(key:) + /// - SeeAlso: canInsert(at:) + /// - SeeAlso: update(:at:) + public mutating func insert(_ newElement: Element, at index: Index) { + precondition(canInsert(key: newElement.key), "Cannot insert duplicate key in OrderedDictionary") + precondition(canInsert(at: index), "Cannot insert at invalid index in OrderedDictionary") + + let (key, value) = newElement + + _orderedKeys.insert(key, at: index) + _keysToValues[key] = value + } + + /// Checks whether the key-value pair at the given index can be updated with the given key-value + /// pair. This is not the case if the key of the updated element is already present in the + /// ordered dictionary and located at another index than the updated one. + /// + /// Although this is a checking method, a valid index has to be provided. + /// + /// - Parameter newElement: The key-value pair to be set at the specified position. + /// - Parameter index: The position at which to set the key-value pair. `index` must be a valid + /// index of the ordered dictionary. + public func canUpdate(_ newElement: Element, at index: Index) -> Bool { + var keyPresentAtIndex = false + return _canUpdate(newElement, at: index, keyPresentAtIndex: &keyPresentAtIndex) + } + + /// Updates the key-value pair located at the specified position. + /// + /// If the key of the updated pair already exists in the ordered dictionary *and* is located at + /// a different position than the specified one, a runtime error is triggered. Use + /// `canUpdate(_:at:)` for performing a check first, so that this method can be executed safely. + /// + /// - Parameter newElement: The key-value pair to be set at the specified position. + /// - Parameter index: The position at which to set the key-value pair. `index` must be a valid + /// index of the ordered dictionary. + /// + /// - SeeAlso: canUpdate(_:at:) + /// - SeeAlso: insert(:at:) + @discardableResult + public mutating func update(_ newElement: Element, at index: Index) -> Element? { + // Store the flag indicating whether the key of the inserted element + // is present at the updated index + var keyPresentAtIndex = false + + precondition( + _canUpdate(newElement, at: index, keyPresentAtIndex: &keyPresentAtIndex), + "OrderedDictionary update duplicates key" + ) + + // Decompose the element + let (key, value) = newElement + + // Load the current element at the index + let replacedElement = self[index] + + // If its key differs, remove its associated value + if (!keyPresentAtIndex) { + _keysToValues.removeValue(forKey: replacedElement.key) + } + + // Store the new position of the key and the new value associated with the key + _orderedKeys[index] = key + _keysToValues[key] = value + + return replacedElement + } + + /// Removes and returns the key-value pair at the specified position if there is any key-value + /// pair, or `nil` if there is none. + /// + /// - Parameter index: The position of the key-value pair to remove. + /// - Returns: The element at the specified index, or `nil` if the position is not taken. + /// + /// - SeeAlso: removeValue(forKey:) + @discardableResult + public mutating func remove(at index: Index) -> Element? { + guard let element = elementAt(index) else { return nil } + + _orderedKeys.remove(at: index) + _keysToValues.removeValue(forKey: element.key) + + return element + } + + private func _canUpdate(_ newElement: Element, at index: Index, keyPresentAtIndex: inout Bool) -> Bool { + precondition(indices.contains(index), "OrderedDictionary index is out of range") + + let currentIndexOfKey = self.index(forKey: newElement.key) + + let keyNotPresent = currentIndexOfKey == nil + keyPresentAtIndex = currentIndexOfKey == index + + return keyNotPresent || keyPresentAtIndex + } + + // ======================================================= // + // MARK: - Moving Elements + // ======================================================= // + + /// Moves an existing key-value pair specified by the given key to the new index by removing it + /// from its original index first and inserting it at the new index. If the movement is + /// actually performed, the previous index of the key-value pair is returned. Otherwise, `nil` + /// is returned. + /// + /// - Parameter key: The key specifying the key-value pair to move. + /// - Parameter newIndex: The new index the key-value pair should be moved to. + /// - Returns: The previous index of the key-value pair if it was sucessfully moved. + @discardableResult + public mutating func moveElement(forKey key: Key, to newIndex: Index) -> Index? { + // Load the previous index and return nil if the index is not found. + guard let previousIndex = index(forKey: key) else { return nil } + + // If the previous and new indices match, threat it as if the movement was already + // performed. + guard previousIndex != newIndex else { return previousIndex } + + // Remove the value for the key at its original index. + let value = removeValue(forKey: key)! + + // Validate the new index. + precondition(canInsert(at: newIndex), "Cannot move to invalid index in OrderedDictionary") + + // Insert the element at the new index. + insert((key: key, value: value), at: newIndex) + + return previousIndex + } + + // ======================================================= // + // MARK: - Sorting Elements + // ======================================================= // + + /// Sorts the ordered dictionary in place, using the given predicate as the comparison between + /// elements. + /// + /// The predicate must be a *strict weak ordering* over the elements. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first argument + /// should be ordered before its second argument; otherwise, `false`. + /// + /// - SeeAlso: MutableCollection.sort(by:), sorted(by:) + public mutating func sort(by areInIncreasingOrder: (Element, Element) -> Bool) { + _orderedKeys = _sortedElements(by: areInIncreasingOrder).map { $0.key } + } + + /// Returns a new ordered dictionary, sorted using the given predicate as the comparison between + /// elements. + /// + /// The predicate must be a *strict weak ordering* over the elements. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first argument + /// should be ordered before its second argument; otherwise, `false`. + /// - Returns: A new ordered dictionary sorted according to the predicate. + /// + /// - SeeAlso: MutableCollection.sorted(by:), sort(by:) + /// - MutatingVariant: sort + public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> OrderedDictionary { + return OrderedDictionary(_sortedElements(by: areInIncreasingOrder)) + } + + private func _sortedElements(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] { + return sorted(by: areInIncreasingOrder) + } + + // ======================================================= // + // MARK: - Slices + // ======================================================= // + + /// Accesses a contiguous subrange of the ordered dictionary. + /// + /// - Parameter bounds: A range of the ordered dictionary's indices. The bounds of the range + /// must be valid indices of the ordered dictionary. + /// - Returns: The slice view at the ordered dictionary in the specified subrange. + public subscript(bounds: Range) -> SubSequence { + return OrderedDictionarySlice(base: self, bounds: bounds) + } + + // ======================================================= // + // MARK: - Indices + // ======================================================= // + + /// The indices that are valid for subscripting the ordered dictionary. + public var indices: Indices { + return _orderedKeys.indices + } + + /// The position of the first key-value pair in a non-empty ordered dictionary. + public var startIndex: Index { + return _orderedKeys.startIndex + } + + /// The position which is one greater than the position of the last valid key-value pair in the + /// ordered dictionary. + public var endIndex: Index { + return _orderedKeys.endIndex + } + + /// Returns the position immediately after the given index. + public func index(after i: Index) -> Index { + return _orderedKeys.index(after: i) + } + + /// Returns the position immediately before the given index. + public func index(before i: Index) -> Index { + return _orderedKeys.index(before: i) + } + + // ======================================================= // + // MARK: - Internal Storage + // ======================================================= // + + /// The backing storage for the ordered keys. + fileprivate var _orderedKeys = [Key]() + + /// The backing storage for the mapping of keys to values. + fileprivate var _keysToValues = [Key: Value]() + +} + +// ======================================================= // +// MARK: - Subtypes +// ======================================================= // + +#if swift(>=4.1) + +/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered +/// dictionary. +public typealias OrderedDictionarySlice = Slice> + +/// A collection containing the keys of the ordered dictionary. +/// +/// Under the hood this is a lazily evaluated bidirectional collection deriving the keys from +/// the base ordered dictionary on-the-fly. +public typealias OrderedDictionaryKeys = LazyMapCollection, Key> + +/// A collection containing the values of the ordered dictionary. +/// +/// Under the hood this is a lazily evaluated bidirectional collection deriving the values from +/// the base ordered dictionary on-the-fly. +public typealias OrderedDictionaryValues = LazyMapCollection, Value> + +#else + +/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered +/// dictionary. +public typealias OrderedDictionarySlice = Slice> + +/// A collection containing the keys of the ordered dictionary. +/// +/// Under the hood this is a lazily evaluated bidirectional collection deriving the keys from +/// the base ordered dictionary on-the-fly. +public typealias OrderedDictionaryKeys = LazyMapCollection, Key> + +/// A collection containing the values of the ordered dictionary. +/// +/// Under the hood this is a lazily evaluated bidirectional collection deriving the values from +/// the base ordered dictionary on-the-fly. +public typealias OrderedDictionaryValues = LazyMapCollection, Value> + +#endif + +// ======================================================= // +// MARK: - Literals +// ======================================================= // + +extension OrderedDictionary: ExpressibleByArrayLiteral { + + /// Creates an ordered dictionary initialized from an array literal containing a list of + /// key-value pairs. + public init(arrayLiteral elements: Element...) { + self.init(elements) + } + +} + +extension OrderedDictionary: ExpressibleByDictionaryLiteral { + + /// Creates an ordered dictionary initialized from a dictionary literal. + public init(dictionaryLiteral elements: (Key, Value)...) { + self.init(elements.map { element in + let (key, value) = element + return (key: key, value: value) + }) + } + +} + +// ======================================================= // +// MARK: - Equatable Conformance +// ======================================================= // + +#if swift(>=4.1) + +extension OrderedDictionary: Equatable where Value: Equatable {} + +#endif + +extension OrderedDictionary where Value: Equatable { + + /// Returns a Boolean value that indicates whether the two given ordered dictionaries with + /// equatable values are equal. + public static func == (lhs: OrderedDictionary, rhs: OrderedDictionary) -> Bool { + return lhs._orderedKeys == rhs._orderedKeys + && lhs._keysToValues == rhs._keysToValues + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PX500Convenience.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PX500Convenience.swift new file mode 100644 index 0000000000..a8754d695b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PX500Convenience.swift @@ -0,0 +1,24 @@ +// +// PX500Convenience.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +func parsePopularPage(withURL: URL) -> Resource { + + let parse = Resource(url: withURL, parseJSON: { jsonData in + + guard let json = jsonData as? JSONDictionary, let photos = json["photos"] as? [JSONDictionary] else { return .failure(.errorParsingJSON) } + + guard let model = PopularPageModel(dictionary: json, photosArray: photos.flatMap(PhotoModel.init)) else { return .failure(.errorParsingJSON) } + + return .success(model) + }) + + return parse +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift new file mode 100644 index 0000000000..0870651e89 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift @@ -0,0 +1,23 @@ +// +// ParseResponse.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +func parsePopularPage(withURL: URL, page: Int) -> Resource { + let parse = Resource(url: withURL, page: page) { metaData, jsonData in + do { + let photos = try JSONDecoder().decode([PhotoModel].self, from: jsonData) + return .success(PopularPageModel(metaData: metaData, photos: photos)) + } catch { + return .failure(.errorParsingJSON) + } + } + + return parse +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift new file mode 100644 index 0000000000..edfe37ff37 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift @@ -0,0 +1,119 @@ +// +// PhotoFeedModel.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +final class PhotoFeedModel { + + // MARK: Properties + + public private(set) var photoFeedModelType: PhotoFeedModelType + + private var orderedPhotos: OrderedDictionary = [:] + private var currentPage: Int = 0 + private var totalPages: Int = 0 + private var totalItems: Int = 0 + private var fetchPageInProgress: Bool = false + + // MARK: Lifecycle + + init(photoFeedModelType: PhotoFeedModelType) { + self.photoFeedModelType = photoFeedModelType + } + + // MARK: API + + lazy var url: URL = { + return URL.URLForFeedModelType(feedModelType: self.photoFeedModelType) + }() + + var numberOfItems: Int { + return orderedPhotos.count + } + + func itemAtIndexPath(_ indexPath: IndexPath) -> PhotoModel { + return orderedPhotos[indexPath.row].value + } + + // return in completion handler the number of additions and the status of internet connection + + func updateNewBatchOfPopularPhotos(additionsAndConnectionStatusCompletion: @escaping (Int, InternetStatus) -> ()) { + + // For this example let's use the main thread as locking queue + DispatchQueue.main.async { + guard !self.fetchPageInProgress else { + return + } + + self.fetchPageInProgress = true + self.fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, error in + self.fetchPageInProgress = false + + if let error = error { + switch error { + case .noInternetConnection: + additionsAndConnectionStatusCompletion(0, .noConnection) + default: + additionsAndConnectionStatusCompletion(0, .connected) + } + } else { + additionsAndConnectionStatusCompletion(additions, .connected) + } + } + } + } + + private func fetchNextPageOfPopularPhotos(replaceData: Bool, numberOfAdditionsCompletion: @escaping (Int, NetworkingError?) -> ()) { + if currentPage == totalPages, currentPage != 0 { + numberOfAdditionsCompletion(0, .customError("No pages left to parse")) + return + } + + let pageToFetch = currentPage + 1 + WebService().load(resource: parsePopularPage(withURL: url, page: pageToFetch)) { [unowned self] result in + // Callback will happen on main for now + switch result { + case .success(let itemsPage): + // Update current state + self.totalItems = itemsPage.totalNumberOfItems + self.totalPages = itemsPage.totalPages + self.currentPage = itemsPage.page + + // Update photos + if replaceData { + self.orderedPhotos = [] + } + var insertedItems = 0 + for photo in itemsPage.photos { + if !self.orderedPhotos.containsKey(photo.photoID) { + // Append a new key-value pair by setting a value for an non-existent key + self.orderedPhotos[photo.photoID] = photo + insertedItems += 1 + } + } + + numberOfAdditionsCompletion(insertedItems, nil) + case .failure(let fail): + print(fail) + numberOfAdditionsCompletion(0, fail) + } + } + } +} + +enum PhotoFeedModelType { + case photoFeedModelTypePopular + case photoFeedModelTypeLocation + case photoFeedModelTypeUserPhotos +} + +enum InternetStatus { + case connected + case noConnection +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift new file mode 100644 index 0000000000..6c2fe39f14 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift @@ -0,0 +1,106 @@ +// +// PhotoFeedTableNodeController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +class PhotoFeedTableNodeController: ASViewController { + + // MARK: Lifecycle + + private lazy var activityIndicatorView: UIActivityIndicatorView = { + return UIActivityIndicatorView(activityIndicatorStyle: .gray) + }() + + var photoFeedModel = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) + + init() { + super.init(node: ASTableNode()) + + navigationItem.title = "ASDK" + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MAKR: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + node.allowsSelection = false + node.dataSource = self + node.delegate = self + node.leadingScreensForBatching = 2.5 + node.view.separatorStyle = .none + + navigationController?.hidesBarsOnSwipe = true + + node.view.addSubview(activityIndicatorView) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Center the activity indicator view + let bounds = node.bounds + activityIndicatorView.frame.origin = CGPoint( + x: (bounds.width - activityIndicatorView.frame.width) / 2.0, + y: (bounds.height - activityIndicatorView.frame.height) / 2.0 + ) + } + + func fetchNewBatchWithContext(_ context: ASBatchContext?) { + DispatchQueue.main.async { + self.activityIndicatorView.startAnimating() + } + + photoFeedModel.updateNewBatchOfPopularPhotos() { additions, connectionStatus in + switch connectionStatus { + case .connected: + self.activityIndicatorView.stopAnimating() + self.addRowsIntoTableNode(newPhotoCount: additions) + context?.completeBatchFetching(true) + case .noConnection: + self.activityIndicatorView.stopAnimating() + context?.completeBatchFetching(true) + } + } + } + + func addRowsIntoTableNode(newPhotoCount newPhotos: Int) { + let indexRange = (photoFeedModel.numberOfItems - newPhotos.. Int { + return photoFeedModel.numberOfItems + } + + func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + let photo = photoFeedModel.itemAtIndexPath(indexPath) + let nodeBlock: ASCellNodeBlock = { _ in + return PhotoTableNodeCell(photoModel: photo) + } + return nodeBlock + } + + func shouldBatchFetchForCollectionNode(collectionNode: ASCollectionNode) -> Bool { + return true + } + + func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { + fetchNewBatchWithContext(context) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift new file mode 100644 index 0000000000..1f19fe8dc9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift @@ -0,0 +1,108 @@ +// +// PhotoFeedTableViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class PhotoFeedTableViewController: UITableViewController { + + var activityIndicator: UIActivityIndicatorView! + var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) + + init() { + super.init(nibName: nil, bundle: nil) + + navigationItem.title = "UIKit" + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationController?.hidesBarsOnSwipe = true + setupActivityIndicator() + configureTableView() + fetchNewBatch() + } + + func fetchNewBatch() { + activityIndicator.startAnimating() + photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in + switch connectionStatus { + case .connected: + self.activityIndicator.stopAnimating() + self.addRowsIntoTableView(newPhotoCount: additions) + case .noConnection: + self.activityIndicator.stopAnimating() + break + } + } + } + + // Helper functions + func setupActivityIndicator() { + let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + self.activityIndicator = activityIndicator + self.tableView.addSubview(activityIndicator) + + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor) + ]) + } + + func configureTableView() { + tableView.register(PhotoTableViewCell.self, forCellReuseIdentifier: "photoCell") + tableView.allowsSelection = false + tableView.rowHeight = UITableViewAutomaticDimension + tableView.separatorStyle = .none + } +} + +extension PhotoFeedTableViewController { + + func addRowsIntoTableView(newPhotoCount newPhotos: Int) { + + let indexRange = (photoFeed.numberOfItems - newPhotos.. Int { + return photoFeed.numberOfItems + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCell", for: indexPath) as? PhotoTableViewCell else { fatalError("Wrong cell type") } + cell.photoModel = photoFeed.itemAtIndexPath(indexPath) + return cell + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return PhotoTableViewCell.height( + for: photoFeed.itemAtIndexPath(indexPath), + withWidth: self.view.frame.size.width + ) + } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + let currentOffSetY = scrollView.contentOffset.y + let contentHeight = scrollView.contentSize.height + let screenHeight = UIScreen.main.bounds.height + let screenfullsBeforeBottom = (contentHeight - currentOffSetY) / screenHeight + if screenfullsBeforeBottom < 2.5 { + self.fetchNewBatch() + } + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift new file mode 100644 index 0000000000..5f5c5bbe71 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift @@ -0,0 +1,133 @@ +// +// PhotoModel.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +// MARK: ProfileImage + +struct ProfileImage: Codable { + let large: String + let medium: String + let small: String +} + +// MARK: UserModel + +struct UserModel: Codable { + let userName: String + let profileImages: ProfileImage + + enum CodingKeys: String, CodingKey { + case userName = "username" + case profileImages = "profile_image" + } +} + +extension UserModel { + var profileImage: String { + return profileImages.medium + } +} + +// MARK: PhotoURL + +struct PhotoURL: Codable { + let full: String + let raw: String + let regular: String + let small: String + let thumb: String +} + +// MARK: PhotoModel + +struct PhotoModel: Codable { + let urls: PhotoURL + let photoID: String + let uploadedDateString: String + let descriptionText: String? + let likesCount: Int + let width: Int + let height: Int + let user: UserModel + + enum CodingKeys: String, CodingKey { + case photoID = "id" + case urls = "urls" + case uploadedDateString = "created_at" + case descriptionText = "description" + case likesCount = "likes" + case width = "width" + case height = "height" + case user = "user" + } +} + +extension PhotoModel { + var url: String { + return urls.regular + } +} + +extension PhotoModel { + + // MARK: - Attributed Strings + + func attributedStringForUserName(withSize size: CGFloat) -> NSAttributedString { + let attributes = [ + NSForegroundColorAttributeName : UIColor.darkGray, + NSFontAttributeName: UIFont.boldSystemFont(ofSize: size) + ] + return NSAttributedString(string: user.userName, attributes: attributes) + } + + func attributedStringForDescription(withSize size: CGFloat) -> NSAttributedString { + let attributes = [ + NSForegroundColorAttributeName : UIColor.darkGray, + NSFontAttributeName: UIFont.systemFont(ofSize: size) + ] + return NSAttributedString(string: descriptionText ?? "", attributes: attributes) + } + + func attributedStringLikes(withSize size: CGFloat) -> NSAttributedString { + guard let formattedLikesNumber = NumberFormatter.decimalNumberFormatter.string(from: NSNumber(value: likesCount)) else { + return NSAttributedString() + } + + let likesAttributes = [ + NSForegroundColorAttributeName : UIColor.mainBarTintColor, + NSFontAttributeName: UIFont.systemFont(ofSize: size) + ] + let likesAttrString = NSAttributedString(string: "\(formattedLikesNumber) Likes", attributes: likesAttributes) + + let heartAttributes = [ + NSForegroundColorAttributeName : UIColor.red, + NSFontAttributeName: UIFont.systemFont(ofSize: size) + ] + let heartAttrString = NSAttributedString(string: "♥︎ ", attributes: heartAttributes) + + let combine = NSMutableAttributedString() + combine.append(heartAttrString) + combine.append(likesAttrString) + return combine + } + + func attributedStringForTimeSinceString(withSize size: CGFloat) -> NSAttributedString { + guard let date = Date.iso8601Formatter.date(from: self.uploadedDateString) else { + return NSAttributedString(); + } + + let attributes = [ + NSForegroundColorAttributeName : UIColor.mainBarTintColor, + NSFontAttributeName: UIFont.systemFont(ofSize: size) + ] + + return NSAttributedString(string: Date.timeStringSince(fromConverted: date), attributes: attributes) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift new file mode 100644 index 0000000000..58f2a7086c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift @@ -0,0 +1,89 @@ +// +// PhotoTableNodeCell.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation +import AsyncDisplayKit + +class PhotoTableNodeCell: ASCellNode { + + // MARK: Properties + + let usernameLabel = ASTextNode() + let timeIntervalLabel = ASTextNode() + let photoLikesLabel = ASTextNode() + let photoDescriptionLabel = ASTextNode() + + let avatarImageNode: ASNetworkImageNode = { + let node = ASNetworkImageNode() + node.contentMode = .scaleAspectFill + // Set the imageModificationBlock for a rounded avatar + node.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil) + return node + }() + + let photoImageNode: ASNetworkImageNode = { + let node = ASNetworkImageNode() + node.contentMode = .scaleAspectFill + return node + }() + + // MARK: Lifecycle + + init(photoModel: PhotoModel) { + super.init() + + automaticallyManagesSubnodes = true + photoImageNode.url = URL(string: photoModel.url) + avatarImageNode.url = URL(string: photoModel.user.profileImage) + usernameLabel.attributedText = photoModel.attributedStringForUserName(withSize: Constants.CellLayout.FontSize) + timeIntervalLabel.attributedText = photoModel.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) + photoLikesLabel.attributedText = photoModel.attributedStringLikes(withSize: Constants.CellLayout.FontSize) + photoDescriptionLabel.attributedText = photoModel.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) + } + + // MARK: ASDisplayNode + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + + // Header Stack + + var headerChildren: [ASLayoutElement] = [] + + let headerStack = ASStackLayoutSpec.horizontal() + headerStack.alignItems = .center + avatarImageNode.style.preferredSize = CGSize( + width: Constants.CellLayout.UserImageHeight, + height: Constants.CellLayout.UserImageHeight + ) + headerChildren.append(ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForAvatar, child: avatarImageNode)) + + usernameLabel.style.flexShrink = 1.0 + headerChildren.append(usernameLabel) + + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1.0 + headerChildren.append(spacer) + + timeIntervalLabel.style.spacingBefore = Constants.CellLayout.HorizontalBuffer + headerChildren.append(timeIntervalLabel) + + let footerStack = ASStackLayoutSpec.vertical() + footerStack.spacing = Constants.CellLayout.VerticalBuffer + footerStack.children = [photoLikesLabel, photoDescriptionLabel] + headerStack.children = headerChildren + + let verticalStack = ASStackLayoutSpec.vertical() + verticalStack.children = [ + ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForHeader, child: headerStack), + ASRatioLayoutSpec(ratio: 1.0, child: photoImageNode), + ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForFooter, child: footerStack) + ] + return verticalStack + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift new file mode 100644 index 0000000000..0698ff2cee --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift @@ -0,0 +1,132 @@ +// +// PhotoTableViewCell.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class PhotoTableViewCell: UITableViewCell { + + var photoModel: PhotoModel? { + didSet { + if let model = photoModel { + photoImageView.loadImageUsingUrlString(urlString: model.url) + avatarImageView.loadImageUsingUrlString(urlString: model.user.profileImage) + photoLikesLabel.attributedText = model.attributedStringLikes(withSize: Constants.CellLayout.FontSize) + usernameLabel.attributedText = model.attributedStringForUserName(withSize: Constants.CellLayout.FontSize) + timeIntervalLabel.attributedText = model.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) + photoDescriptionLabel.attributedText = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) + photoDescriptionLabel.sizeToFit() + var rect = photoDescriptionLabel.frame + let availableWidth = self.bounds.size.width - Constants.CellLayout.HorizontalBuffer * 2 + rect.size = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize).boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size + photoDescriptionLabel.frame = rect + } + } + } + + let photoImageView: NetworkImageView = { + let imageView = NetworkImageView() + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + let avatarImageView: NetworkImageView = { + let imageView = NetworkImageView() + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.layer.cornerRadius = Constants.CellLayout.UserImageHeight / 2 + imageView.clipsToBounds = true + return imageView + }() + + let usernameLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let timeIntervalLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let photoLikesLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let photoDescriptionLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + return label + }() + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupViews() { + addSubview(photoImageView) + addSubview(avatarImageView) + addSubview(usernameLabel) + addSubview(timeIntervalLabel) + addSubview(photoLikesLabel) + addSubview(photoDescriptionLabel) + setupConstraints() + } + + func setupConstraints() { + + NSLayoutConstraint.activate ([ + //photoImageView + photoImageView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.CellLayout.HeaderHeight), + photoImageView.widthAnchor.constraint(equalTo: widthAnchor), + photoImageView.heightAnchor.constraint(equalTo: photoImageView.widthAnchor), + // avatarImageView + avatarImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.CellLayout.HorizontalBuffer), + avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.CellLayout.HorizontalBuffer), + avatarImageView.heightAnchor.constraint(equalToConstant: Constants.CellLayout.UserImageHeight), + avatarImageView.widthAnchor.constraint(equalTo: avatarImageView.heightAnchor), + // usernameLabel + usernameLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: Constants.CellLayout.HorizontalBuffer), + usernameLabel.rightAnchor.constraint(equalTo: timeIntervalLabel.leftAnchor, constant: -Constants.CellLayout.HorizontalBuffer), + usernameLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), + // timeIntervalLabel + timeIntervalLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constants.CellLayout.HorizontalBuffer), + timeIntervalLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), + // photoLikesLabel + photoLikesLabel.topAnchor.constraint(equalTo: photoImageView.bottomAnchor, constant: Constants.CellLayout.VerticalBuffer), + photoLikesLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.CellLayout.HorizontalBuffer), + // photoDescriptionLabel + photoDescriptionLabel.topAnchor.constraint(equalTo: photoLikesLabel.bottomAnchor, constant: Constants.CellLayout.VerticalBuffer), + photoDescriptionLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.CellLayout.HorizontalBuffer), + photoDescriptionLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constants.CellLayout.HorizontalBuffer), + photoDescriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.CellLayout.VerticalBuffer) + ]) + } + + class func height(for photo: PhotoModel, withWidth width: CGFloat) -> CGFloat { + let photoHeight = width + let font = UIFont.systemFont(ofSize: Constants.CellLayout.FontSize) + let likesHeight = round(font.lineHeight) + let descriptionAttrString = photo.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) + let availableWidth = width - Constants.CellLayout.HorizontalBuffer * 2 + let descriptionHeight = descriptionAttrString.boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size.height + + return likesHeight + descriptionHeight + photoHeight + Constants.CellLayout.HeaderHeight + Constants.CellLayout.VerticalBuffer * 3 + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift new file mode 100644 index 0000000000..048b219211 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift @@ -0,0 +1,24 @@ +// +// PopularPageModel.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +struct PopularPageModel { + let page: Int + let totalPages: Int + let totalNumberOfItems: Int + let photos: [PhotoModel] + + init(metaData: ResponseMetadata, photos:[PhotoModel]) { + self.page = metaData.currentPage + self.totalPages = metaData.pagesTotal + self.totalNumberOfItems = metaData.itemsTotal + self.photos = photos + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift new file mode 100644 index 0000000000..5498f07ce9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift @@ -0,0 +1,16 @@ +// +// UIColor.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +extension UIColor { + static var mainBarTintColor: UIColor { + return UIColor(red: 69/255, green: 142/255, blue: 255/255, alpha: 1) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift new file mode 100644 index 0000000000..be9b4a49e9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift @@ -0,0 +1,29 @@ +// +// URL.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +extension URL { + static func URLForFeedModelType(feedModelType: PhotoFeedModelType) -> URL { + switch feedModelType { + case .photoFeedModelTypePopular: + return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.PopularEndpoint))! + + case .photoFeedModelTypeLocation: + return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.SearchEndpoint))! + + case .photoFeedModelTypeUserPhotos: + return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.UserEndpoint))! + } + } + + private static func assembleUnsplashURLString(endpoint: String) -> String { + return Constants.Unsplash.URLS.Host + endpoint + Constants.Unsplash.URLS.ConsumerKey + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift new file mode 100644 index 0000000000..a103d21653 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift @@ -0,0 +1,109 @@ +// +// Webservice.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +final class WebService { + /// Load a new resource. Callback is called on main + func load(resource: Resource, completion: @escaping (Result) -> ()) { + URLSession.shared.dataTask(with: resource.url) { data, response, error in + // Check for errors in responses. + let result = self.checkForNetworkErrors(data, response, error) + DispatchQueue.main.async { + // Parsing should happen off main + switch result { + case .success(let data): + completion(resource.parse(data, response)) + case .failure(let error): + completion(.failure(error)) + } + } + }.resume() + } +} + +extension WebService { + /// // Check for errors in responses. + fileprivate func checkForNetworkErrors(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Result { + if let error = error { + switch error { + case URLError.notConnectedToInternet, URLError.timedOut: + return .failure(.noInternetConnection) + default: + return .failure(.returnedError(error)) + } + } + + if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 { + return .failure((.invalidStatusCode("Request returned status code other than 2xx \(response)"))) + } + + guard let data = data else { + return .failure(.dataReturnedNil) + } + + return .success(data) + } +} + +struct ResponseMetadata { + let currentPage: Int + let itemsTotal: Int + let itemsPerPage: Int +} + +extension ResponseMetadata { + var pagesTotal: Int { + return itemsTotal / itemsPerPage + } +} + +struct Resource { + let url: URL + let parse: (Data, URLResponse?) -> Result +} + +extension Resource { + init(url: URL, page: Int, parseResponse: @escaping (ResponseMetadata, Data) -> Result) { + // Append extra data to url for paging + guard let url = URL(string: url.absoluteString.appending("&page=\(page)")) else { + fatalError("Malformed URL given"); + } + self.url = url + self.parse = { data, response in + // Parse out metadata from header + guard let httpUrlResponse = response as? HTTPURLResponse, + let xTotalString = httpUrlResponse.allHeaderFields["x-total"] as? String, + let xTotal = Int(xTotalString), + let xPerPageString = httpUrlResponse.allHeaderFields["x-per-page"] as? String, + let xPerPage = Int(xPerPageString) + else { + return .failure(.errorParsingResponse) + } + + let metadata = ResponseMetadata(currentPage: page, itemsTotal: xTotal, itemsPerPage: xPerPage) + return parseResponse(metadata, data) + } + } +} + +enum Result { + case success(T) + case failure(NetworkingError) +} + +enum NetworkingError: Error { + case errorParsingResponse + case errorParsingJSON + case noInternetConnection + case dataReturnedNil + case returnedError(Error) + case invalidStatusCode(String) + case customError(String) +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/Podfile b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/Podfile new file mode 100644 index 0000000000..5510b59d9e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASDKgram-Swift/Podfile @@ -0,0 +1,9 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'ASDKgram-Swift' do + use_frameworks! + inhibit_all_warnings! + + pod 'Texture/PINRemoteImage', :path => '../..' + +end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Podfile b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Podfile new file mode 100644 index 0000000000..3b379097a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +use_frameworks! +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..b77a5e410a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,381 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 5E6D34211DB4C9D000FB9B0A /* Sample.h in Headers */ = {isa = PBXBuildFile; fileRef = 5E6D341F1DB4C9D000FB9B0A /* Sample.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FF896945AEA7EF2D9CD93A65 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC767A9EB720B4BF08C89936 /* Pods_Sample.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 5C5154389F056C672F4E9EEA /* 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 = ""; }; + 5E6D341D1DB4C9D000FB9B0A /* Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E6D341F1DB4C9D000FB9B0A /* Sample.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Sample.h; sourceTree = ""; }; + 5E6D34201DB4C9D000FB9B0A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5E6D34251DB4CA8E00FB9B0A /* libPods-Sample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Sample.a"; path = "Pods/../build/Debug-iphoneos/libPods-Sample.a"; sourceTree = ""; }; + 5E6D34271DB4CBAA00FB9B0A /* Sample.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Sample.playground; sourceTree = ""; }; + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + EC767A9EB720B4BF08C89936 /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FDF496F367580DF9280D36EA /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5E6D34191DB4C9D000FB9B0A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FF896945AEA7EF2D9CD93A65 /* Pods_Sample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 5E6D341E1DB4C9D000FB9B0A /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 5E6D341D1DB4C9D000FB9B0A /* Sample.framework */, + ); + name = Products; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5E6D34251DB4CA8E00FB9B0A /* libPods-Sample.a */, + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */, + EC767A9EB720B4BF08C89936 /* Pods_Sample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */, + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */, + FDF496F367580DF9280D36EA /* Pods-Sample.debug.xcconfig */, + 5C5154389F056C672F4E9EEA /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 5E6D341E1DB4C9D000FB9B0A /* Sample */ = { + isa = PBXGroup; + children = ( + 5E6D341F1DB4C9D000FB9B0A /* Sample.h */, + 5E6D34201DB4C9D000FB9B0A /* Info.plist */, + 5E6D34271DB4CBAA00FB9B0A /* Sample.playground */, + ); + path = Sample; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 5E6D341A1DB4C9D000FB9B0A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5E6D34211DB4C9D000FB9B0A /* Sample.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 5E6D341C1DB4C9D000FB9B0A /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5E6D34221DB4C9D000FB9B0A /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 43927A700F47FC31FA2FB429 /* [CP] Check Pods Manifest.lock */, + 5E6D34181DB4C9D000FB9B0A /* Sources */, + 5E6D34191DB4C9D000FB9B0A /* Frameworks */, + 5E6D341A1DB4C9D000FB9B0A /* Headers */, + 5E6D341B1DB4C9D000FB9B0A /* Resources */, + 0FF30A537A157312FD5042F7 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 5E6D341D1DB4C9D000FB9B0A /* Sample.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 5E6D341C1DB4C9D000FB9B0A = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5E6D341C1DB4C9D000FB9B0A /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5E6D341B1DB4C9D000FB9B0A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0FF30A537A157312FD5042F7 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 43927A700F47FC31FA2FB429 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5E6D34181DB4C9D000FB9B0A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5E6D34231DB4C9D000FB9B0A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FDF496F367580DF9280D36EA /* Pods-Sample.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = Sample/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 5E6D34241DB4C9D000FB9B0A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5C5154389F056C672F4E9EEA /* Pods-Sample.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = Sample/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5E6D34221DB4C9D000FB9B0A /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5E6D34231DB4C9D000FB9B0A /* Debug */, + 5E6D34241DB4C9D000FB9B0A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Info.plist new file mode 100644 index 0000000000..fbe1e6b314 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.h b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.h new file mode 100644 index 0000000000..d80265a099 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.h @@ -0,0 +1,19 @@ +// +// Sample.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +//! Project version number for Sample. +FOUNDATION_EXPORT double SampleVersionNumber; + +//! Project version string for Sample. +FOUNDATION_EXPORT const unsigned char SampleVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/HorizontalStackWithSpacer.xcplaygroundpage/Contents.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/HorizontalStackWithSpacer.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000000..43cf09026e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/HorizontalStackWithSpacer.xcplaygroundpage/Contents.swift @@ -0,0 +1,52 @@ +// +// Contents.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +extension HorizontalStackWithSpacer { + + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + usernameNode.style.flexShrink = 1.0 + postLocationNode.style.flexShrink = 1.0 + + let verticalStackSpec = ASStackLayoutSpec.vertical() + verticalStackSpec.style.flexShrink = 1.0 + + // if fetching post location data from server, check if it is available yet + if postLocationNode.attributedText != nil { + verticalStackSpec.children = [usernameNode, postLocationNode] + } else { + verticalStackSpec.children = [usernameNode] + } + + let spacerSpec = ASLayoutSpec() + spacerSpec.style.flexGrow = 1.0 + spacerSpec.style.flexShrink = 1.0 + + // horizontal stack + let horizontalStack = ASStackLayoutSpec.horizontal() + horizontalStack.alignItems = .center // center items vertically in horiz stack + horizontalStack.justifyContent = .start // justify content to left + horizontalStack.style.flexShrink = 1.0 + horizontalStack.style.flexGrow = 1.0 + horizontalStack.children = [verticalStackSpec, spacerSpec, postTimeNode] + + // inset horizontal stack + let insets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) + let headerInsetSpec = ASInsetLayoutSpec(insets: insets, child: horizontalStack) + headerInsetSpec.style.flexShrink = 1.0 + headerInsetSpec.style.flexGrow = 1.0 + + return headerInsetSpec + } + +} + +HorizontalStackWithSpacer().show() + +//: [Index](Index) diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/Index.xcplaygroundpage/Contents.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/Index.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000000..655f86b0f5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/Index.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +// +// Contents.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/PhotoWithInsetTextOverlay.xcplaygroundpage/Contents.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/PhotoWithInsetTextOverlay.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000000..88360fd4fb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/PhotoWithInsetTextOverlay.xcplaygroundpage/Contents.swift @@ -0,0 +1,32 @@ +// +// Contents.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +let userImageHeight = 60 + +extension PhotoWithInsetTextOverlay { + + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + photoNode.style.preferredSize = CGSize(width: userImageHeight * 2, height: userImageHeight * 2) + let backgroundImageAbsoluteSpec = ASAbsoluteLayoutSpec(children: [photoNode]) + + let insets = UIEdgeInsets(top: CGFloat.infinity, left: 12, bottom: 12, right: 12) + let textInsetSpec = ASInsetLayoutSpec(insets: insets, + child: titleNode) + + let textOverlaySpec = ASOverlayLayoutSpec(child: backgroundImageAbsoluteSpec, overlay: textInsetSpec) + + return textOverlaySpec + } + +} + +PhotoWithInsetTextOverlay().show() + +//: [Photo With Outset Icon Overlay](PhotoWithOutsetIconOverlay) diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/PhotoWithOutsetIconOverlay.xcplaygroundpage/Contents.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/PhotoWithOutsetIconOverlay.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000000..ba866f3b6d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/PhotoWithOutsetIconOverlay.xcplaygroundpage/Contents.swift @@ -0,0 +1,34 @@ +// +// Contents.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit + +extension PhotoWithOutsetIconOverlay { + + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let iconWidth: CGFloat = 40 + let iconHeight: CGFloat = 40 + + iconNode.style.preferredSize = CGSize(width: iconWidth, height: iconWidth) + photoNode.style.preferredSize = CGSize(width: 150, height: 150) + + let x: CGFloat = 150 + let y: CGFloat = 0 + + iconNode.style.layoutPosition = CGPoint(x: x, y: y) + photoNode.style.layoutPosition = CGPoint(x: iconWidth * 0.5, y: iconHeight * 0.5); + + let absoluteLayoutSpec = ASAbsoluteLayoutSpec(children: [photoNode, iconNode]) + return absoluteLayoutSpec; + } + +} + +PhotoWithOutsetIconOverlay().show() + +//: [Horizontal Stack With Spacer](HorizontalStackWithSpacer) diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/StackLayout.xcplaygroundpage/Contents.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/StackLayout.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000000..ec63def40a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Pages/StackLayout.xcplaygroundpage/Contents.swift @@ -0,0 +1,34 @@ +// +// Contents.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import AsyncDisplayKit + +extension StackLayout { + + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + // Try commenting out the flexShrink to see its consequences. + subtitleNode.style.flexShrink = 1.0 + + let stackSpec = ASStackLayoutSpec(direction: .horizontal, + spacing: 5, + justifyContent: .start, + alignItems: .start, + children: [titleNode, subtitleNode]) + + let insetSpec = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 5, + left: 5, + bottom: 5, + right: 5), + child: stackSpec) + return insetSpec + } + +} + +StackLayout().show() + +//: [Photo With Inset Text Overlay](PhotoWithInsetTextOverlay) diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/ASPlayground.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/ASPlayground.swift new file mode 100644 index 0000000000..1edf7db0bb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/ASPlayground.swift @@ -0,0 +1,33 @@ +// +// ASPlayground.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import PlaygroundSupport +import AsyncDisplayKit + +public protocol ASPlayground: class { + func display(inRect: CGRect) +} + +extension ASPlayground { + public func display(inRect rect: CGRect) { + var rect = rect + if rect.size == .zero { + rect.size = CGSize(width: 400, height: 400) + } + + guard let nodeSelf = self as? ASDisplayNode else { + assertionFailure("Class inheriting ASPlayground must be an ASDisplayNode") + return + } + + let constrainedSize = ASSizeRange(min: rect.size, max: rect.size) + _ = ASCalculateRootLayout(nodeSelf, constrainedSize) + nodeSelf.frame = rect + PlaygroundPage.current.needsIndefiniteExecution = true + PlaygroundPage.current.liveView = nodeSelf.view + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/HorizontalStackWithSpacer.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/HorizontalStackWithSpacer.swift new file mode 100644 index 0000000000..84b38c8e4d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/HorizontalStackWithSpacer.swift @@ -0,0 +1,45 @@ +// +// HorizontalStackWithSpacer.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import AsyncDisplayKit + +fileprivate let fontSize: CGFloat = 20 + +public class HorizontalStackWithSpacer: ASDisplayNode, ASPlayground { + public let usernameNode = ASTextNode() + public let postLocationNode = ASTextNode() + public let postTimeNode = ASTextNode() + + override public init() { + super.init() + backgroundColor = .white + + automaticallyManagesSubnodes = true + setupNodes() + } + + private func setupNodes() { + usernameNode.backgroundColor = .yellow + usernameNode.attributedText = NSAttributedString.attributedString(string: "hannahmbanana", fontSize: fontSize, color: .darkBlueColor(), firstWordColor: nil) + + postLocationNode.backgroundColor = .lightGray + postLocationNode.maximumNumberOfLines = 1; + postLocationNode.attributedText = NSAttributedString.attributedString(string: "San Fransisco, CA", fontSize: fontSize, color: .lightBlueColor(), firstWordColor: nil) + + postTimeNode.backgroundColor = .brown + postTimeNode.attributedText = NSAttributedString.attributedString(string: "30m", fontSize: fontSize, color: .lightGray, firstWordColor: nil) + } + + // This is used to expose this function for overriding in extensions + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASLayoutSpec() + } + + public func show() { + display(inRect: CGRect(x: 0, y: 0, width: 450, height: 100)) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/PhotoWithInsetTextOverlay.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/PhotoWithInsetTextOverlay.swift new file mode 100644 index 0000000000..2a9736446c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/PhotoWithInsetTextOverlay.swift @@ -0,0 +1,40 @@ +// +// PhotoWithInsetTextOverlay.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import AsyncDisplayKit + +public class PhotoWithInsetTextOverlay: ASDisplayNode, ASPlayground { + public let photoNode = ASNetworkImageNode() + public let titleNode = ASTextNode() + + override public init() { + super.init() + backgroundColor = .white + + automaticallyManagesSubnodes = true + setupNodes() + } + + private func setupNodes() { + photoNode.url = URL(string: "http://asyncdisplaykit.org/static/images/layout-examples-photo-with-inset-text-overlay-photo.png") + photoNode.backgroundColor = .black + + titleNode.backgroundColor = .blue + titleNode.maximumNumberOfLines = 2 + titleNode.truncationAttributedText = NSAttributedString.attributedString(string: "...", fontSize: 16, color: .white, firstWordColor: nil) + titleNode.attributedText = NSAttributedString.attributedString(string: "family fall hikes", fontSize: 16, color: .white, firstWordColor: nil) + } + + // This is used to expose this function for overriding in extensions + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASLayoutSpec() + } + + public func show() { + display(inRect: CGRect(x: 0, y: 0, width: 120, height: 120)) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/PhotoWithOutsetIconOverlay.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/PhotoWithOutsetIconOverlay.swift new file mode 100644 index 0000000000..9a8c9f3ab3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/PhotoWithOutsetIconOverlay.swift @@ -0,0 +1,40 @@ +// +// PhotoWithOutsetIconOverlay.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import AsyncDisplayKit + +fileprivate let userImageHeight = 60 + +public class PhotoWithOutsetIconOverlay: ASDisplayNode, ASPlayground { + public let photoNode = ASNetworkImageNode() + public let iconNode = ASNetworkImageNode() + + override public init() { + super.init() + backgroundColor = .white + + automaticallyManagesSubnodes = true + setupNodes() + } + + private func setupNodes() { + photoNode.url = URL(string: "http://asyncdisplaykit.org/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png") + photoNode.backgroundColor = .black + + iconNode.url = URL(string: "http://asyncdisplaykit.org/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png") + iconNode.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(10, .white) + } + + // This is used to expose this function for overriding in extensions + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASLayoutSpec() + } + + public func show() { + display(inRect: CGRect(x: 0, y: 0, width: 190, height: 190)) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/StackLayout.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/StackLayout.swift new file mode 100644 index 0000000000..871252e0fc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/StackLayout.swift @@ -0,0 +1,38 @@ +// +// StackLayout.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import AsyncDisplayKit + +public class StackLayout: ASDisplayNode, ASPlayground { + public let titleNode = ASTextNode() + public let subtitleNode = ASTextNode() + + override public init() { + super.init() + backgroundColor = .white + + automaticallyManagesSubnodes = true + setupNodes() + } + + private func setupNodes() { + titleNode.backgroundColor = .blue + titleNode.attributedText = NSAttributedString.attributedString(string: "Headline!", fontSize: 14, color: .white, firstWordColor: nil) + + subtitleNode.backgroundColor = .yellow + subtitleNode.attributedText = NSAttributedString(string: "Lorem ipsum dolor sit amet, sed ex laudem utroque meliore, at cum lucilius vituperata. Ludus mollis consulatu mei eu, esse vocent epicurei sed at. Ut cum recusabo prodesset. Ut cetero periculis sed, mundi senserit est ut. Nam ut sonet mandamus intellegebat, summo voluptaria vim ad.") + } + + // This is used to expose this function for overriding in extensions + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASLayoutSpec() + } + + public func show() { + display(inRect: .zero) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/Utilities.swift b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/Utilities.swift new file mode 100644 index 0000000000..ab2cccc798 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/Sources/Utilities.swift @@ -0,0 +1,50 @@ +// +// Utilities.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// +import UIKit +import Foundation + +extension UIColor { + + static func darkBlueColor() -> UIColor { + return UIColor(red: 18.0/255.0, green: 86.0/255.0, blue: 136.0/255.0, alpha: 1.0) + } + + + static func lightBlueColor() -> UIColor { + return UIColor(red: 0.0, green: 122.0/255.0, blue: 1.0, alpha: 1.0) + } + + static func duskColor() -> UIColor { + return UIColor(red: 255/255.0, green: 181/255.0, blue: 68/255.0, alpha: 1.0) + } + + static func customOrangeColor() -> UIColor { + return UIColor(red: 40/255.0, green: 43/255.0, blue: 53/255.0, alpha: 1.0) + } + +} + +extension NSAttributedString { + + static func attributedString(string: String, fontSize size: CGFloat, color: UIColor?, firstWordColor: UIColor?) -> NSAttributedString { + let attributes = [NSForegroundColorAttributeName: color ?? UIColor.black, + NSFontAttributeName: UIFont.boldSystemFont(ofSize: size)] + + let attributedString = NSMutableAttributedString(string: string, attributes: attributes) + + if let firstWordColor = firstWordColor { + let nsString = string as NSString + let firstSpaceRange = nsString.rangeOfCharacter(from: NSCharacterSet.whitespaces) + let firstWordRange = NSMakeRange(0, firstSpaceRange.location) + attributedString.addAttribute(NSForegroundColorAttributeName, value: firstWordColor, range: firstWordRange) + } + + return attributedString + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/contents.xcplayground b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/contents.xcplayground new file mode 100644 index 0000000000..c7f819f0e3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASLayoutSpecPlayground-Swift/Sample/Sample.playground/contents.xcplayground @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Podfile b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c0da5a2015 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,364 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 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 */; }; + 881AF5D3D4458C15BACC8930 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D65D3016E9D596BDDD17FA44 /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0431779F19E096F3CEC4D269 /* 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 = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 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; }; + D65D3016E9D596BDDD17FA44 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DBA49A0CCF4CA8FC1F96CB6D /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 881AF5D3D4458C15BACC8930 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + D65D3016E9D596BDDD17FA44 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + DBA49A0CCF4CA8FC1F96CB6D /* Pods-Sample.debug.xcconfig */, + 0431779F19E096F3CEC4D269 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 75CADB9ECE58AB74892E1D67 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 75CADB9ECE58AB74892E1D67 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DBA49A0CCF4CA8FC1F96CB6D /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0431779F19E096F3CEC4D269 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..5c91bfc64d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/AppDelegate.m new file mode 100644 index 0000000000..1a89581c33 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/AppDelegate.m @@ -0,0 +1,47 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +#import +#import + +@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 = [[UINavigationController alloc] init]; + + [self pushNewViewControllerAnimated:NO]; + + [self.window makeKeyAndVisible]; + + return YES; +} + +- (void)pushNewViewControllerAnimated:(BOOL)animated +{ + UINavigationController *navController = (UINavigationController *)self.window.rootViewController; + + UIViewController *viewController = [[ViewController alloc] init]; + viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Another Copy" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; + + [navController pushViewController:viewController animated:animated]; +} + +- (void)pushNewViewController +{ + [self pushNewViewControllerAnimated:YES]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/Info.plist new file mode 100644 index 0000000000..ad825d6e33 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/ViewController.m new file mode 100644 index 0000000000..d75f9ac3ef --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/ViewController.m @@ -0,0 +1,192 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import + +#define NumberOfSections 10 +#define NumberOfRowsPerSection 20 +#define NumberOfReloadIterations 50 + +typedef enum : NSUInteger { + ReloadData, + ReloadRows, + ReloadSections, + ReloadTypeMax +} ReloadType; + +@interface ViewController () +{ + ASTableView *_tableView; + NSMutableArray *_sections; // Contains arrays of indexPaths representing rows +} + +@end + + +@implementation ViewController + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.asyncDataSource = self; + _tableView.asyncDelegate = self; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + _sections = [NSMutableArray arrayWithCapacity:NumberOfSections]; + for (int i = 0; i < NumberOfSections; i++) { + NSMutableArray *rowsArray = [NSMutableArray arrayWithCapacity:NumberOfRowsPerSection]; + for (int j = 0; j < NumberOfRowsPerSection; j++) { + [rowsArray addObject:[NSIndexPath indexPathForRow:j inSection:i]]; + } + [_sections addObject:rowsArray]; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_tableView]; +} + +- (void)viewWillLayoutSubviews +{ + _tableView.frame = self.view.bounds; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + [self thrashTableView]; +} + +- (NSIndexSet *)randomIndexSet +{ + u_int32_t upperBound = (u_int32_t)_sections.count - 1; + u_int32_t randA = arc4random_uniform(upperBound); + u_int32_t randB = arc4random_uniform(upperBound); + + return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))]; +} + +- (NSArray *)randomIndexPathsExisting:(BOOL)existing +{ + NSMutableArray *indexPaths = [NSMutableArray array]; + [[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = [self tableView:_tableView numberOfRowsInSection:idx]; + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) { + // Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows + if (existing && arc4random_uniform(2) == 0) { + continue; + } + + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + } + }]; + return indexPaths; +} + +- (void)thrashTableView +{ + [_tableView reloadData]; + + NSArray *indexPathsAddedAndRemoved = nil; + + for (int i = 0; i < NumberOfReloadIterations; ++i) { + UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); + + BOOL animatedScroll = (arc4random_uniform(2) == 0 ? YES : NO); + ReloadType reloadType = (arc4random_uniform(ReloadTypeMax)); + BOOL letRunloopProceed = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL useBeginEndUpdates = (arc4random_uniform(3) == 0 ? YES : NO); + + // FIXME: Need to revise the logic to support mutating the data source rather than just reload thrashing. + // UITableView itself does not support deleting a row in the same edit transaction as reloading it, for example. + BOOL addIndexPaths = NO; //(arc4random_uniform(2) == 0 ? YES : NO); + + if (useBeginEndUpdates) { + [_tableView beginUpdates]; + } + + switch (reloadType) { + case ReloadData: + [_tableView reloadData]; + break; + + case ReloadRows: + [_tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation]; + break; + + case ReloadSections: + [_tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation]; + break; + + default: + break; + } + + if (addIndexPaths && !indexPathsAddedAndRemoved) { + indexPathsAddedAndRemoved = [self randomIndexPathsExisting:NO]; + for (NSIndexPath *indexPath in indexPathsAddedAndRemoved) { + [_sections[indexPath.section] addObject:indexPath]; + } + [_tableView insertRowsAtIndexPaths:indexPathsAddedAndRemoved withRowAnimation:rowAnimation]; + } + + [_tableView setContentOffset:CGPointMake(0, arc4random_uniform(_tableView.contentSize.height - _tableView.bounds.size.height)) animated:animatedScroll]; + + if (letRunloopProceed) { + // Run other stuff on the main queue for between 2ms and 1000ms. + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]]; + + if (indexPathsAddedAndRemoved) { + for (NSIndexPath *indexPath in indexPathsAddedAndRemoved) { + [_sections[indexPath.section] removeObjectIdenticalTo:indexPath]; + } + [_tableView deleteRowsAtIndexPaths:indexPathsAddedAndRemoved withRowAnimation:rowAnimation]; + indexPathsAddedAndRemoved = nil; + } + } + + if (useBeginEndUpdates) { + [_tableView endUpdates]; + } + } +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return _sections.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return [(NSArray *)[_sections objectAtIndex:section] count]; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + ASTextCellNode *textCellNode = [ASTextCellNode new]; + textCellNode.text = indexPath.description; + + return textCellNode; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTableViewStressTest/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Podfile b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..ca9ec160eb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 1BEECAB53F4B61DCB949ED44 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EEDFC574739077BA65E0CF5 /* libPods-Sample.a */; }; + 9C37D01E1CC94BC9004C8BC1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9C37D01D1CC94BC9004C8BC1 /* Launch Screen.storyboard */; }; + 9CACC7811CCEAF9E009A1613 /* TableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CACC7801CCEAF9E009A1613 /* TableViewController.m */; }; + 9CACC7841CCEAFAE009A1613 /* CollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CACC7831CCEAFAE009A1613 /* CollectionViewController.m */; }; + 9CACC7871CCEBD3B009A1613 /* KittenNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CACC7861CCEBD3B009A1613 /* KittenNode.m */; }; + 9CACC78A1CCEC82C009A1613 /* OverrideViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CACC7891CCEC82C009A1613 /* OverrideViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 056298286C03B7760575CC56 /* 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 = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 1EEDFC574739077BA65E0CF5 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C37D01D1CC94BC9004C8BC1 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + 9CACC77F1CCEAF9E009A1613 /* TableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TableViewController.h; sourceTree = ""; }; + 9CACC7801CCEAF9E009A1613 /* TableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TableViewController.m; sourceTree = ""; }; + 9CACC7821CCEAFAE009A1613 /* CollectionViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CollectionViewController.h; sourceTree = ""; }; + 9CACC7831CCEAFAE009A1613 /* CollectionViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CollectionViewController.m; sourceTree = ""; }; + 9CACC7851CCEBD3B009A1613 /* KittenNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KittenNode.h; sourceTree = ""; }; + 9CACC7861CCEBD3B009A1613 /* KittenNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KittenNode.m; sourceTree = ""; }; + 9CACC7881CCEC82C009A1613 /* OverrideViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OverrideViewController.h; sourceTree = ""; }; + 9CACC7891CCEC82C009A1613 /* OverrideViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OverrideViewController.m; sourceTree = ""; }; + A7F0013FBBCBEA0C9FB68986 /* 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 = ""; }; + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BEECAB53F4B61DCB949ED44 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + 9CACC77F1CCEAF9E009A1613 /* TableViewController.h */, + 9CACC7801CCEAF9E009A1613 /* TableViewController.m */, + 9CACC7821CCEAFAE009A1613 /* CollectionViewController.h */, + 9CACC7831CCEAFAE009A1613 /* CollectionViewController.m */, + 9CACC7851CCEBD3B009A1613 /* KittenNode.h */, + 9CACC7861CCEBD3B009A1613 /* KittenNode.m */, + 9CACC7881CCEC82C009A1613 /* OverrideViewController.h */, + 9CACC7891CCEC82C009A1613 /* OverrideViewController.m */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + 9C37D01D1CC94BC9004C8BC1 /* Launch Screen.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */, + 1EEDFC574739077BA65E0CF5 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */, + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */, + 056298286C03B7760575CC56 /* Pods-Sample.debug.xcconfig */, + A7F0013FBBCBEA0C9FB68986 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */, + FFF65E837E66ADA71296F0FF /* 📦 Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C37D01E1CC94BC9004C8BC1 /* Launch Screen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + FFF65E837E66ADA71296F0FF /* 📦 Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 9CACC78A1CCEC82C009A1613 /* OverrideViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + 9CACC7841CCEAFAE009A1613 /* CollectionViewController.m in Sources */, + 9CACC7871CCEBD3B009A1613 /* KittenNode.m in Sources */, + 9CACC7811CCEAF9E009A1613 /* TableViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 056298286C03B7760575CC56 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A7F0013FBBCBEA0C9FB68986 /* Pods-Sample.release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/AppDelegate.m new file mode 100644 index 0000000000..0a4654ae14 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/AppDelegate.m @@ -0,0 +1,29 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" +#import "TableViewController.h" +#import "CollectionViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + UITabBarController *tabController = [[UITabBarController alloc] init]; + [tabController setViewControllers:@[[[ViewController alloc] init], [[TableViewController alloc] init], [[CollectionViewController alloc] init]]]; + self.window.rootViewController = tabController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/CollectionViewController.h b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/CollectionViewController.h new file mode 100644 index 0000000000..0528310b79 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/CollectionViewController.h @@ -0,0 +1,13 @@ +// +// CollectionViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface CollectionViewController : ASViewController +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/CollectionViewController.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/CollectionViewController.m new file mode 100644 index 0000000000..7e57f74434 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/CollectionViewController.m @@ -0,0 +1,71 @@ +// +// CollectionViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "CollectionViewController.h" +#import "KittenNode.h" +#import + +@interface CollectionViewController () +@property (nonatomic, strong) ASCollectionNode *collectionNode; +@end + +@implementation CollectionViewController + +- (instancetype)init +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.minimumLineSpacing = 10; + layout.minimumInteritemSpacing = 10; + + ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + + if (!(self = [super initWithNode:collectionNode])) + return nil; + + self.title = @"Collection Node"; + _collectionNode = collectionNode; + collectionNode.dataSource = self; + collectionNode.delegate = self; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.collectionNode.view.contentInset = UIEdgeInsetsMake(20, 10, CGRectGetHeight(self.tabBarController.tabBar.frame), 10); +} + +#pragma mark - ASCollectionDataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return 50; +} + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + KittenNode *cell = [[KittenNode alloc] init]; + cell.textNode.maximumNumberOfLines = 3; + cell.imageTappedBlock = ^{ + [KittenNode defaultImageTappedAction:self]; + }; + return cell; +} + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + ASTraitCollection *traitCollection = [self.collectionNode asyncTraitCollection]; + + if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) { + return ASSizeRangeMake(CGSizeMake(200, 120), CGSizeMake(200, 120)); + } + return ASSizeRangeMake(CGSizeMake(132, 180), CGSizeMake(132, 180)); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/Info.plist new file mode 100644 index 0000000000..acc713cc71 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/Info.plist @@ -0,0 +1,39 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launch Screen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/KittenNode.h b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/KittenNode.h new file mode 100644 index 0000000000..17655e0765 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/KittenNode.h @@ -0,0 +1,21 @@ +// +// KittenNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface KittenNode : ASCellNode +@property (nonatomic, strong, readonly) ASNetworkImageNode *imageNode; +@property (nonatomic, strong, readonly) ASTextNode *textNode; + +@property (nonatomic, copy) dispatch_block_t imageTappedBlock; + +// The default action when an image node is tapped. This action will create an +// OverrideVC and override its display traits to always be compact. ++ (void)defaultImageTappedAction:(ASViewController *)sourceViewController; +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/KittenNode.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/KittenNode.m new file mode 100644 index 0000000000..1526563e04 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/KittenNode.m @@ -0,0 +1,167 @@ +// +// KittenNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "KittenNode.h" +#import "OverrideViewController.h" + +#import + +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + +@interface KittenNode () +{ + CGSize _kittenSize; +} + +@end + + +@implementation KittenNode + +// lorem ipsum text courtesy https://kittyipsum.com/ <3 ++ (NSArray *)placeholders +{ + static NSArray *placeholders = nil; + + static dispatch_once_t once; + dispatch_once(&once, ^{ + placeholders = @[ + @"Kitty ipsum dolor sit amet, purr sleep on your face lay down in your way biting, sniff tincidunt a etiam fluffy fur judging you stuck in a tree kittens.", + @"Lick tincidunt a biting eat the grass, egestas enim ut lick leap puking climb the curtains lick.", + @"Lick quis nunc toss the mousie vel, tortor pellentesque sunbathe orci turpis non tail flick suscipit sleep in the sink.", + @"Orci turpis litter box et stuck in a tree, egestas ac tempus et aliquam elit.", + @"Hairball iaculis dolor dolor neque, nibh adipiscing vehicula egestas dolor aliquam.", + @"Sunbathe fluffy fur tortor faucibus pharetra jump, enim jump on the table I don't like that food catnip toss the mousie scratched.", + @"Quis nunc nam sleep in the sink quis nunc purr faucibus, chase the red dot consectetur bat sagittis.", + @"Lick tail flick jump on the table stretching purr amet, rhoncus scratched jump on the table run.", + @"Suspendisse aliquam vulputate feed me sleep on your keyboard, rip the couch faucibus sleep on your keyboard tristique give me fish dolor.", + @"Rip the couch hiss attack your ankles biting pellentesque puking, enim suspendisse enim mauris a.", + @"Sollicitudin iaculis vestibulum toss the mousie biting attack your ankles, puking nunc jump adipiscing in viverra.", + @"Nam zzz amet neque, bat tincidunt a iaculis sniff hiss bibendum leap nibh.", + @"Chase the red dot enim puking chuf, tristique et egestas sniff sollicitudin pharetra enim ut mauris a.", + @"Sagittis scratched et lick, hairball leap attack adipiscing catnip tail flick iaculis lick.", + @"Neque neque sleep in the sink neque sleep on your face, climb the curtains chuf tail flick sniff tortor non.", + @"Ac etiam kittens claw toss the mousie jump, pellentesque rhoncus litter box give me fish adipiscing mauris a.", + @"Pharetra egestas sunbathe faucibus ac fluffy fur, hiss feed me give me fish accumsan.", + @"Tortor leap tristique accumsan rutrum sleep in the sink, amet sollicitudin adipiscing dolor chase the red dot.", + @"Knock over the lamp pharetra vehicula sleep on your face rhoncus, jump elit cras nec quis quis nunc nam.", + @"Sollicitudin feed me et ac in viverra catnip, nunc eat I don't like that food iaculis give me fish.", + ]; + }); + + return placeholders; +} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _kittenSize = CGSizeMake(100,100); + + // kitten image, with a solid background colour serving as placeholder + _imageNode = [[ASNetworkImageNode alloc] init]; + _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _imageNode.style.preferredSize = _kittenSize; + [_imageNode addTarget:self action:@selector(imageTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; + + CGFloat scale = [UIScreen mainScreen].scale; + _imageNode.URL = [NSURL URLWithString:[NSString stringWithFormat:@"https://placekitten.com/%zd/%zd?image=%zd", + (NSInteger)roundl(_kittenSize.width * scale), + (NSInteger)roundl(_kittenSize.height * scale), + (NSInteger)arc4random_uniform(20)]]; + [self addSubnode:_imageNode]; + + // lorem ipsum text, plus some nice styling + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:[self kittyIpsum] + attributes:[self textStyle]]; + _textNode.style.flexShrink = 1.0; + _textNode.style.flexGrow = 1.0; + [self addSubnode:_textNode]; + + return self; +} + +- (void)imageTapped:(id)sender +{ + if (self.imageTappedBlock) { + self.imageTappedBlock(); + } +} + +- (NSString *)kittyIpsum +{ + NSArray *placeholders = [KittenNode placeholders]; + u_int32_t ipsumCount = (u_int32_t)[placeholders count]; + u_int32_t location = arc4random_uniform(ipsumCount); + u_int32_t length = arc4random_uniform(ipsumCount - location); + + NSMutableString *string = [placeholders[location] mutableCopy]; + for (u_int32_t i = location + 1; i < location + length; i++) { + [string appendString:(i % 2 == 0) ? @"\n" : @" "]; + [string appendString:placeholders[i]]; + } + + return string; +} + +- (NSDictionary *)textStyle +{ + UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:12.0f]; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + style.paragraphSpacing = 0.5 * font.lineHeight; + style.hyphenationFactor = 1.0; + + return @{ NSFontAttributeName: font, + NSParagraphStyleAttributeName: style, + ASTextNodeWordKerningAttributeName : @.5}; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASStackLayoutSpec *stackSpec = [[ASStackLayoutSpec alloc] init]; + stackSpec.spacing = kInnerPadding; + [stackSpec setChildren:@[_imageNode, _textNode]]; + + if (self.asyncTraitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) { + _imageNode.style.alignSelf = ASStackLayoutAlignSelfStart; + stackSpec.direction = ASStackLayoutDirectionHorizontal; + } else { + _imageNode.style.alignSelf = ASStackLayoutAlignSelfCenter; + stackSpec.direction = ASStackLayoutDirectionVertical; + } + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding) child:stackSpec]; +} + ++ (void)defaultImageTappedAction:(ASViewController *)sourceViewController +{ + OverrideViewController *overrideVC = [[OverrideViewController alloc] init]; + + __weak OverrideViewController *weakOverrideVC = overrideVC; + overrideVC.overrideDisplayTraitsWithTraitCollection = ^(UITraitCollection *traitCollection) { + ASTraitCollection *asyncTraitCollection = [ASTraitCollection traitCollectionWithDisplayScale:traitCollection.displayScale + userInterfaceIdiom:traitCollection.userInterfaceIdiom + horizontalSizeClass:UIUserInterfaceSizeClassCompact + verticalSizeClass:UIUserInterfaceSizeClassCompact + forceTouchCapability:traitCollection.forceTouchCapability + containerSize:weakOverrideVC.view.bounds.size]; + return asyncTraitCollection; + }; + + [sourceViewController presentViewController:overrideVC animated:YES completion:nil]; + overrideVC.closeBlock = ^{ + [sourceViewController dismissViewControllerAnimated:YES completion:nil]; + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/Launch Screen.storyboard b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/Launch Screen.storyboard new file mode 100644 index 0000000000..95c8ef474d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/Launch Screen.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/OverrideViewController.h b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/OverrideViewController.h new file mode 100644 index 0000000000..f5de270ae5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/OverrideViewController.h @@ -0,0 +1,27 @@ +// +// OverrideViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/* + * A simple node that displays the attribution for the kitties in the app. Note that + * for a regular horizontal size class it does something stupid and sets the font size to 100. + * It's VC, OverrideViewController, will have its display traits overridden such that + * it will always have a compact horizontal size class. + */ +@interface OverrideNode : ASDisplayNode +@end + +/* + * This is a fairly stupid VC that's main purpose is to show how to override ASDisplayTraits. + * Take a look at `defaultImageTappedAction` in KittenNode to see how this is accomplished. + */ +@interface OverrideViewController : ASViewController +@property (nonatomic, copy) dispatch_block_t closeBlock; +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/OverrideViewController.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/OverrideViewController.m new file mode 100644 index 0000000000..3e9fcc305e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/OverrideViewController.m @@ -0,0 +1,95 @@ +// +// OverrideViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "OverrideViewController.h" +#import + +static NSString *kLinkAttributeName = @"PlaceKittenNodeLinkAttributeName"; + +@interface OverrideNode() +@property (nonatomic, strong) ASTextNode *textNode; +@property (nonatomic, strong) ASButtonNode *buttonNode; +@end + +@implementation OverrideNode + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _textNode = [[ASTextNode alloc] init]; + _textNode.style.flexGrow = 1.0; + _textNode.style.flexShrink = 1.0; + _textNode.maximumNumberOfLines = 3; + [self addSubnode:_textNode]; + + _buttonNode = [[ASButtonNode alloc] init]; + [_buttonNode setAttributedTitle:[[NSAttributedString alloc] initWithString:@"Close"] forState:UIControlStateNormal]; + [self addSubnode:_buttonNode]; + + self.backgroundColor = [UIColor lightGrayColor]; + + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGFloat pointSize = 16.f; + ASTraitCollection *traitCollection = [self asyncTraitCollection]; + if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) { + // This should never happen because we override the VC's display traits to always be compact. + pointSize = 100; + } + + NSString *blurb = @"kittens courtesy placekitten.com"; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue" size:pointSize] range:NSMakeRange(0, blurb.length)]; + [string addAttributes:@{ + kLinkAttributeName: [NSURL URLWithString:@"http://placekitten.com/"], + NSForegroundColorAttributeName: [UIColor grayColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } + range:[blurb rangeOfString:@"placekitten.com"]]; + + _textNode.attributedText = string; + + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + stackSpec.children = @[_textNode, _buttonNode]; + stackSpec.spacing = 10; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(40, 20, 20, 20) child:stackSpec]; +} + +@end + +@interface OverrideViewController () + +@end + +@implementation OverrideViewController + +- (instancetype)init +{ + OverrideNode *overrideNode = [[OverrideNode alloc] init]; + + if (!(self = [super initWithNode:overrideNode])) + return nil; + + [overrideNode.buttonNode addTarget:self action:@selector(closeTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; + return self; +} + +- (void)closeTapped:(id)sender +{ + if (self.closeBlock) { + self.closeBlock(); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/TableViewController.h b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/TableViewController.h new file mode 100644 index 0000000000..bfb5fad618 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/TableViewController.h @@ -0,0 +1,14 @@ +// +// TableViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface TableViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/TableViewController.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/TableViewController.m new file mode 100644 index 0000000000..3a91be7588 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/TableViewController.m @@ -0,0 +1,60 @@ +// +// TableViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "TableViewController.h" +#import "KittenNode.h" + +@interface TableViewController () +@property (nonatomic, strong) ASTableNode *tableNode; +@end + +@implementation TableViewController + +- (instancetype)init +{ + ASTableNode *tableNode = [[ASTableNode alloc] init]; + if (!(self = [super initWithNode:tableNode])) + return nil; + + _tableNode = tableNode; + tableNode.delegate = self; + tableNode.dataSource = self; + self.title = @"Table Node"; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.tableNode.view.contentInset = UIEdgeInsetsMake(CGRectGetHeight([[UIApplication sharedApplication] statusBarFrame]), 0, CGRectGetHeight(self.tabBarController.tabBar.frame), 0); +} + +#pragma mark - +#pragma mark ASTableView. + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + KittenNode *cell = [[KittenNode alloc] init]; + cell.imageTappedBlock = ^{ + [KittenNode defaultImageTappedAction:self]; + }; + return cell; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return 15; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/ViewController.h new file mode 100644 index 0000000000..4499f93bab --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/ViewController.m new file mode 100644 index 0000000000..ddb1d708f2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/ViewController.m @@ -0,0 +1,43 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import "KittenNode.h" +#import "OverrideViewController.h" + +#import +#import + +@interface ViewController () +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + KittenNode *displayNode = [[KittenNode alloc] init]; + if (!(self = [super initWithNode:displayNode])) + return nil; + + self.title = @"Display Node"; + displayNode.imageTappedBlock = ^{ + [KittenNode defaultImageTappedAction:self]; + }; + return self; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/ASTraitCollection/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Podfile b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Podfile new file mode 100644 index 0000000000..3b379097a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +use_frameworks! +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6a163e01f2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,370 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + BCDB7EDE9701EB3DD88BCDA0 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DD2D199AD8BD92717ED9783 /* Pods_Sample.framework */; }; + CCB8301E1C7688B500847D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCB8301D1C7688B500847D42 /* Assets.xcassets */; }; + CCB830211C7688B500847D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CCB8301F1C7688B500847D42 /* LaunchScreen.storyboard */; }; + CCB8302C1C7688EC00847D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB830281C7688EC00847D42 /* AppDelegate.swift */; }; + CCB8302D1C7688EC00847D42 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB830291C7688EC00847D42 /* ViewController.swift */; }; + CCB8302E1C7688EC00847D42 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB8302A1C7688EC00847D42 /* Utilities.swift */; }; + CCB8302F1C7688EC00847D42 /* DemoCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB8302B1C7688EC00847D42 /* DemoCellNode.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0C1B25A26B6D6815A16D0911 /* 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 = ""; }; + 46B216EB47D6586F63D99B86 /* 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 = ""; }; + 9DD2D199AD8BD92717ED9783 /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CCB830131C7688B500847D42 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CCB8301D1C7688B500847D42 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CCB830201C7688B500847D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + CCB830221C7688B500847D42 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CCB830281C7688EC00847D42 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + CCB830291C7688EC00847D42 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + CCB8302A1C7688EC00847D42 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; + CCB8302B1C7688EC00847D42 /* DemoCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoCellNode.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CCB830101C7688B500847D42 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BCDB7EDE9701EB3DD88BCDA0 /* Pods_Sample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2D17B5A68E217F63F504A0AB /* Pods */ = { + isa = PBXGroup; + children = ( + 46B216EB47D6586F63D99B86 /* Pods-Sample.debug.xcconfig */, + 0C1B25A26B6D6815A16D0911 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 4C1A8C0BFF16C5457DF86019 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9DD2D199AD8BD92717ED9783 /* Pods_Sample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + CCB8300A1C7688B500847D42 = { + isa = PBXGroup; + children = ( + CCB830151C7688B500847D42 /* Sample */, + CCB830141C7688B500847D42 /* Products */, + 2D17B5A68E217F63F504A0AB /* Pods */, + 4C1A8C0BFF16C5457DF86019 /* Frameworks */, + ); + sourceTree = ""; + }; + CCB830141C7688B500847D42 /* Products */ = { + isa = PBXGroup; + children = ( + CCB830131C7688B500847D42 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + CCB830151C7688B500847D42 /* Sample */ = { + isa = PBXGroup; + children = ( + CCB830281C7688EC00847D42 /* AppDelegate.swift */, + CCB830291C7688EC00847D42 /* ViewController.swift */, + CCB8302A1C7688EC00847D42 /* Utilities.swift */, + CCB8302B1C7688EC00847D42 /* DemoCellNode.swift */, + CCB8301D1C7688B500847D42 /* Assets.xcassets */, + CCB8301F1C7688B500847D42 /* LaunchScreen.storyboard */, + CCB830221C7688B500847D42 /* Info.plist */, + ); + path = Sample; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CCB830121C7688B500847D42 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = CCB830251C7688B500847D42 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 95C31681ADDC41EDF46E7EDD /* Check Pods Manifest.lock */, + CCB8300F1C7688B500847D42 /* Sources */, + CCB830101C7688B500847D42 /* Frameworks */, + CCB830111C7688B500847D42 /* Resources */, + D7905F8C75C02D4B15A13055 /* Embed Pods Frameworks */, + FC69305B914BC44642AD442E /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = CCB830131C7688B500847D42 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CCB8300B1C7688B500847D42 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = "Adlai Holler"; + TargetAttributes = { + CCB830121C7688B500847D42 = { + CreatedOnToolsVersion = 7.2.1; + }; + }; + }; + buildConfigurationList = CCB8300E1C7688B500847D42 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CCB8300A1C7688B500847D42; + productRefGroup = CCB830141C7688B500847D42 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CCB830121C7688B500847D42 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CCB830111C7688B500847D42 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CCB830211C7688B500847D42 /* LaunchScreen.storyboard in Resources */, + CCB8301E1C7688B500847D42 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 95C31681ADDC41EDF46E7EDD /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + D7905F8C75C02D4B15A13055 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FC69305B914BC44642AD442E /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CCB8300F1C7688B500847D42 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CCB8302F1C7688EC00847D42 /* DemoCellNode.swift in Sources */, + CCB8302E1C7688EC00847D42 /* Utilities.swift in Sources */, + CCB8302D1C7688EC00847D42 /* ViewController.swift in Sources */, + CCB8302C1C7688EC00847D42 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + CCB8301F1C7688B500847D42 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + CCB830201C7688B500847D42 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + CCB830231C7688B500847D42 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + CCB830241C7688B500847D42 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_VERSION = 2.3; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CCB830261C7688B500847D42 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 46B216EB47D6586F63D99B86 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = adlai.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + CCB830271C7688B500847D42 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0C1B25A26B6D6815A16D0911 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = adlai.Sample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CCB8300E1C7688B500847D42 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CCB830231C7688B500847D42 /* Debug */, + CCB830241C7688B500847D42 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CCB830251C7688B500847D42 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CCB830261C7688B500847D42 /* Debug */, + CCB830271C7688B500847D42 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CCB8300B1C7688B500847D42 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..58a9f2df4d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/AppDelegate.swift b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/AppDelegate.swift new file mode 100644 index 0000000000..5868314aaf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/AppDelegate.swift @@ -0,0 +1,51 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + let window = UIWindow(frame: UIScreen.mainScreen().bounds) + self.window = window + let vc = ViewController() + window.rootViewController = UINavigationController(rootViewController: vc) + window.makeKeyAndVisible() + return true + } + + func applicationWillResignActive(application: UIApplication) { + // 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. + } + + func applicationDidEnterBackground(application: UIApplication) { + // 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. + } + + func applicationWillEnterForeground(application: UIApplication) { + // 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. + } + + func applicationDidBecomeActive(application: UIApplication) { + // 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. + } + + func applicationWillTerminate(application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..2e721e1833 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/DemoCellNode.swift b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/DemoCellNode.swift new file mode 100644 index 0000000000..6c3e7e529b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/DemoCellNode.swift @@ -0,0 +1,88 @@ +// +// DemoCellNode.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +final class DemoCellNode: ASCellNode { + let childA = ASDisplayNode() + let childB = ASDisplayNode() + var state = State.Right + + override init() { + super.init() + automaticallyManagesSubnodes = true + } + + override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { + let specA = ASRatioLayoutSpec(ratio: 1, child: childA) + specA.style.flexBasis = ASDimensionMakeWithPoints(1) + specA.style.flexGrow = 1.0 + let specB = ASRatioLayoutSpec(ratio: 1, child: childB) + specB.style.flexBasis = ASDimensionMakeWithPoints(1) + specB.style.flexGrow = 1.0 + let children = state.isReverse ? [ specB, specA ] : [ specA, specB ] + let direction: ASStackLayoutDirection = state.isVertical ? .Vertical : .Horizontal + return ASStackLayoutSpec(direction: direction, + spacing: 20, + justifyContent: .SpaceAround, + alignItems: .Center, + children: children) + } + + override func animateLayoutTransition(context: ASContextTransitioning) { + childA.frame = context.initialFrameForNode(childA) + childB.frame = context.initialFrameForNode(childB) + let tinyDelay = drand48() / 10 + UIView.animateWithDuration(0.5, delay: tinyDelay, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5, options: .BeginFromCurrentState, animations: { () -> Void in + self.childA.frame = context.finalFrameForNode(self.childA) + self.childB.frame = context.finalFrameForNode(self.childB) + }, completion: { + context.completeTransition($0) + }) + } + + enum State { + case Right + case Up + case Left + case Down + + var isVertical: Bool { + switch self { + case .Up, .Down: + return true + default: + return false + } + } + + var isReverse: Bool { + switch self { + case .Left, .Up: + return true + default: + return false + } + } + + mutating func advance() { + switch self { + case .Right: + self = .Up + case .Up: + self = .Left + case .Left: + self = .Down + case .Down: + self = .Right + } + } + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Info.plist new file mode 100644 index 0000000000..61861abb1a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Utilities.swift b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Utilities.swift new file mode 100644 index 0000000000..f33065245c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/Utilities.swift @@ -0,0 +1,16 @@ +// +// Utilities.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +extension UIColor { + static func random() -> UIColor { + return UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/ViewController.swift b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/ViewController.swift new file mode 100644 index 0000000000..47faf4384e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/BackgroundPropertySetting/Sample/ViewController.swift @@ -0,0 +1,96 @@ +// +// ViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +final class ViewController: ASViewController, ASCollectionDelegate, ASCollectionDataSource { + let itemCount = 1000 + + let itemSize: CGSize + let padding: CGFloat + var collectionNode: ASCollectionNode { + return node as! ASCollectionNode + } + + init() { + let layout = UICollectionViewFlowLayout() + (padding, itemSize) = ViewController.computeLayoutSizesForMainScreen() + layout.minimumInteritemSpacing = padding + layout.minimumLineSpacing = padding + super.init(node: ASCollectionNode(collectionViewLayout: layout)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Color", style: .Plain, target: self, action: #selector(didTapColorsButton)) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Layout", style: .Plain, target: self, action: #selector(didTapLayoutButton)) + collectionNode.delegate = self + collectionNode.dataSource = self + title = "Background Updating" + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: ASCollectionDataSource + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return itemCount + } + + func collectionView(collectionView: ASCollectionView, nodeBlockForItemAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock { + return { + let node = DemoCellNode() + node.backgroundColor = UIColor.random() + node.childA.backgroundColor = UIColor.random() + node.childB.backgroundColor = UIColor.random() + return node + } + } + + func collectionView(collectionView: ASCollectionView, constrainedSizeForNodeAtIndexPath indexPath: NSIndexPath) -> ASSizeRange { + return ASSizeRangeMake(itemSize, itemSize) + } + + // MARK: Action Handling + + @objc private func didTapColorsButton() { + let currentlyVisibleNodes = collectionNode.view.visibleNodes() + let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + dispatch_async(queue) { + for case let node as DemoCellNode in currentlyVisibleNodes { + node.backgroundColor = UIColor.random() + } + } + } + + @objc private func didTapLayoutButton() { + let currentlyVisibleNodes = collectionNode.view.visibleNodes() + let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + dispatch_async(queue) { + for case let node as DemoCellNode in currentlyVisibleNodes { + node.state.advance() + node.setNeedsLayout() + } + } + } + + // MARK: Static + + static func computeLayoutSizesForMainScreen() -> (padding: CGFloat, itemSize: CGSize) { + let numberOfColumns = 4 + let screen = UIScreen.mainScreen() + let scale = screen.scale + let screenWidth = Int(screen.bounds.width * screen.scale) + let itemWidthPx = (screenWidth - (numberOfColumns - 1)) / numberOfColumns + let leftover = screenWidth - itemWidthPx * numberOfColumns + let paddingPx = leftover / (numberOfColumns - 1) + let itemDimension = CGFloat(itemWidthPx) / scale + let padding = CGFloat(paddingPx) / scale + return (padding: padding, itemSize: CGSize(width: itemDimension, height: itemDimension)) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Cartfile b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Cartfile new file mode 100644 index 0000000000..aa14143b00 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Cartfile @@ -0,0 +1 @@ +github "facebook/AsyncDisplayKit" "master" diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.h new file mode 100644 index 0000000000..8d58a13cbe --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.m new file mode 100644 index 0000000000..f4d3339633 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.m @@ -0,0 +1,48 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +@import AsyncDisplayKit; + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..ebf48f6039 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..82cf14be21 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Info.plist new file mode 100644 index 0000000000..6905cc67bb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/ViewController.h new file mode 100644 index 0000000000..4627e29285 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/ViewController.h @@ -0,0 +1,16 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + + +@end + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/ViewController.m new file mode 100644 index 0000000000..993d6ac152 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/ViewController.m @@ -0,0 +1,36 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import + +@interface ViewController () + +@end + +@implementation ViewController + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + CGSize screenSize = self.view.bounds.size; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + ASTextNode *node = [[ASTextNode alloc] init]; + node.attributedText = [[NSAttributedString alloc] initWithString:@"hello world"]; + [node layoutThatFits:ASSizeRangeMake(CGSizeZero, (CGSize){.width = screenSize.width, .height = CGFLOAT_MAX})]; + node.frame = (CGRect) {.origin = (CGPoint){.x = 100, .y = 100}, .size = node.calculatedSize }; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.view addSubview:node.view]; + }); + }); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/main.m b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/main.m new file mode 100644 index 0000000000..0e5da05001 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/CarthageExample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/README.md b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/README.md new file mode 100644 index 0000000000..65ad6a6737 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/README.md @@ -0,0 +1,7 @@ +This project is supposed to test that the `AsyncDisplayKit.framework` built by Carthage from the master branch can be imported as a module without causing any warnings and errors. + +Steps to verify: + +- Run `carthage update --platform iOS` +- Build `CarthageExample.xcodeproj` +- Verify that there are 0 Errors and 0 Warnings diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6d3073fb34 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,357 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 871BB34E1C7C98B1005CF62A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 871BB34D1C7C98B1005CF62A /* main.m */; }; + 871BB3511C7C98B1005CF62A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 871BB3501C7C98B1005CF62A /* AppDelegate.m */; }; + 871BB3541C7C98B1005CF62A /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 871BB3531C7C98B1005CF62A /* ViewController.m */; }; + 871BB3571C7C98B1005CF62A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 871BB3551C7C98B1005CF62A /* Main.storyboard */; }; + 871BB3591C7C98B1005CF62A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 871BB3581C7C98B1005CF62A /* Assets.xcassets */; }; + 871BB35C1C7C98B1005CF62A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 871BB35A1C7C98B1005CF62A /* LaunchScreen.storyboard */; }; + 871BB3651C7C99B0005CF62A /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */; }; + DEAE185D1D1A504A0083FAD0 /* PINCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEAE185B1D1A504A0083FAD0 /* PINCache.framework */; }; + DEAE185E1D1A504A0083FAD0 /* PINRemoteImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 871BB3491C7C98B1005CF62A /* CarthageExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CarthageExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 871BB34D1C7C98B1005CF62A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 871BB34F1C7C98B1005CF62A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 871BB3501C7C98B1005CF62A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 871BB3521C7C98B1005CF62A /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 871BB3531C7C98B1005CF62A /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 871BB3561C7C98B1005CF62A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 871BB3581C7C98B1005CF62A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 871BB35B1C7C98B1005CF62A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 871BB35D1C7C98B1005CF62A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AsyncDisplayKit.framework; path = Carthage/Build/iOS/AsyncDisplayKit.framework; sourceTree = SOURCE_ROOT; }; + DEAE185B1D1A504A0083FAD0 /* PINCache.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINCache.framework; path = ../Carthage/Build/iOS/PINCache.framework; sourceTree = ""; }; + DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINRemoteImage.framework; path = ../Carthage/Build/iOS/PINRemoteImage.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 871BB3461C7C98B1005CF62A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 871BB3651C7C99B0005CF62A /* AsyncDisplayKit.framework in Frameworks */, + DEAE185D1D1A504A0083FAD0 /* PINCache.framework in Frameworks */, + DEAE185E1D1A504A0083FAD0 /* PINRemoteImage.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 871BB3401C7C98B1005CF62A = { + isa = PBXGroup; + children = ( + 871BB34B1C7C98B1005CF62A /* CarthageExample */, + 871BB34A1C7C98B1005CF62A /* Products */, + ); + sourceTree = ""; + }; + 871BB34A1C7C98B1005CF62A /* Products */ = { + isa = PBXGroup; + children = ( + 871BB3491C7C98B1005CF62A /* CarthageExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 871BB34B1C7C98B1005CF62A /* CarthageExample */ = { + isa = PBXGroup; + children = ( + 871BB34F1C7C98B1005CF62A /* AppDelegate.h */, + 871BB3501C7C98B1005CF62A /* AppDelegate.m */, + 871BB3581C7C98B1005CF62A /* Assets.xcassets */, + 871BB3631C7C9994005CF62A /* Frameworks */, + 871BB35D1C7C98B1005CF62A /* Info.plist */, + 871BB35A1C7C98B1005CF62A /* LaunchScreen.storyboard */, + 871BB3551C7C98B1005CF62A /* Main.storyboard */, + 871BB34C1C7C98B1005CF62A /* Supporting Files */, + 871BB3521C7C98B1005CF62A /* ViewController.h */, + 871BB3531C7C98B1005CF62A /* ViewController.m */, + ); + path = CarthageExample; + sourceTree = ""; + }; + 871BB34C1C7C98B1005CF62A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 871BB34D1C7C98B1005CF62A /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 871BB3631C7C9994005CF62A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */, + DEAE185B1D1A504A0083FAD0 /* PINCache.framework */, + DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 871BB3481C7C98B1005CF62A /* CarthageExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 871BB3601C7C98B1005CF62A /* Build configuration list for PBXNativeTarget "CarthageExample" */; + buildPhases = ( + 871BB3451C7C98B1005CF62A /* Sources */, + 871BB3461C7C98B1005CF62A /* Frameworks */, + 871BB3471C7C98B1005CF62A /* Resources */, + 871BB3661C7C99B8005CF62A /* Copy Framworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CarthageExample; + productName = CarthageExample; + productReference = 871BB3491C7C98B1005CF62A /* CarthageExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 871BB3411C7C98B1005CF62A /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = "Engin Kurutepe"; + TargetAttributes = { + 871BB3481C7C98B1005CF62A = { + CreatedOnToolsVersion = 7.2.1; + }; + }; + }; + buildConfigurationList = 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 871BB3401C7C98B1005CF62A; + productRefGroup = 871BB34A1C7C98B1005CF62A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 871BB3481C7C98B1005CF62A /* CarthageExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 871BB3471C7C98B1005CF62A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 871BB35C1C7C98B1005CF62A /* LaunchScreen.storyboard in Resources */, + 871BB3591C7C98B1005CF62A /* Assets.xcassets in Resources */, + 871BB3571C7C98B1005CF62A /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 871BB3661C7C99B8005CF62A /* Copy Framworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/Carthage/Build/iOS/AsyncDisplayKit.framework", + ); + name = "Copy Framworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/usr/local/bin/carthage copy-frameworks"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 871BB3451C7C98B1005CF62A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 871BB3541C7C98B1005CF62A /* ViewController.m in Sources */, + 871BB3511C7C98B1005CF62A /* AppDelegate.m in Sources */, + 871BB34E1C7C98B1005CF62A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 871BB3551C7C98B1005CF62A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 871BB3561C7C98B1005CF62A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 871BB35A1C7C98B1005CF62A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 871BB35B1C7C98B1005CF62A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 871BB35E1C7C98B1005CF62A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 871BB35F1C7C98B1005CF62A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 871BB3611C7C98B1005CF62A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = CarthageExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.asyncdisplaykit.CarthageExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 871BB3621C7C98B1005CF62A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = CarthageExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.asyncdisplaykit.CarthageExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 871BB35E1C7C98B1005CF62A /* Debug */, + 871BB35F1C7C98B1005CF62A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 871BB3601C7C98B1005CF62A /* Build configuration list for PBXNativeTarget "CarthageExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 871BB3611C7C98B1005CF62A /* Debug */, + 871BB3621C7C98B1005CF62A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 871BB3411C7C98B1005CF62A /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..ef35de35d9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..08de0be8d3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..8b3132f292 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Podfile b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..718ba91ba8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,379 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25A1FA851C02F7AC00193875 /* MosaicCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A1FA841C02F7AC00193875 /* MosaicCollectionViewLayout.m */; }; + 637D7C9FD46862FB6060DE4D /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DB64B734017B22EADB64FD /* libPods-Sample.a */; }; + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; }; + AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + AEE6B3E51C16B65600238D20 /* ImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AEE6B3E41C16B65600238D20 /* ImageViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 25A1FA831C02F7AC00193875 /* MosaicCollectionViewLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionViewLayout.h; sourceTree = ""; }; + 25A1FA841C02F7AC00193875 /* MosaicCollectionViewLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MosaicCollectionViewLayout.m; sourceTree = ""; }; + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SupplementaryNode.h; sourceTree = ""; }; + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SupplementaryNode.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; + AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AC3C4A651A11F47200143C57 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AC3C4A661A11F47200143C57 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AC3C4A681A11F47200143C57 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + AEE6B3E31C16B65600238D20 /* ImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageViewController.h; sourceTree = ""; }; + AEE6B3E41C16B65600238D20 /* ImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageViewController.m; sourceTree = ""; }; + D61D292E4C992F2B47A062A3 /* 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 = ""; }; + D9DB64B734017B22EADB64FD /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F594729764C63FA050734ED5 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC3C4A5B1A11F47200143C57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 637D7C9FD46862FB6060DE4D /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 90A2B9C5397C46134C8A793B /* Pods */ = { + isa = PBXGroup; + children = ( + F594729764C63FA050734ED5 /* Pods-Sample.debug.xcconfig */, + D61D292E4C992F2B47A062A3 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AC3C4A551A11F47200143C57 = { + isa = PBXGroup; + children = ( + AC3C4A601A11F47200143C57 /* Sample */, + AC3C4A5F1A11F47200143C57 /* Products */, + 90A2B9C5397C46134C8A793B /* Pods */, + D6E38FF0CB18E3F55CF06437 /* Frameworks */, + ); + sourceTree = ""; + }; + AC3C4A5F1A11F47200143C57 /* Products */ = { + isa = PBXGroup; + children = ( + AC3C4A5E1A11F47200143C57 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + AC3C4A601A11F47200143C57 /* Sample */ = { + isa = PBXGroup; + children = ( + 25A1FA831C02F7AC00193875 /* MosaicCollectionViewLayout.h */, + 25A1FA841C02F7AC00193875 /* MosaicCollectionViewLayout.m */, + AC3C4A651A11F47200143C57 /* AppDelegate.h */, + AC3C4A661A11F47200143C57 /* AppDelegate.m */, + AC3C4A681A11F47200143C57 /* ViewController.h */, + AC3C4A691A11F47200143C57 /* ViewController.m */, + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, + AC3C4A611A11F47200143C57 /* Supporting Files */, + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */, + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */, + AEE6B3E31C16B65600238D20 /* ImageViewController.h */, + AEE6B3E41C16B65600238D20 /* ImageViewController.m */, + ); + indentWidth = 2; + path = Sample; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + AC3C4A611A11F47200143C57 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + AC3C4A621A11F47200143C57 /* Info.plist */, + AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D6E38FF0CB18E3F55CF06437 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D9DB64B734017B22EADB64FD /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AC3C4A5D1A11F47200143C57 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + F868CFBB21824CC9521B6588 /* Check Pods Manifest.lock */, + AC3C4A5A1A11F47200143C57 /* Sources */, + AC3C4A5B1A11F47200143C57 /* Frameworks */, + AC3C4A5C1A11F47200143C57 /* Resources */, + A6902C454C7661D0D277AC62 /* Copy Pods Resources */, + 4E5B5F4E697ED7E9DB2D6324 /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = AC3C4A5E1A11F47200143C57 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AC3C4A561A11F47200143C57 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + AC3C4A5D1A11F47200143C57 = { + CreatedOnToolsVersion = 6.1; + }; + }; + }; + buildConfigurationList = AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AC3C4A551A11F47200143C57; + productRefGroup = AC3C4A5F1A11F47200143C57 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AC3C4A5D1A11F47200143C57 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AC3C4A5C1A11F47200143C57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 4E5B5F4E697ED7E9DB2D6324 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A6902C454C7661D0D277AC62 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + F868CFBB21824CC9521B6588 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AC3C4A5A1A11F47200143C57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25A1FA851C02F7AC00193875 /* MosaicCollectionViewLayout.m in Sources */, + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */, + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, + AC3C4A641A11F47200143C57 /* main.m in Sources */, + AEE6B3E51C16B65600238D20 /* ImageViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AC3C4A7F1A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A801A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AC3C4A821A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F594729764C63FA050734ED5 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + AC3C4A831A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D61D292E4C992F2B47A062A3 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A7F1A11F47200143C57 /* Debug */, + AC3C4A801A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A821A11F47200143C57 /* Debug */, + AC3C4A831A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AC3C4A561A11F47200143C57 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..f49edc75d6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/AppDelegate.m new file mode 100644 index 0000000000..78de0a3151 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/AppDelegate.m @@ -0,0 +1,27 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + + [self.window makeKeyAndVisible]; + + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ImageViewController.h b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ImageViewController.h new file mode 100644 index 0000000000..07000b0e3f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ImageViewController.h @@ -0,0 +1,14 @@ +// +// ImageViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ImageViewController : UIViewController +- (instancetype)initWithImage:(UIImage *)image; +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ImageViewController.m b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ImageViewController.m new file mode 100644 index 0000000000..60e5027dae --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ImageViewController.m @@ -0,0 +1,48 @@ +// +// ImageViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + + +#import "ImageViewController.h" + +@interface ImageViewController () +@property (nonatomic) UIImageView *imageView; +@end + +@implementation ImageViewController + +- (instancetype)initWithImage:(UIImage *)image { + if (!(self = [super init])) { return nil; } + + self.imageView = [[UIImageView alloc] initWithImage:image]; + + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + [self.view addSubview:self.imageView]; + + UIGestureRecognizer *tap = [[UIGestureRecognizer alloc] initWithTarget:self action:@selector(tapped)]; + [self.view addGestureRecognizer:tap]; + + self.imageView.contentMode = UIViewContentModeScaleAspectFill; +} + +- (void)tapped; +{ + NSLog(@"tapped!"); +} + +- (void)viewWillLayoutSubviews +{ + self.imageView.frame = self.view.bounds; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..f0fce54771 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,39 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "scale" : "1x", + "orientation" : "portrait" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "orientation" : "portrait" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png new file mode 100644 index 0000000000..1547a98454 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_0.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_0.imageset/Contents.json new file mode 100644 index 0000000000..4eaff61cc1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_0.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_0.imageset/image_0.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_0.imageset/image_0.jpg new file mode 100644 index 0000000000..4a365897ea Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_0.imageset/image_0.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_1.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_1.imageset/Contents.json new file mode 100644 index 0000000000..80c90eca3e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_1.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_1.imageset/image_1.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_1.imageset/image_1.jpg new file mode 100644 index 0000000000..5cb4828f44 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_1.imageset/image_1.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_10.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_10.imageset/Contents.json new file mode 100644 index 0000000000..d61e934e39 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_10.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_10.imageset/image_10.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_10.imageset/image_10.jpg new file mode 100644 index 0000000000..ea5cd6d268 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_10.imageset/image_10.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_11.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_11.imageset/Contents.json new file mode 100644 index 0000000000..94921077f9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_11.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_11.imageset/image_11.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_11.imageset/image_11.jpg new file mode 100644 index 0000000000..e93c68e512 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_11.imageset/image_11.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_12.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_12.imageset/Contents.json new file mode 100644 index 0000000000..61488a9fdc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_12.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_12.imageset/image_12.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_12.imageset/image_12.jpg new file mode 100644 index 0000000000..d520b6d80f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_12.imageset/image_12.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_13.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_13.imageset/Contents.json new file mode 100644 index 0000000000..7f83f8a390 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_13.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_13.imageset/image_13.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_13.imageset/image_13.jpg new file mode 100644 index 0000000000..c0232370cd Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_13.imageset/image_13.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_2.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_2.imageset/Contents.json new file mode 100644 index 0000000000..774cde7833 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_2.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_2.imageset/image_2.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_2.imageset/image_2.jpg new file mode 100644 index 0000000000..175343454d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_2.imageset/image_2.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_3.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_3.imageset/Contents.json new file mode 100644 index 0000000000..c0abe414cd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_3.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_3.imageset/image_3.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_3.imageset/image_3.jpg new file mode 100644 index 0000000000..f5398cac79 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_3.imageset/image_3.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_4.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_4.imageset/Contents.json new file mode 100644 index 0000000000..55a498a8a0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_4.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_4.imageset/image_4.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_4.imageset/image_4.jpg new file mode 100644 index 0000000000..2a6fe4c264 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_4.imageset/image_4.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_5.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_5.imageset/Contents.json new file mode 100644 index 0000000000..9a1181e83b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_5.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_5.imageset/image_5.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_5.imageset/image_5.jpg new file mode 100644 index 0000000000..4e507b8064 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_5.imageset/image_5.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_6.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_6.imageset/Contents.json new file mode 100644 index 0000000000..6aef7d6047 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_6.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_6.imageset/image_6.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_6.imageset/image_6.jpg new file mode 100644 index 0000000000..35fe778b3a Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_6.imageset/image_6.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_7.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_7.imageset/Contents.json new file mode 100644 index 0000000000..acdb0e87f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_7.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_7.imageset/image_7.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_7.imageset/image_7.jpg new file mode 100644 index 0000000000..8f5e037722 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_7.imageset/image_7.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_8.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_8.imageset/Contents.json new file mode 100644 index 0000000000..40d616ed40 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_8.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_8.imageset/image_8.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_8.imageset/image_8.jpg new file mode 100644 index 0000000000..5651436bb6 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_8.imageset/image_8.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_9.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_9.imageset/Contents.json new file mode 100644 index 0000000000..b3b3c74e12 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "image_9.jpg" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_9.imageset/image_9.jpg b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_9.imageset/image_9.jpg new file mode 100644 index 0000000000..9fb6e47d3f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Images.xcassets/image_9.imageset/image_9.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Info.plist new file mode 100644 index 0000000000..eeb71a8d35 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launchboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Launchboard.storyboard b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/MosaicCollectionViewLayout.h b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/MosaicCollectionViewLayout.h new file mode 100644 index 0000000000..c7a9867fb5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/MosaicCollectionViewLayout.h @@ -0,0 +1,31 @@ +// +// MosaicCollectionViewLayout.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface MosaicCollectionViewLayout : UICollectionViewLayout + +@property (assign, nonatomic) NSUInteger numberOfColumns; +@property (assign, nonatomic) CGFloat columnSpacing; +@property (assign, nonatomic) UIEdgeInsets sectionInset; +@property (assign, nonatomic) UIEdgeInsets interItemSpacing; +@property (assign, nonatomic) CGFloat headerHeight; + +@end + +@protocol MosaicCollectionViewLayoutDelegate + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(MosaicCollectionViewLayout *)layout originalItemSizeAtIndexPath:(NSIndexPath *)indexPath; + +@end + +@interface MosaicCollectionViewLayoutInspector : NSObject + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/MosaicCollectionViewLayout.m b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/MosaicCollectionViewLayout.m new file mode 100644 index 0000000000..bc48035100 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/MosaicCollectionViewLayout.m @@ -0,0 +1,231 @@ +// +// MosaicCollectionViewLayout.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "MosaicCollectionViewLayout.h" + +@implementation MosaicCollectionViewLayout { + NSMutableArray *_columnHeights; + NSMutableArray *_itemAttributes; + NSMutableDictionary *_headerAttributes; + NSMutableArray *_allAttributes; +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + self.numberOfColumns = 3; + self.columnSpacing = 10.0; + self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); + self.interItemSpacing = UIEdgeInsetsMake(10.0, 0, 10.0, 0); + } + return self; +} + +- (void)prepareLayout +{ + _itemAttributes = [NSMutableArray array]; + _columnHeights = [NSMutableArray array]; + _allAttributes = [NSMutableArray array]; + _headerAttributes = [NSMutableDictionary dictionary]; + + CGFloat top = 0; + + NSInteger numberOfSections = [self.collectionView numberOfSections]; + for (NSUInteger section = 0; section < numberOfSections; section++) { + NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:section]; + + top += _sectionInset.top; + + if (_headerHeight > 0) { + CGSize headerSize = [self _headerSizeForSection:section]; + UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes + layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader + withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + attributes.frame = CGRectMake(_sectionInset.left, top, headerSize.width, headerSize.height); + _headerAttributes[@(section)] = attributes; + [_allAttributes addObject:attributes]; + top = CGRectGetMaxY(attributes.frame); + } + + [_columnHeights addObject:[NSMutableArray array]]; + for (NSUInteger idx = 0; idx < self.numberOfColumns; idx++) { + [_columnHeights[section] addObject:@(top)]; + } + + CGFloat columnWidth = [self _columnWidthForSection:section]; + [_itemAttributes addObject:[NSMutableArray array]]; + for (NSUInteger idx = 0; idx < numberOfItems; idx++) { + NSUInteger columnIndex = [self _shortestColumnIndexInSection:section]; + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; + + CGSize itemSize = [self _itemSizeAtIndexPath:indexPath]; + CGFloat xOffset = _sectionInset.left + (columnWidth + _columnSpacing) * columnIndex; + CGFloat yOffset = [_columnHeights[section][columnIndex] floatValue]; + + UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes + layoutAttributesForCellWithIndexPath:indexPath]; + attributes.frame = CGRectMake(xOffset, yOffset, itemSize.width, itemSize.height); + + _columnHeights[section][columnIndex] = @(CGRectGetMaxY(attributes.frame) + _interItemSpacing.bottom); + + [_itemAttributes[section] addObject:attributes]; + [_allAttributes addObject:attributes]; + } + + NSUInteger columnIndex = [self _tallestColumnIndexInSection:section]; + top = [_columnHeights[section][columnIndex] floatValue] - _interItemSpacing.bottom + _sectionInset.bottom; + + for (NSUInteger idx = 0; idx < [_columnHeights[section] count]; idx++) { + _columnHeights[section][idx] = @(top); + } + } +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +{ + NSMutableArray *includedAttributes = [NSMutableArray array]; + // Slow search for small batches + for (UICollectionViewLayoutAttributes *attributes in _allAttributes) { + if (CGRectIntersectsRect(attributes.frame, rect)) { + [includedAttributes addObject:attributes]; + } + } + return includedAttributes; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section >= _itemAttributes.count) { + return nil; + } else if (indexPath.item >= [_itemAttributes[indexPath.section] count]) { + return nil; + } + return _itemAttributes[indexPath.section][indexPath.item]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath +{ + if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) { + return _headerAttributes[@(indexPath.section)]; + } + return nil; +} + +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds +{ + if (!CGRectEqualToRect(self.collectionView.bounds, newBounds)) { + return YES; + } + return NO; +} + +- (CGFloat)_widthForSection:(NSUInteger)section +{ + return self.collectionView.bounds.size.width - _sectionInset.left - _sectionInset.right; +} + +- (CGFloat)_columnWidthForSection:(NSUInteger)section +{ + return ([self _widthForSection:section] - ((_numberOfColumns - 1) * _columnSpacing)) / _numberOfColumns; +} + +- (CGSize)_itemSizeAtIndexPath:(NSIndexPath *)indexPath +{ + CGSize size = CGSizeMake([self _columnWidthForSection:indexPath.section], 0); + CGSize originalSize = [[self _delegate] collectionView:self.collectionView layout:self originalItemSizeAtIndexPath:indexPath]; + if (originalSize.height > 0 && originalSize.width > 0) { + size.height = originalSize.height / originalSize.width * size.width; + } + return size; +} + +- (CGSize)_headerSizeForSection:(NSUInteger)section +{ + return CGSizeMake([self _widthForSection:section], _headerHeight); +} + +- (CGSize)collectionViewContentSize +{ + CGFloat height = [[[_columnHeights lastObject] firstObject] floatValue]; + return CGSizeMake(self.collectionView.bounds.size.width, height); +} + +- (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section +{ + __block NSUInteger index = 0; + __block CGFloat tallestHeight = 0; + [_columnHeights[section] enumerateObjectsUsingBlock:^(NSNumber *height, NSUInteger idx, BOOL *stop) { + if (height.floatValue > tallestHeight) { + index = idx; + tallestHeight = height.floatValue; + } + }]; + return index; +} + +- (NSUInteger)_shortestColumnIndexInSection:(NSUInteger)section +{ + __block NSUInteger index = 0; + __block CGFloat shortestHeight = CGFLOAT_MAX; + [_columnHeights[section] enumerateObjectsUsingBlock:^(NSNumber *height, NSUInteger idx, BOOL *stop) { + if (height.floatValue < shortestHeight) { + index = idx; + shortestHeight = height.floatValue; + } + }]; + return index; +} + +- (id)_delegate +{ + return (id)self.collectionView.delegate; +} + +@end + +@implementation MosaicCollectionViewLayoutInspector + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + MosaicCollectionViewLayout *layout = (MosaicCollectionViewLayout *)[collectionView collectionViewLayout]; + return ASSizeRangeMake(CGSizeZero, [layout _itemSizeAtIndexPath:indexPath]); +} + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + MosaicCollectionViewLayout *layout = (MosaicCollectionViewLayout *)[collectionView collectionViewLayout]; + return ASSizeRangeMake(CGSizeZero, [layout _headerSizeForSection:indexPath.section]); +} + +/** + * Asks the inspector for the number of supplementary sections in the collection view for the given kind. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind +{ + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + return [[collectionView asyncDataSource] numberOfSectionsInCollectionView:collectionView]; + } else { + return 0; + } +} + +/** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + return 1; + } else { + return 0; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/SupplementaryNode.h b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/SupplementaryNode.h new file mode 100644 index 0000000000..b29ec002b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/SupplementaryNode.h @@ -0,0 +1,16 @@ +// +// SupplementaryNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface SupplementaryNode : ASCellNode + +- (instancetype)initWithText:(NSString *)text; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/SupplementaryNode.m b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/SupplementaryNode.m new file mode 100644 index 0000000000..47dfa786b5 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/SupplementaryNode.m @@ -0,0 +1,50 @@ +// +// SupplementaryNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "SupplementaryNode.h" + +#import +#import +#import + +@implementation SupplementaryNode { + ASTextNode *_textNode; +} + +- (instancetype)initWithText:(NSString *)text +{ + self = [super init]; + if (self != nil) { + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:text + attributes:[self textAttributes]]; + [self addSubnode:_textNode]; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *center = [[ASCenterLayoutSpec alloc] init]; + center.centeringOptions = ASCenterLayoutSpecCenteringY; + center.child = _textNode; + return center; +} + +#pragma mark - Text Formatting + +- (NSDictionary *)textAttributes +{ + return @{ + NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline], + NSForegroundColorAttributeName: [UIColor grayColor], + }; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ViewController.m new file mode 100644 index 0000000000..60979faf3e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/ViewController.m @@ -0,0 +1,122 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import "MosaicCollectionViewLayout.h" +#import "SupplementaryNode.h" +#import "ImageViewController.h" + +static NSUInteger kNumberOfImages = 14; + +@interface ViewController () +{ + NSMutableArray *_sections; + ASCollectionView *_collectionView; + MosaicCollectionViewLayoutInspector *_layoutInspector; +} + +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + self = [super init]; + if (self) { + + _sections = [NSMutableArray array]; + [_sections addObject:[NSMutableArray array]]; + for (NSUInteger idx = 0, section = 0; idx < kNumberOfImages; idx++) { + NSString *name = [NSString stringWithFormat:@"image_%lu.jpg", (unsigned long)idx]; + [_sections[section] addObject:[UIImage imageNamed:name]]; + if ((idx + 1) % 5 == 0 && idx < kNumberOfImages - 1) { + section++; + [_sections addObject:[NSMutableArray array]]; + } + } + + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + MosaicCollectionViewLayout *layout = [[MosaicCollectionViewLayout alloc] init]; + layout.numberOfColumns = 2; + layout.headerHeight = 44.0; + + _layoutInspector = [[MosaicCollectionViewLayoutInspector alloc] init]; + + _collectionView = [[ASCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; + _collectionView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + _collectionView.asyncDataSource = self; + _collectionView.asyncDelegate = self; + _collectionView.layoutInspector = _layoutInspector; + _collectionView.backgroundColor = [UIColor whiteColor]; + + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [self.view addSubview:_collectionView]; +} + +- (void)dealloc +{ + _collectionView.asyncDataSource = nil; + _collectionView.asyncDelegate = nil; +} + +- (void)reloadTapped +{ + [_collectionView reloadData]; +} + +#pragma mark - +#pragma mark ASCollectionView data source. + +- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + UIImage *image = _sections[indexPath.section][indexPath.item]; + return ^{ + return [[ASCellNode alloc] initWithViewControllerBlock:^UIViewController *{ + return [[ImageViewController alloc] initWithImage:image]; + } didLoadBlock:^(ASDisplayNode * _Nonnull node) { + node.layer.borderWidth = 1.0; + node.layer.borderColor = [UIColor blackColor].CGColor; + }]; + }; +} + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSString *text = [NSString stringWithFormat:@"Section %d", (int)indexPath.section + 1]; + return [[SupplementaryNode alloc] initWithText:text]; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return _sections.count; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return [_sections[section] count]; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout originalItemSizeAtIndexPath:(NSIndexPath *)indexPath +{ + return [(UIImage *)_sections[indexPath.section][indexPath.item] size]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/main.m new file mode 100644 index 0000000000..65850400e4 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/CollectionViewWithViewControllerCells/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/EditableText/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Podfile b/submodules/AsyncDisplayKit/examples_extra/EditableText/Podfile new file mode 100644 index 0000000000..08d1b7add6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..2afe6c7b24 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,366 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 536169B9EF7CEDECCE1B60F0 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B6B3820335D6E75E7E0BE9FF /* 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 5C6A0F715CB9F7DF29FC026B /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + B6B3820335D6E75E7E0BE9FF /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DED40FC71CCB2096003C123B /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; + DED40FC81CCB2096003C123B /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 536169B9EF7CEDECCE1B60F0 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5C6A0F715CB9F7DF29FC026B /* libPods.a */, + B6B3820335D6E75E7E0BE9FF /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + DED40FC71CCB2096003C123B /* Pods-Sample.debug.xcconfig */, + DED40FC81CCB2096003C123B /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + 6FBCCC34F8CCA9B610492536 /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6FBCCC34F8CCA9B610492536 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DED40FC71CCB2096003C123B /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DED40FC81CCB2096003C123B /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..d41d58c5d8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/AppDelegate.m new file mode 100644 index 0000000000..d0fd66f775 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/ViewController.m new file mode 100644 index 0000000000..0a9703b897 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/ViewController.m @@ -0,0 +1,94 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import + + +@interface ViewController () +{ + ASEditableTextNode *_textNode; + + // These elements are a test case for ASTextNode truncation. + UILabel *_label; + ASTextNode *_node; +} + +@end + + +@implementation ViewController + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // simple editable text node. here we use it synchronously, but it fully supports async layout & display + _textNode = [[ASEditableTextNode alloc] init]; + _textNode.returnKeyType = UIReturnKeyDone; + _textNode.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.1f]; + + // with placeholder text (displayed if the user hasn't entered text) + NSDictionary *placeholderAttrs = @{ NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue-LightItalic" size:18.0f] }; + _textNode.attributedPlaceholderText = [[NSAttributedString alloc] initWithString:@"Tap to type!" + attributes:placeholderAttrs]; + + // and typing attributes (style for any text the user enters) + _textNode.typingAttributes = @{ NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue-Light" size:18.0f] }; + + // the usual delegate methods are available; see ASEditableTextNodeDelegate + _textNode.delegate = self; + + + // Do any additional setup after loading the view, typically from a nib. + NSDictionary *attrs = @{ NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue" size:12.0f] }; + NSAttributedString *string = [[NSAttributedString alloc] initWithString:@"1\n2\n3\n4\n5" attributes:attrs]; + + _label = [[UILabel alloc] init]; + _label.attributedText = string; + _label.backgroundColor = [UIColor lightGrayColor]; + _label.numberOfLines = 3; + _label.frame = CGRectMake(20, 400, 40, 100); + + _node = [[ASTextNode alloc] init]; + _node.maximumNumberOfLines = 3; + _node.backgroundColor = [UIColor lightGrayColor]; + _node.attributedText = string; + _node.frame = CGRectMake(70, 400, 40, 100); +// [_node measure:CGSizeMake(40, 50)]; No longer needed now that https://github.com/facebook/AsyncDisplayKit/issues/1295 is fixed. + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubnode:_textNode]; + [self.view addSubnode:_node]; + [self.view addSubview:_label]; + + [self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)]]; +} + +- (void)viewWillLayoutSubviews +{ + // place the text node in the top half of the screen, with a bit of padding + _textNode.frame = CGRectMake(0, 20, self.view.bounds.size.width, (self.view.bounds.size.height / 2) - 40); +} + +- (void)tap:(UITapGestureRecognizer *)sender +{ + // dismiss the keyboard when we tap outside the text field + [_textNode resignFirstResponder]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/EditableText/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Podfile b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..9c8770b730 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,760 @@ + + + + + archiveVersion + 1 + classes + + objectVersion + 46 + objects + + 0585427F19D4DBE100606EA6 + + isa + PBXFileReference + lastKnownFileType + image.png + name + Default-568h@2x.png + path + ../Default-568h@2x.png + sourceTree + <group> + + 0585428019D4DBE100606EA6 + + fileRef + 0585427F19D4DBE100606EA6 + isa + PBXBuildFile + + 05E2127819D4DB510098F589 + + children + + 05E2128319D4DB510098F589 + 05E2128219D4DB510098F589 + 1A943BF0259746F18D6E423F + 1AE410B73DA5C3BD087ACDD7 + + indentWidth + 2 + isa + PBXGroup + sourceTree + <group> + tabWidth + 2 + usesTabs + 0 + + 05E2127919D4DB510098F589 + + attributes + + LastUpgradeCheck + 0600 + ORGANIZATIONNAME + Facebook + TargetAttributes + + 05E2128019D4DB510098F589 + + CreatedOnToolsVersion + 6.0.1 + + + + buildConfigurationList + 05E2127C19D4DB510098F589 + compatibilityVersion + Xcode 3.2 + developmentRegion + English + hasScannedForEncodings + 0 + isa + PBXProject + knownRegions + + en + Base + + mainGroup + 05E2127819D4DB510098F589 + productRefGroup + 05E2128219D4DB510098F589 + projectDirPath + + projectReferences + + projectRoot + + targets + + 05E2128019D4DB510098F589 + + + 05E2127C19D4DB510098F589 + + buildConfigurations + + 05E212A219D4DB510098F589 + 05E212A319D4DB510098F589 + + defaultConfigurationIsVisible + 0 + defaultConfigurationName + Release + isa + XCConfigurationList + + 05E2127D19D4DB510098F589 + + buildActionMask + 2147483647 + files + + 05E2128D19D4DB510098F589 + 05E2128A19D4DB510098F589 + 05E2128719D4DB510098F589 + ACC945AE1BA9EFBA005E1FB8 + + isa + PBXSourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 05E2127E19D4DB510098F589 + + buildActionMask + 2147483647 + files + + AD61969DBDF7FD632C7D07F8 + + isa + PBXFrameworksBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 05E2127F19D4DB510098F589 + + buildActionMask + 2147483647 + files + + 0585428019D4DBE100606EA6 + 6C2C82AC19EE274300767484 + 6C2C82AD19EE274300767484 + + isa + PBXResourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 05E2128019D4DB510098F589 + + buildConfigurationList + 05E212A419D4DB510098F589 + buildPhases + + E080B80F89C34A25B3488E26 + 05E2127D19D4DB510098F589 + 05E2127E19D4DB510098F589 + 05E2127F19D4DB510098F589 + F012A6F39E0149F18F564F50 + 682D6483AA9410CE0AF21363 + + buildRules + + dependencies + + isa + PBXNativeTarget + name + Sample + productName + Sample + productReference + 05E2128119D4DB510098F589 + productType + com.apple.product-type.application + + 05E2128119D4DB510098F589 + + explicitFileType + wrapper.application + includeInIndex + 0 + isa + PBXFileReference + path + Sample.app + sourceTree + BUILT_PRODUCTS_DIR + + 05E2128219D4DB510098F589 + + children + + 05E2128119D4DB510098F589 + + isa + PBXGroup + name + Products + sourceTree + <group> + + 05E2128319D4DB510098F589 + + children + + 05E2128819D4DB510098F589 + 05E2128919D4DB510098F589 + 05E2128B19D4DB510098F589 + 05E2128C19D4DB510098F589 + ACC945AC1BA9EFB3005E1FB8 + ACC945AD1BA9EFBA005E1FB8 + 05E2128419D4DB510098F589 + + isa + PBXGroup + path + Sample + sourceTree + <group> + + 05E2128419D4DB510098F589 + + children + + 0585427F19D4DBE100606EA6 + 6C2C82AA19EE274300767484 + 6C2C82AB19EE274300767484 + 05E2128519D4DB510098F589 + 05E2128619D4DB510098F589 + + isa + PBXGroup + name + Supporting Files + sourceTree + <group> + + 05E2128519D4DB510098F589 + + isa + PBXFileReference + lastKnownFileType + text.plist.xml + path + Info.plist + sourceTree + <group> + + 05E2128619D4DB510098F589 + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + main.m + sourceTree + <group> + + 05E2128719D4DB510098F589 + + fileRef + 05E2128619D4DB510098F589 + isa + PBXBuildFile + + 05E2128819D4DB510098F589 + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + AppDelegate.h + sourceTree + <group> + + 05E2128919D4DB510098F589 + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + AppDelegate.m + sourceTree + <group> + + 05E2128A19D4DB510098F589 + + fileRef + 05E2128919D4DB510098F589 + isa + PBXBuildFile + + 05E2128B19D4DB510098F589 + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ViewController.h + sourceTree + <group> + + 05E2128C19D4DB510098F589 + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ViewController.m + sourceTree + <group> + + 05E2128D19D4DB510098F589 + + fileRef + 05E2128C19D4DB510098F589 + isa + PBXBuildFile + + 05E212A219D4DB510098F589 + + buildSettings + + ALWAYS_SEARCH_USER_PATHS + NO + CLANG_CXX_LANGUAGE_STANDARD + gnu++0x + CLANG_CXX_LIBRARY + libc++ + CLANG_ENABLE_MODULES + YES + CLANG_ENABLE_OBJC_ARC + YES + CLANG_WARN_BOOL_CONVERSION + YES + CLANG_WARN_CONSTANT_CONVERSION + YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE + YES_ERROR + CLANG_WARN_EMPTY_BODY + YES + CLANG_WARN_ENUM_CONVERSION + YES + CLANG_WARN_INT_CONVERSION + YES + CLANG_WARN_OBJC_ROOT_CLASS + YES_ERROR + CLANG_WARN_UNREACHABLE_CODE + YES + CLANG_WARN__DUPLICATE_METHOD_MATCH + YES + CODE_SIGN_IDENTITY[sdk=iphoneos*] + iPhone Developer + COPY_PHASE_STRIP + NO + ENABLE_STRICT_OBJC_MSGSEND + YES + GCC_C_LANGUAGE_STANDARD + gnu99 + GCC_DYNAMIC_NO_PIC + NO + GCC_OPTIMIZATION_LEVEL + 0 + GCC_PREPROCESSOR_DEFINITIONS + + DEBUG=1 + $(inherited) + + GCC_SYMBOLS_PRIVATE_EXTERN + NO + GCC_WARN_64_TO_32_BIT_CONVERSION + YES + GCC_WARN_ABOUT_RETURN_TYPE + YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR + YES + GCC_WARN_UNINITIALIZED_AUTOS + YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION + YES + GCC_WARN_UNUSED_VARIABLE + YES + IPHONEOS_DEPLOYMENT_TARGET + 8.0 + MTL_ENABLE_DEBUG_INFO + YES + ONLY_ACTIVE_ARCH + YES + SDKROOT + iphoneos + + isa + XCBuildConfiguration + name + Debug + + 05E212A319D4DB510098F589 + + buildSettings + + ALWAYS_SEARCH_USER_PATHS + NO + CLANG_CXX_LANGUAGE_STANDARD + gnu++0x + CLANG_CXX_LIBRARY + libc++ + CLANG_ENABLE_MODULES + YES + CLANG_ENABLE_OBJC_ARC + YES + CLANG_WARN_BOOL_CONVERSION + YES + CLANG_WARN_CONSTANT_CONVERSION + YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE + YES_ERROR + CLANG_WARN_EMPTY_BODY + YES + CLANG_WARN_ENUM_CONVERSION + YES + CLANG_WARN_INT_CONVERSION + YES + CLANG_WARN_OBJC_ROOT_CLASS + YES_ERROR + CLANG_WARN_UNREACHABLE_CODE + YES + CLANG_WARN__DUPLICATE_METHOD_MATCH + YES + CODE_SIGN_IDENTITY[sdk=iphoneos*] + iPhone Developer + COPY_PHASE_STRIP + YES + ENABLE_NS_ASSERTIONS + NO + ENABLE_STRICT_OBJC_MSGSEND + YES + GCC_C_LANGUAGE_STANDARD + gnu99 + GCC_WARN_64_TO_32_BIT_CONVERSION + YES + GCC_WARN_ABOUT_RETURN_TYPE + YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR + YES + GCC_WARN_UNINITIALIZED_AUTOS + YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION + YES + GCC_WARN_UNUSED_VARIABLE + YES + IPHONEOS_DEPLOYMENT_TARGET + 8.0 + MTL_ENABLE_DEBUG_INFO + NO + SDKROOT + iphoneos + VALIDATE_PRODUCT + YES + + isa + XCBuildConfiguration + name + Release + + 05E212A419D4DB510098F589 + + buildConfigurations + + 05E212A519D4DB510098F589 + 05E212A619D4DB510098F589 + + defaultConfigurationIsVisible + 0 + defaultConfigurationName + Release + isa + XCConfigurationList + + 05E212A519D4DB510098F589 + + baseConfigurationReference + 4E60AE604B72B745B8D6B008 + buildSettings + + ASSETCATALOG_COMPILER_APPICON_NAME + AppIcon + INFOPLIST_FILE + Sample/Info.plist + LD_RUNPATH_SEARCH_PATHS + $(inherited) @executable_path/Frameworks + PRODUCT_NAME + $(TARGET_NAME) + + isa + XCBuildConfiguration + name + Debug + + 05E212A619D4DB510098F589 + + baseConfigurationReference + 1F2942882A3B5220B7506FFC + buildSettings + + ASSETCATALOG_COMPILER_APPICON_NAME + AppIcon + INFOPLIST_FILE + Sample/Info.plist + LD_RUNPATH_SEARCH_PATHS + $(inherited) @executable_path/Frameworks + PRODUCT_NAME + $(TARGET_NAME) + + isa + XCBuildConfiguration + name + Release + + 1A943BF0259746F18D6E423F + + children + + 4F8E019F6C75C267AC72A2F3 + + isa + PBXGroup + name + Frameworks + sourceTree + <group> + + 1AE410B73DA5C3BD087ACDD7 + + children + + 4E60AE604B72B745B8D6B008 + 1F2942882A3B5220B7506FFC + + isa + PBXGroup + name + Pods + sourceTree + <group> + + 1F2942882A3B5220B7506FFC + + includeInIndex + 1 + isa + PBXFileReference + lastKnownFileType + text.xcconfig + name + Pods-Sample.release.xcconfig + path + Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig + sourceTree + <group> + + 4E60AE604B72B745B8D6B008 + + includeInIndex + 1 + isa + PBXFileReference + lastKnownFileType + text.xcconfig + name + Pods-Sample.debug.xcconfig + path + Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig + sourceTree + <group> + + 4F8E019F6C75C267AC72A2F3 + + explicitFileType + archive.ar + includeInIndex + 0 + isa + PBXFileReference + path + libPods-Sample.a + sourceTree + BUILT_PRODUCTS_DIR + + 682D6483AA9410CE0AF21363 + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + Embed Pods Frameworks + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh" + + showEnvVarsInLog + 0 + + 6C2C82AA19EE274300767484 + + isa + PBXFileReference + lastKnownFileType + image.png + path + Default-667h@2x.png + sourceTree + SOURCE_ROOT + + 6C2C82AB19EE274300767484 + + isa + PBXFileReference + lastKnownFileType + image.png + path + Default-736h@3x.png + sourceTree + SOURCE_ROOT + + 6C2C82AC19EE274300767484 + + fileRef + 6C2C82AA19EE274300767484 + isa + PBXBuildFile + + 6C2C82AD19EE274300767484 + + fileRef + 6C2C82AB19EE274300767484 + isa + PBXBuildFile + + ACC945AC1BA9EFB3005E1FB8 + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ScreenNode.h + sourceTree + <group> + + ACC945AD1BA9EFBA005E1FB8 + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ScreenNode.m + sourceTree + <group> + + ACC945AE1BA9EFBA005E1FB8 + + fileRef + ACC945AD1BA9EFBA005E1FB8 + isa + PBXBuildFile + + AD61969DBDF7FD632C7D07F8 + + fileRef + 4F8E019F6C75C267AC72A2F3 + isa + PBXBuildFile + + E080B80F89C34A25B3488E26 + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + Check Pods Manifest.lock + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + diff "${PODS_ROOT}/../Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null +if [[ $? != 0 ]] ; then + cat << EOM +error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. +EOM + exit 1 +fi + + showEnvVarsInLog + 0 + + F012A6F39E0149F18F564F50 + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + Copy Pods Resources + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh" + + showEnvVarsInLog + 0 + + + rootObject + 05E2127919D4DB510098F589 + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/AppDelegate.m new file mode 100644 index 0000000000..d0fd66f775 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ScreenNode.h b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ScreenNode.h new file mode 100644 index 0000000000..4ade23a06b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ScreenNode.h @@ -0,0 +1,20 @@ +// +// ScreenNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ScreenNode : ASDisplayNode + +@property (nonatomic, strong) ASMultiplexImageNode *imageNode; +@property (nonatomic, strong) ASButtonNode *buttonNode; + +- (void)start; +- (void)reload; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ScreenNode.m b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ScreenNode.m new file mode 100644 index 0000000000..345c5bed5d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ScreenNode.m @@ -0,0 +1,159 @@ +// +// ScreenNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ScreenNode.h" + +@interface ScreenNode() +@end + +@implementation ScreenNode + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + // multiplex image node! + // NB: we're using a custom downloader with an artificial delay for this demo, but ASPINRemoteImageDownloader works too! + _imageNode = [[ASMultiplexImageNode alloc] initWithCache:nil downloader:self]; + _imageNode.dataSource = self; + _imageNode.delegate = self; + + // placeholder colour + _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + + // load low-quality images before high-quality images + _imageNode.downloadsIntermediateImages = YES; + + // simple status label. Synchronous to avoid flicker / placeholder state when updating. + _buttonNode = [[ASButtonNode alloc] init]; + [_buttonNode addTarget:self action:@selector(reload) forControlEvents:ASControlNodeEventTouchUpInside]; + _buttonNode.titleNode.displaysAsynchronously = NO; + + [self addSubnode:_imageNode]; + [self addSubnode:_buttonNode]; + + return self; +} + +- (void)start +{ + [self setText:@"loading…"]; + _buttonNode.userInteractionEnabled = NO; + _imageNode.imageIdentifiers = @[ @"best", @"medium", @"worst" ]; // go! +} + +- (void)reload +{ + [self start]; + [_imageNode reloadImageIdentifierSources]; +} + +- (void)setText:(NSString *)text +{ + NSDictionary *attributes = @{NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue-Light" size:22.0f]}; + NSAttributedString *string = [[NSAttributedString alloc] initWithString:text + attributes:attributes]; + [_buttonNode setAttributedTitle:string forState:UIControlStateNormal]; + [self setNeedsLayout]; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASRatioLayoutSpec *imagePlaceholder = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1 child:_imageNode]; + + ASStackLayoutSpec *verticalStack = [[ASStackLayoutSpec alloc] init]; + verticalStack.direction = ASStackLayoutDirectionVertical; + verticalStack.spacing = 10; + verticalStack.justifyContent = ASStackLayoutJustifyContentCenter; + verticalStack.alignItems = ASStackLayoutAlignItemsCenter; + verticalStack.children = @[imagePlaceholder, _buttonNode]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) child:verticalStack]; +} + +#pragma mark - +#pragma mark ASMultiplexImageNode data source & delegate. + +- (NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(id)imageIdentifier +{ + if ([imageIdentifier isEqualToString:@"worst"]) { + return [NSURL URLWithString:@"https://raw.githubusercontent.com/facebook/AsyncDisplayKit/master/examples_extra/Multiplex/worst.png"]; + } + + if ([imageIdentifier isEqualToString:@"medium"]) { + return [NSURL URLWithString:@"https://raw.githubusercontent.com/facebook/AsyncDisplayKit/master/examples_extra/Multiplex/medium.png"]; + } + + if ([imageIdentifier isEqualToString:@"best"]) { + return [NSURL URLWithString:@"https://raw.githubusercontent.com/facebook/AsyncDisplayKit/master/examples_extra/Multiplex/best.png"]; + } + + // unexpected identifier + return nil; +} + +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didFinishDownloadingImageWithIdentifier:(id)imageIdentifier error:(NSError *)error +{ + [self setText:[NSString stringWithFormat:@"loaded '%@'", imageIdentifier]]; + + if ([imageIdentifier isEqualToString:@"best"]) { + [self setText:[_buttonNode.titleNode.attributedText.string stringByAppendingString:@". tap to reload"]]; + _buttonNode.userInteractionEnabled = YES; + } +} + + +#pragma mark - +#pragma mark ASImageDownloaderProtocol. + +- (nullable id)downloadImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgressBlock + completion:(ASImageDownloaderCompletion)completion +{ + // if no callback queue is supplied, run on the main thread + if (callbackQueue == nil) { + callbackQueue = dispatch_get_main_queue(); + } + + // call completion blocks + void (^handler)(NSURLResponse *, NSData *, NSError *) = ^(NSURLResponse *response, NSData *data, NSError *connectionError) { + // add an artificial delay + usleep(1.0 * USEC_PER_SEC); + + // ASMultiplexImageNode callbacks + dispatch_async(callbackQueue, ^{ + if (downloadProgressBlock) { + downloadProgressBlock(1.0f); + } + + if (completion) { + completion([UIImage imageWithData:data], connectionError, nil, nil); + } + }); + }; + + // let NSURLConnection do the heavy lifting + NSURLRequest *request = [NSURLRequest requestWithURL:URL]; + [NSURLConnection sendAsynchronousRequest:request + queue:[[NSOperationQueue alloc] init] + completionHandler:handler]; + + // return nil, don't support cancellation + return nil; +} + +- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier +{ + // no-op, don't support cancellation +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ViewController.h new file mode 100644 index 0000000000..27738fcbe1 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ViewController.m new file mode 100644 index 0000000000..cd687f5bc7 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/ViewController.m @@ -0,0 +1,40 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import "ScreenNode.h" + +@interface ViewController() +{ + ScreenNode *_screenNode; +} + +@end + +@implementation ViewController + +- (instancetype)init +{ + ScreenNode *node = [[ScreenNode alloc] init]; + if (!(self = [super initWithNode:node])) + return nil; + + _screenNode = node; + + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + // This should be done before calling super's viewWillAppear which triggers data fetching on the node. + [_screenNode start]; + [super viewWillAppear:animated]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Multiplex/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/best.png b/submodules/AsyncDisplayKit/examples_extra/Multiplex/best.png new file mode 100644 index 0000000000..d50d8103a6 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Multiplex/best.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/medium.png b/submodules/AsyncDisplayKit/examples_extra/Multiplex/medium.png new file mode 100644 index 0000000000..7c08e0adc0 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Multiplex/medium.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Multiplex/worst.png b/submodules/AsyncDisplayKit/examples_extra/Multiplex/worst.png new file mode 100644 index 0000000000..78727fa98f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Multiplex/worst.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Podfile b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6054b17bd9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,389 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 29A7F3A51A3638DE00CF34F2 /* SlowpokeTextNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29A7F3A41A3638DE00CF34F2 /* SlowpokeTextNode.m */; }; + 29E35E9B1A2F8DB0007B4B17 /* SlowpokeImageNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E35E9A1A2F8DB0007B4B17 /* SlowpokeImageNode.m */; }; + 29E35E9E1A2F8DBC007B4B17 /* SlowpokeShareNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E35E9D1A2F8DBC007B4B17 /* SlowpokeShareNode.m */; }; + 29E35EA01A2F9650007B4B17 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 29E35E9F1A2F9650007B4B17 /* logo.png */; }; + 29E35EA31A2FD0E9007B4B17 /* PostNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E35EA21A2FD0E9007B4B17 /* PostNode.m */; }; + 37563A5CF6FA5205CCAB18B2 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B04B566CE0BBB69447C7222 /* 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 0B04B566CE0BBB69447C7222 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 29A7F3A31A3638DE00CF34F2 /* SlowpokeTextNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SlowpokeTextNode.h; sourceTree = ""; }; + 29A7F3A41A3638DE00CF34F2 /* SlowpokeTextNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SlowpokeTextNode.m; sourceTree = ""; }; + 29E35E991A2F8DB0007B4B17 /* SlowpokeImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SlowpokeImageNode.h; sourceTree = ""; }; + 29E35E9A1A2F8DB0007B4B17 /* SlowpokeImageNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SlowpokeImageNode.m; sourceTree = ""; }; + 29E35E9C1A2F8DBC007B4B17 /* SlowpokeShareNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SlowpokeShareNode.h; sourceTree = ""; }; + 29E35E9D1A2F8DBC007B4B17 /* SlowpokeShareNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SlowpokeShareNode.m; sourceTree = ""; }; + 29E35E9F1A2F9650007B4B17 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; + 29E35EA11A2FD0E9007B4B17 /* PostNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostNode.h; sourceTree = ""; }; + 29E35EA21A2FD0E9007B4B17 /* PostNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostNode.m; sourceTree = ""; }; + 2D75070E7AB95D12F1F0BAB6 /* 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 = ""; }; + 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; }; + 719D9CF4B9A9C5DC6EE4934C /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 37563A5CF6FA5205CCAB18B2 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 29E35E9F1A2F9650007B4B17 /* logo.png */, + 29E35EA11A2FD0E9007B4B17 /* PostNode.h */, + 29E35EA21A2FD0E9007B4B17 /* PostNode.m */, + 29E35E991A2F8DB0007B4B17 /* SlowpokeImageNode.h */, + 29E35E9A1A2F8DB0007B4B17 /* SlowpokeImageNode.m */, + 29E35E9C1A2F8DBC007B4B17 /* SlowpokeShareNode.h */, + 29E35E9D1A2F8DBC007B4B17 /* SlowpokeShareNode.m */, + 29A7F3A31A3638DE00CF34F2 /* SlowpokeTextNode.h */, + 29A7F3A41A3638DE00CF34F2 /* SlowpokeTextNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0B04B566CE0BBB69447C7222 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 719D9CF4B9A9C5DC6EE4934C /* Pods-Sample.debug.xcconfig */, + 2D75070E7AB95D12F1F0BAB6 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + 8DF84F9CA640D8BF98C61D50 /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29E35EA01A2F9650007B4B17 /* logo.png in Resources */, + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 8DF84F9CA640D8BF98C61D50 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29A7F3A51A3638DE00CF34F2 /* SlowpokeTextNode.m in Sources */, + 29E35E9B1A2F8DB0007B4B17 /* SlowpokeImageNode.m in Sources */, + 29E35E9E1A2F8DBC007B4B17 /* SlowpokeShareNode.m in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 29E35EA31A2FD0E9007B4B17 /* PostNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 719D9CF4B9A9C5DC6EE4934C /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D75070E7AB95D12F1F0BAB6 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/AppDelegate.h new file mode 100644 index 0000000000..19db03c153 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/AppDelegate.h @@ -0,0 +1,16 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/AppDelegate.m new file mode 100644 index 0000000000..d0fd66f775 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/PostNode.h b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/PostNode.h new file mode 100644 index 0000000000..646f72232d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/PostNode.h @@ -0,0 +1,14 @@ +// +// PostNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface PostNode : ASDisplayNode + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/PostNode.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/PostNode.m new file mode 100644 index 0000000000..5c8bbd6dad --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/PostNode.m @@ -0,0 +1,92 @@ +// +// PostNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "PostNode.h" + +#import "SlowpokeShareNode.h" +#import "SlowpokeTextNode.h" +#import + +@interface PostNode () +{ + SlowpokeTextNode *_textNode; + SlowpokeShareNode *_needyChildNode; // this node slows down display +} + +@end + +@implementation PostNode + +// turn on to demo that the parent displays a placeholder even if it takes the longest +//+ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +//{ +// usleep( (long)(1.2 * USEC_PER_SEC) ); // artificial delay of 1.2s +// +// // demonstrates that the parent node should also adhere to the placeholder +// [[UIColor colorWithWhite:0.95 alpha:1.0] setFill]; +// UIRectFill(bounds); +//} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _textNode = [[SlowpokeTextNode alloc] init]; + _textNode.placeholderInsets = UIEdgeInsetsMake(3.0, 0.0, 3.0, 0.0); + _textNode.placeholderEnabled = YES; + + NSString *text = @"Etiam porta sem malesuada magna mollis euismod. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh."; + NSDictionary *attributes = @{ NSFontAttributeName: [UIFont systemFontOfSize:17.0] }; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes]; + + _needyChildNode = [[SlowpokeShareNode alloc] init]; + _needyChildNode.opaque = NO; + + [self addSubnode:_textNode]; + [self addSubnode:_needyChildNode]; + + return self; +} + +- (UIImage *)placeholderImage +{ + CGSize size = self.calculatedSize; + if (CGSizeEqualToSize(size, CGSizeZero)) { + return nil; + } + + UIGraphicsBeginImageContext(size); + [[UIColor colorWithWhite:0.9 alpha:1] setFill]; + UIRectFill((CGRect){CGPointZero, size}); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize textSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + CGSize shareSize = [_needyChildNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + return CGSizeMake(constrainedSize.width, textSize.height + 10.0 + shareSize.height); +} + +- (void)layout +{ + [super layout]; + + CGSize textSize = _textNode.calculatedSize; + CGSize needyChildSize = _needyChildNode.calculatedSize; + + _textNode.frame = (CGRect){CGPointZero, textSize}; + _needyChildNode.frame = (CGRect){0.0, CGRectGetMaxY(_textNode.frame) + 10.0, needyChildSize}; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeImageNode.h b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeImageNode.h new file mode 100644 index 0000000000..9c6ef5c42d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeImageNode.h @@ -0,0 +1,14 @@ +// +// SlowpokeImageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface SlowpokeImageNode : ASImageNode + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeImageNode.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeImageNode.m new file mode 100644 index 0000000000..8bf0f25bdf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeImageNode.m @@ -0,0 +1,76 @@ +// +// SlowpokeImageNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "SlowpokeImageNode.h" + +#import + +static CGFloat const kASDKLogoAspectRatio = 2.79; + +@interface ASImageNode (ForwardWorkaround) +// This is a workaround until subclass overriding of custom drawing class methods is fixed +- (UIImage *)displayWithParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock; +@end + +@implementation SlowpokeImageNode + +- (UIImage *)displayWithParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock +{ + usleep( (long)(0.5 * USEC_PER_SEC) ); // artificial delay of 0.5s + + return [super displayWithParameters:parameters isCancelled:isCancelledBlock]; +} + +- (instancetype)init +{ + if (self = [super init]) { + self.placeholderEnabled = YES; + self.placeholderFadeDuration = 0.1; + } + return self; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + if (constrainedSize.width > 0.0) { + return CGSizeMake(constrainedSize.width, constrainedSize.width / kASDKLogoAspectRatio); + } else if (constrainedSize.height > 0.0) { + return CGSizeMake(constrainedSize.height * kASDKLogoAspectRatio, constrainedSize.height); + } + return CGSizeZero; +} + +- (UIImage *)placeholderImage +{ + CGSize size = self.calculatedSize; + if (CGSizeEqualToSize(size, CGSizeZero)) { + return nil; + } + + UIGraphicsBeginImageContext(size); + [[UIColor whiteColor] setFill]; + [[UIColor colorWithWhite:0.9 alpha:1] setStroke]; + + UIRectFill((CGRect){CGPointZero, size}); + + UIBezierPath *path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointZero]; + [path addLineToPoint:(CGPoint){size.width, size.height}]; + [path stroke]; + + [path moveToPoint:(CGPoint){size.width, 0.0}]; + [path addLineToPoint:(CGPoint){0.0, size.height}]; + [path stroke]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeShareNode.h b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeShareNode.h new file mode 100644 index 0000000000..3742854276 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeShareNode.h @@ -0,0 +1,14 @@ +// +// SlowpokeShareNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface SlowpokeShareNode : ASControlNode + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeShareNode.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeShareNode.m new file mode 100644 index 0000000000..3d1e568756 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeShareNode.m @@ -0,0 +1,41 @@ +// +// SlowpokeShareNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "SlowpokeShareNode.h" + +#import + +static NSUInteger const kRingCount = 3; +static CGFloat const kRingStrokeWidth = 1.0; +static CGSize const kIconSize = (CGSize){ 60.0, 17.0 }; + +@implementation SlowpokeShareNode + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + usleep( (long)(0.8 * USEC_PER_SEC) ); // artificial delay of 0.8s + + [[UIColor colorWithRed:0.f green:122/255.f blue:1.f alpha:1.f] setStroke]; + + for (NSUInteger i = 0; i < kRingCount; i++) { + CGFloat x = i * kIconSize.width / kRingCount; + CGRect frame = CGRectMake(x, 0.f, kIconSize.height, kIconSize.height); + CGRect strokeFrame = CGRectInset(frame, kRingStrokeWidth, kRingStrokeWidth); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeFrame cornerRadius:kIconSize.height / 2.f]; + [path setLineWidth:kRingStrokeWidth]; + [path stroke]; + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + return kIconSize; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeTextNode.h b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeTextNode.h new file mode 100644 index 0000000000..069833e53f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeTextNode.h @@ -0,0 +1,14 @@ +// +// SlowpokeTextNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface SlowpokeTextNode : ASTextNode + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeTextNode.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeTextNode.m new file mode 100644 index 0000000000..81cedf3ccd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/SlowpokeTextNode.m @@ -0,0 +1,28 @@ +// +// SlowpokeTextNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "SlowpokeTextNode.h" + +#import + +@interface ASTextNode (ForwardWorkaround) +// This is a workaround until subclass overriding of custom drawing class methods is fixed +- (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; +@end + +@implementation SlowpokeTextNode + +- (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + usleep( (long)(1.0 * USEC_PER_SEC) ); // artificial delay of 1.0 + + [super drawRect:bounds withParameters:parameters isCancelled:isCancelledBlock isRasterizing:isRasterizing]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/ViewController.m new file mode 100644 index 0000000000..d10f83c55d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/ViewController.m @@ -0,0 +1,103 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import + +#import "PostNode.h" +#import "SlowpokeImageNode.h" +#import + +@interface ViewController () +{ + PostNode *_postNode; + SlowpokeImageNode *_imageNode; + UIButton *_displayButton; +} + +@end + + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _displayButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_displayButton setTitle:@"Display me!" forState:UIControlStateNormal]; + [_displayButton addTarget:self action:@selector(onDisplayButton:) forControlEvents:UIControlEventTouchUpInside]; + + UIColor *tintBlue = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0]; + [_displayButton setTitleColor:tintBlue forState:UIControlStateNormal]; + [_displayButton setTitleColor:[tintBlue colorWithAlphaComponent:0.5] forState:UIControlStateHighlighted]; + _displayButton.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_displayButton]; +} + +- (void)viewWillLayoutSubviews +{ + CGFloat padding = 20.0; + CGRect bounds = self.view.bounds; + CGFloat constrainedWidth = CGRectGetWidth(bounds); + CGSize constrainedSize = CGSizeMake(constrainedWidth - 2 * padding, CGFLOAT_MAX); + + CGSize postSize = [_postNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + CGSize imageSize = [_imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; + + _imageNode.frame = (CGRect){padding, padding, imageSize}; + _postNode.frame = (CGRect){padding, CGRectGetMaxY(_imageNode.frame) + 10.0, postSize}; + + CGFloat buttonHeight = 55.0; + _displayButton.frame = (CGRect){0.0, CGRectGetHeight(bounds) - buttonHeight, CGRectGetWidth(bounds), buttonHeight}; +} + +// this method is pretty gross and just for demonstration :] +- (void)createAndDisplayNodes +{ + [_imageNode.view removeFromSuperview]; + [_postNode.view removeFromSuperview]; + + // ASImageNode gets placeholders by default + _imageNode = [[SlowpokeImageNode alloc] init]; + _imageNode.image = [UIImage imageNamed:@"logo"]; + + _postNode = [[PostNode alloc] init]; + + // change to NO to see text placeholders, change to YES to see the parent placeholder + // this placeholder will cover all subnodes while they are displaying, just a like a stage curtain! + _postNode.placeholderEnabled = NO; + + [self.view addSubnode:_imageNode]; + [self.view addSubnode:_postNode]; +} + + +#pragma mark - +#pragma mark Actions + +- (void)onDisplayButton:(id)sender +{ + [self createAndDisplayNodes]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/logo.png b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/logo.png new file mode 100755 index 0000000000..dce1ebc950 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/logo.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Placeholders/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/Podfile b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/Podfile new file mode 100644 index 0000000000..21dc49860f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/Podfile @@ -0,0 +1,9 @@ +platform :ios, '9.0' + +target 'RepoSearcher' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for RepoSearcher + pod 'Texture/IGListKit', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..aa470f0eb7 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj @@ -0,0 +1,442 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 427F7FCA1E58519300D3E11B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FC91E58519300D3E11B /* AppDelegate.swift */; }; + 427F7FCC1E58519300D3E11B /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FCB1E58519300D3E11B /* SearchViewController.swift */; }; + 427F7FD11E58519300D3E11B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 427F7FD01E58519300D3E11B /* Assets.xcassets */; }; + 427F7FDC1E58558C00D3E11B /* LabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FDB1E58558C00D3E11B /* LabelSectionController.swift */; }; + 427F7FDE1E58626A00D3E11B /* SearchSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FDD1E58626A00D3E11B /* SearchSectionController.swift */; }; + 427F7FE01E58627B00D3E11B /* SearchNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FDF1E58627B00D3E11B /* SearchNode.swift */; }; + 427F7FE21E58659600D3E11B /* NSObject+IGListDiffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FE11E58659600D3E11B /* NSObject+IGListDiffable.swift */; }; + 427F7FE71E5868BD00D3E11B /* IGListCollectionContext+ASDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427F7FE61E5868BD00D3E11B /* IGListCollectionContext+ASDK.swift */; }; + 427F7FED1E5872D200D3E11B /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 427F7FEC1E5872D200D3E11B /* Launch Screen.storyboard */; }; + E222079F2736F3FCAE57814E /* Pods_RepoSearcher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD4426BE878430E4E8B66198 /* Pods_RepoSearcher.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 427F7FC61E58519300D3E11B /* RepoSearcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RepoSearcher.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 427F7FC91E58519300D3E11B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 427F7FCB1E58519300D3E11B /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; + 427F7FD01E58519300D3E11B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 427F7FD51E58519300D3E11B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 427F7FDB1E58558C00D3E11B /* LabelSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelSectionController.swift; sourceTree = ""; }; + 427F7FDD1E58626A00D3E11B /* SearchSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchSectionController.swift; sourceTree = ""; }; + 427F7FDF1E58627B00D3E11B /* SearchNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchNode.swift; sourceTree = ""; }; + 427F7FE11E58659600D3E11B /* NSObject+IGListDiffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+IGListDiffable.swift"; sourceTree = ""; }; + 427F7FE61E5868BD00D3E11B /* IGListCollectionContext+ASDK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IGListCollectionContext+ASDK.swift"; sourceTree = ""; }; + 427F7FEC1E5872D200D3E11B /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + DA53F83B08FF5735C4EAA6A5 /* Pods-RepoSearcher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RepoSearcher.release.xcconfig"; path = "Pods/Target Support Files/Pods-RepoSearcher/Pods-RepoSearcher.release.xcconfig"; sourceTree = ""; }; + DD4426BE878430E4E8B66198 /* Pods_RepoSearcher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RepoSearcher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E925D85286FDB929874729EE /* Pods-RepoSearcher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RepoSearcher.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RepoSearcher/Pods-RepoSearcher.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 427F7FC31E58519300D3E11B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E222079F2736F3FCAE57814E /* Pods_RepoSearcher.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 12D677BC1212718498BBD9BF /* Frameworks */ = { + isa = PBXGroup; + children = ( + DD4426BE878430E4E8B66198 /* Pods_RepoSearcher.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 427F7FBD1E58519300D3E11B = { + isa = PBXGroup; + children = ( + 427F7FC81E58519300D3E11B /* RepoSearcher */, + 427F7FC71E58519300D3E11B /* Products */, + 427F7FE91E5869F500D3E11B /* Resources */, + 7EEB49B1AF149712F6D01B0B /* Pods */, + 12D677BC1212718498BBD9BF /* Frameworks */, + ); + sourceTree = ""; + }; + 427F7FC71E58519300D3E11B /* Products */ = { + isa = PBXGroup; + children = ( + 427F7FC61E58519300D3E11B /* RepoSearcher.app */, + ); + name = Products; + sourceTree = ""; + }; + 427F7FC81E58519300D3E11B /* RepoSearcher */ = { + isa = PBXGroup; + children = ( + 427F7FC91E58519300D3E11B /* AppDelegate.swift */, + 427F7FE81E5869EB00D3E11B /* View Controllers */, + 427F7FE41E58664700D3E11B /* Section Controller */, + 427F7FE31E58664000D3E11B /* Nodes */, + 427F7FE51E58666400D3E11B /* Extensions */, + ); + path = RepoSearcher; + sourceTree = ""; + }; + 427F7FE31E58664000D3E11B /* Nodes */ = { + isa = PBXGroup; + children = ( + 427F7FDF1E58627B00D3E11B /* SearchNode.swift */, + ); + name = Nodes; + sourceTree = ""; + }; + 427F7FE41E58664700D3E11B /* Section Controller */ = { + isa = PBXGroup; + children = ( + 427F7FDB1E58558C00D3E11B /* LabelSectionController.swift */, + 427F7FDD1E58626A00D3E11B /* SearchSectionController.swift */, + ); + name = "Section Controller"; + sourceTree = ""; + }; + 427F7FE51E58666400D3E11B /* Extensions */ = { + isa = PBXGroup; + children = ( + 427F7FE11E58659600D3E11B /* NSObject+IGListDiffable.swift */, + 427F7FE61E5868BD00D3E11B /* IGListCollectionContext+ASDK.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 427F7FE81E5869EB00D3E11B /* View Controllers */ = { + isa = PBXGroup; + children = ( + 427F7FCB1E58519300D3E11B /* SearchViewController.swift */, + ); + name = "View Controllers"; + sourceTree = ""; + }; + 427F7FE91E5869F500D3E11B /* Resources */ = { + isa = PBXGroup; + children = ( + 427F7FD01E58519300D3E11B /* Assets.xcassets */, + 427F7FD51E58519300D3E11B /* Info.plist */, + 427F7FEC1E5872D200D3E11B /* Launch Screen.storyboard */, + ); + name = Resources; + path = RepoSearcher; + sourceTree = ""; + }; + 7EEB49B1AF149712F6D01B0B /* Pods */ = { + isa = PBXGroup; + children = ( + E925D85286FDB929874729EE /* Pods-RepoSearcher.debug.xcconfig */, + DA53F83B08FF5735C4EAA6A5 /* Pods-RepoSearcher.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 427F7FC51E58519300D3E11B /* RepoSearcher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 427F7FD81E58519300D3E11B /* Build configuration list for PBXNativeTarget "RepoSearcher" */; + buildPhases = ( + DF4AE1C3A409D227D336F673 /* [CP] Check Pods Manifest.lock */, + 427F7FC21E58519300D3E11B /* Sources */, + 427F7FC31E58519300D3E11B /* Frameworks */, + 427F7FC41E58519300D3E11B /* Resources */, + D44B2BB927A2C0B1D632AB44 /* [CP] Embed Pods Frameworks */, + D6DD975D2C366D8DF2A87634 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RepoSearcher; + productName = RepoSearcher; + productReference = 427F7FC61E58519300D3E11B /* RepoSearcher.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 427F7FBE1E58519300D3E11B /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0820; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "Marvin Nazari"; + TargetAttributes = { + 427F7FC51E58519300D3E11B = { + CreatedOnToolsVersion = 8.2.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 427F7FC11E58519300D3E11B /* Build configuration list for PBXProject "RepoSearcher" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 427F7FBD1E58519300D3E11B; + productRefGroup = 427F7FC71E58519300D3E11B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 427F7FC51E58519300D3E11B /* RepoSearcher */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 427F7FC41E58519300D3E11B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 427F7FED1E5872D200D3E11B /* Launch Screen.storyboard in Resources */, + 427F7FD11E58519300D3E11B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D44B2BB927A2C0B1D632AB44 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-RepoSearcher/Pods-RepoSearcher-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RepoSearcher/Pods-RepoSearcher-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D6DD975D2C366D8DF2A87634 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RepoSearcher/Pods-RepoSearcher-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + DF4AE1C3A409D227D336F673 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RepoSearcher-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 427F7FC21E58519300D3E11B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 427F7FE21E58659600D3E11B /* NSObject+IGListDiffable.swift in Sources */, + 427F7FCC1E58519300D3E11B /* SearchViewController.swift in Sources */, + 427F7FE01E58627B00D3E11B /* SearchNode.swift in Sources */, + 427F7FE71E5868BD00D3E11B /* IGListCollectionContext+ASDK.swift in Sources */, + 427F7FCA1E58519300D3E11B /* AppDelegate.swift in Sources */, + 427F7FDC1E58558C00D3E11B /* LabelSectionController.swift in Sources */, + 427F7FDE1E58626A00D3E11B /* SearchSectionController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 427F7FD61E58519300D3E11B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 427F7FD71E58519300D3E11B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 427F7FD91E58519300D3E11B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E925D85286FDB929874729EE /* Pods-RepoSearcher.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = RepoSearcher/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = wavio.RepoSearcher; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 427F7FDA1E58519300D3E11B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DA53F83B08FF5735C4EAA6A5 /* Pods-RepoSearcher.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = RepoSearcher/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = wavio.RepoSearcher; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 427F7FC11E58519300D3E11B /* Build configuration list for PBXProject "RepoSearcher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 427F7FD61E58519300D3E11B /* Debug */, + 427F7FD71E58519300D3E11B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 427F7FD81E58519300D3E11B /* Build configuration list for PBXNativeTarget "RepoSearcher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 427F7FD91E58519300D3E11B /* Debug */, + 427F7FDA1E58519300D3E11B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 427F7FBE1E58519300D3E11B /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..bd13674404 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..c9f12e24dd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/AppDelegate.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/AppDelegate.swift new file mode 100644 index 0000000000..75583b7cd9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/AppDelegate.swift @@ -0,0 +1,27 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? = { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = .white + return window + }() + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + window?.rootViewController = UINavigationController(rootViewController: SearchViewController()) + window?.makeKeyAndVisible() + + return true + } +} + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..36d2c80d88 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift new file mode 100644 index 0000000000..31a951f349 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift @@ -0,0 +1,17 @@ +// +// IGListCollectionContext+ASDK.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation +import IGListKit +import AsyncDisplayKit + +extension ListCollectionContext { + func nodeForItem(at index: Int, sectionController: ListSectionController) -> ASCellNode? { + return (cellForItem(at: index, sectionController: sectionController) as? _ASCollectionViewCell)?.node + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Info.plist b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Info.plist new file mode 100644 index 0000000000..0f3cc77a42 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launch Screen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift new file mode 100644 index 0000000000..d76dfdd78c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift @@ -0,0 +1,44 @@ +// +// LabelSectionController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation +import AsyncDisplayKit +import IGListKit + +final class LabelSectionController: ListSectionController, ASSectionController { + var object: String? + + func nodeBlockForItem(at index: Int) -> ASCellNodeBlock { + let text = object ?? "" + return { + let node = ASTextCellNode() + node.text = text + return node + } + } + + override func numberOfItems() -> Int { + return 1 + } + + override func didUpdate(to object: Any) { + self.object = String(describing: object) + } + + override func didSelectItem(at index: Int) {} + + //ASDK Replacement + override func sizeForItem(at index: Int) -> CGSize { + return ASIGListSectionControllerMethods.sizeForItem(at: index) + } + + override func cellForItem(at index: Int) -> UICollectionViewCell { + return ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) + } +} + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Launch Screen.storyboard b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Launch Screen.storyboard new file mode 100644 index 0000000000..b90f693902 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/Launch Screen.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift new file mode 100644 index 0000000000..eb95b06c78 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift @@ -0,0 +1,18 @@ +// +// NSObject+IGListDiffable.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import IGListKit + +extension NSObject: ListDiffable { + public func diffIdentifier() -> NSObjectProtocol { + return self + } + public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { + return isEqual(object) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift new file mode 100644 index 0000000000..c25322c9cc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift @@ -0,0 +1,52 @@ +// +// SearchNode.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation +import AsyncDisplayKit + +class SearchNode: ASCellNode { + var searchBarNode: SearchBarNode + + init(delegate: UISearchBarDelegate?) { + self.searchBarNode = SearchBarNode(delegate: delegate) + super.init() + automaticallyManagesSubnodes = true + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASInsetLayoutSpec(insets: .zero, child: searchBarNode) + } +} + +final class SearchBarNode: ASDisplayNode { + + weak var delegate: UISearchBarDelegate? + + init(delegate: UISearchBarDelegate?) { + self.delegate = delegate + super.init() + setViewBlock { + UISearchBar() + } + + style.preferredSize = CGSize(width: UIScreen.main.bounds.width, height: 44) + } + + var searchBar: UISearchBar { + return view as! UISearchBar + } + + override func didLoad() { + super.didLoad() + searchBar.delegate = delegate + searchBar.searchBarStyle = .minimal + searchBar.tintColor = .black + searchBar.backgroundColor = .white + searchBar.placeholder = "Search" + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift new file mode 100644 index 0000000000..def4b10edd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift @@ -0,0 +1,71 @@ +// +// SearchSectionController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import AsyncDisplayKit +import IGListKit + +protocol SearchSectionControllerDelegate: class { + func searchSectionController(_ sectionController: SearchSectionController, didChangeText text: String) +} + +final class SearchSectionController: ListSectionController, ASSectionController { + + weak var delegate: SearchSectionControllerDelegate? + + override init() { + super.init() + scrollDelegate = self + } + + func nodeBlockForItem(at index: Int) -> ASCellNodeBlock { + return { [weak self] in + return SearchNode(delegate: self) + } + } + + override func numberOfItems() -> Int { + return 1 + } + + override func didUpdate(to object: Any) {} + override func didSelectItem(at index: Int) {} + + //ASDK Replacement + override func sizeForItem(at index: Int) -> CGSize { + return ASIGListSectionControllerMethods.sizeForItem(at: index) + } + + override func cellForItem(at index: Int) -> UICollectionViewCell { + return ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) + } +} + +extension SearchSectionController: ListScrollDelegate { + func listAdapter(_ listAdapter: ListAdapter, didScroll sectionController: ListSectionController) { + guard let searchNode = collectionContext?.nodeForItem(at: 0, sectionController: self) as? SearchNode else { return } + + let searchBar = searchNode.searchBarNode.searchBar + searchBar.text = "" + searchBar.resignFirstResponder() + } + + func listAdapter(_ listAdapter: ListAdapter, willBeginDragging sectionController: ListSectionController) {} + func listAdapter(_ listAdapter: ListAdapter, didEndDragging sectionController: ListSectionController, willDecelerate decelerate: Bool) {} + +} + +extension SearchSectionController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + delegate?.searchSectionController(self, didChangeText: searchText) + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + delegate?.searchSectionController(self, didChangeText: "") + } +} + diff --git a/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift new file mode 100644 index 0000000000..59556d4b37 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift @@ -0,0 +1,65 @@ +// +// SearchViewController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit +import IGListKit + +class SearchToken: NSObject {} + +final class SearchViewController: ASViewController { + + lazy var adapter: ListAdapter = { + return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0) + }() + + let words = ["first", "second", "third", "more", "hi", "others"] + + let searchToken = SearchToken() + var filterString = "" + + init() { + let flowLayout = UICollectionViewFlowLayout() + super.init(node: ASCollectionNode(collectionViewLayout: flowLayout)) + adapter.setASDKCollectionNode(node) + adapter.dataSource = self + title = "Search" + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SearchViewController: ListAdapterDataSource { + func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { + if object is SearchToken { + let section = SearchSectionController() + section.delegate = self + return section + } + return LabelSectionController() + } + + func emptyView(for listAdapter: ListAdapter) -> UIView? { + // emptyView dosent work in this secenario, there is always one section (searchbar) present in collection + return nil + } + + func objects(for listAdapter: ListAdapter) -> [ListDiffable] { + guard filterString != "" else { return [searchToken] + words.map { $0 as ListDiffable } } + return [searchToken] + words.filter { $0.lowercased().contains(filterString.lowercased()) }.map { $0 as ListDiffable } + } +} + +extension SearchViewController: SearchSectionControllerDelegate { + func searchSectionController(_ sectionController: SearchSectionController, didChangeText text: String) { + filterString = text + adapter.performUpdates(animated: true, completion: nil) + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Podfile b/submodules/AsyncDisplayKit/examples_extra/Shop/Podfile new file mode 100644 index 0000000000..b12d45be71 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Podfile @@ -0,0 +1,17 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'Shop' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + # use_frameworks! + + # Pods for Shop + +pod 'Texture' + + target 'ShopTests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0008.jpg b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0008.jpg new file mode 100644 index 0000000000..8e0862586d Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0008.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0009.jpg b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0009.jpg new file mode 100644 index 0000000000..9e7906cc60 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0009.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0010.jpg b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0010.jpg new file mode 100644 index 0000000000..3d1209299f Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0010.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0011.jpg b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0011.jpg new file mode 100644 index 0000000000..717deae6b2 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Screenshots/IMG_0011.jpg differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..5c9cc401b9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcodeproj/project.pbxproj @@ -0,0 +1,682 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 27068CAB1DDC5F4400F1A191 /* ProductsLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27068CAA1DDC5F4400F1A191 /* ProductsLayout.swift */; }; + 27120DCB1DD9AB2100123E7E /* ShopCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27120DCA1DD9AB2100123E7E /* ShopCellNode.swift */; }; + 27120DCD1DD9BA1100123E7E /* DummyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27120DCC1DD9BA1100123E7E /* DummyGenerator.swift */; }; + 278BFA221DD4A7B80065BACA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA211DD4A7B80065BACA /* AppDelegate.swift */; }; + 278BFA291DD4A7B80065BACA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 278BFA281DD4A7B80065BACA /* Assets.xcassets */; }; + 278BFA2C1DD4A7B80065BACA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 278BFA2A1DD4A7B80065BACA /* LaunchScreen.storyboard */; }; + 278BFA371DD4A7B80065BACA /* ShopTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA361DD4A7B80065BACA /* ShopTests.swift */; }; + 278BFA4E1DD4ABE80065BACA /* ShopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA4D1DD4ABE80065BACA /* ShopViewController.swift */; }; + 278BFA501DD4AC0C0065BACA /* ProductViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA4F1DD4AC0C0065BACA /* ProductViewController.swift */; }; + 278BFA581DD4B91E0065BACA /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA571DD4B91E0065BACA /* Product.swift */; }; + 278BFA5B1DD4BA720065BACA /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA5A1DD4BA720065BACA /* UIColor.swift */; }; + 278BFA5D1DD4BB270065BACA /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278BFA5C1DD4BB270065BACA /* Category.swift */; }; + 279300201DDAFA06000E9596 /* ProductsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2793001F1DDAFA06000E9596 /* ProductsTableViewController.swift */; }; + 279300221DDAFE40000E9596 /* ProductsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279300211DDAFE40000E9596 /* ProductsCollectionViewController.swift */; }; + 279300241DDAFF5C000E9596 /* ProductNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279300231DDAFF5C000E9596 /* ProductNode.swift */; }; + 279300261DDB0936000E9596 /* ProductTableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279300251DDB0936000E9596 /* ProductTableNode.swift */; }; + 279300281DDB094C000E9596 /* ProductCollectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279300271DDB094C000E9596 /* ProductCollectionNode.swift */; }; + 2793002A1DDB0D9D000E9596 /* StarRatingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279300291DDB0D9D000E9596 /* StarRatingNode.swift */; }; + 2793002E1DDB7819000E9596 /* ProductCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2793002D1DDB7819000E9596 /* ProductCellNode.swift */; }; + 8F5717B4BA9D2C52DF152D58 /* libPods-ShopTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B0D288DC1A9E61BEAAD7610A /* libPods-ShopTests.a */; }; + E7640D0D33E7EF7F0CCA9C7E /* libPods-Shop.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DC5B45681E16CC3AB224974 /* libPods-Shop.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 278BFA331DD4A7B80065BACA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 278BFA161DD4A7B80065BACA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 278BFA1D1DD4A7B80065BACA; + remoteInfo = Shop; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 27068CAA1DDC5F4400F1A191 /* ProductsLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsLayout.swift; sourceTree = ""; }; + 27120DCA1DD9AB2100123E7E /* ShopCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShopCellNode.swift; sourceTree = ""; }; + 27120DCC1DD9BA1100123E7E /* DummyGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DummyGenerator.swift; sourceTree = ""; }; + 278BFA1E1DD4A7B80065BACA /* Shop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Shop.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 278BFA211DD4A7B80065BACA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 278BFA281DD4A7B80065BACA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 278BFA2B1DD4A7B80065BACA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 278BFA2D1DD4A7B80065BACA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 278BFA321DD4A7B80065BACA /* ShopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShopTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 278BFA361DD4A7B80065BACA /* ShopTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopTests.swift; sourceTree = ""; }; + 278BFA381DD4A7B80065BACA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 278BFA4D1DD4ABE80065BACA /* ShopViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShopViewController.swift; sourceTree = ""; }; + 278BFA4F1DD4AC0C0065BACA /* ProductViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductViewController.swift; sourceTree = ""; }; + 278BFA531DD4ACA20065BACA /* Shop-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Shop-Bridging-Header.h"; sourceTree = ""; }; + 278BFA571DD4B91E0065BACA /* Product.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; + 278BFA5A1DD4BA720065BACA /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; + 278BFA5C1DD4BB270065BACA /* Category.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = ""; }; + 2793001F1DDAFA06000E9596 /* ProductsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsTableViewController.swift; sourceTree = ""; }; + 279300211DDAFE40000E9596 /* ProductsCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsCollectionViewController.swift; sourceTree = ""; }; + 279300231DDAFF5C000E9596 /* ProductNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductNode.swift; sourceTree = ""; }; + 279300251DDB0936000E9596 /* ProductTableNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductTableNode.swift; sourceTree = ""; }; + 279300271DDB094C000E9596 /* ProductCollectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductCollectionNode.swift; sourceTree = ""; }; + 279300291DDB0D9D000E9596 /* StarRatingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StarRatingNode.swift; sourceTree = ""; }; + 2793002D1DDB7819000E9596 /* ProductCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductCellNode.swift; sourceTree = ""; }; + 5D73AC819C6A1A66142C6E20 /* Pods-ShopTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShopTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShopTests/Pods-ShopTests.debug.xcconfig"; sourceTree = ""; }; + 6B92A48D8DC2366AED7058DF /* Pods-Shop.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Shop.release.xcconfig"; path = "Pods/Target Support Files/Pods-Shop/Pods-Shop.release.xcconfig"; sourceTree = ""; }; + 9BCAE8CB2E27DCA70B4C59A2 /* Pods-Shop.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Shop.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Shop/Pods-Shop.debug.xcconfig"; sourceTree = ""; }; + 9DC5B45681E16CC3AB224974 /* libPods-Shop.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Shop.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F7CBB92CF3D36E2CB9ADD7E /* Pods-ShopTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShopTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ShopTests/Pods-ShopTests.release.xcconfig"; sourceTree = ""; }; + B0D288DC1A9E61BEAAD7610A /* libPods-ShopTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShopTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 278BFA1B1DD4A7B80065BACA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E7640D0D33E7EF7F0CCA9C7E /* libPods-Shop.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 278BFA2F1DD4A7B80065BACA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F5717B4BA9D2C52DF152D58 /* libPods-ShopTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 278BFA151DD4A7B80065BACA = { + isa = PBXGroup; + children = ( + 278BFA201DD4A7B80065BACA /* Shop */, + 278BFA351DD4A7B80065BACA /* ShopTests */, + 278BFA1F1DD4A7B80065BACA /* Products */, + D6E142ADAC3713000502DF84 /* Pods */, + 8B843E1CE2A6C2C590C60825 /* Frameworks */, + ); + sourceTree = ""; + }; + 278BFA1F1DD4A7B80065BACA /* Products */ = { + isa = PBXGroup; + children = ( + 278BFA1E1DD4A7B80065BACA /* Shop.app */, + 278BFA321DD4A7B80065BACA /* ShopTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 278BFA201DD4A7B80065BACA /* Shop */ = { + isa = PBXGroup; + children = ( + 278BFA591DD4BA590065BACA /* Extensions */, + 278BFA491DD4AB880065BACA /* Scenes */, + 278BFA481DD4AB510065BACA /* Models */, + 278BFA211DD4A7B80065BACA /* AppDelegate.swift */, + 278BFA281DD4A7B80065BACA /* Assets.xcassets */, + 278BFA2A1DD4A7B80065BACA /* LaunchScreen.storyboard */, + 278BFA2D1DD4A7B80065BACA /* Info.plist */, + 278BFA531DD4ACA20065BACA /* Shop-Bridging-Header.h */, + ); + path = Shop; + sourceTree = ""; + }; + 278BFA351DD4A7B80065BACA /* ShopTests */ = { + isa = PBXGroup; + children = ( + 278BFA361DD4A7B80065BACA /* ShopTests.swift */, + 278BFA381DD4A7B80065BACA /* Info.plist */, + ); + path = ShopTests; + sourceTree = ""; + }; + 278BFA481DD4AB510065BACA /* Models */ = { + isa = PBXGroup; + children = ( + 278BFA571DD4B91E0065BACA /* Product.swift */, + 278BFA5C1DD4BB270065BACA /* Category.swift */, + 27120DCC1DD9BA1100123E7E /* DummyGenerator.swift */, + ); + path = Models; + sourceTree = ""; + }; + 278BFA491DD4AB880065BACA /* Scenes */ = { + isa = PBXGroup; + children = ( + 278BFA4C1DD4ABA90065BACA /* Product */, + 278BFA4B1DD4AB9F0065BACA /* Products */, + 278BFA4A1DD4AB950065BACA /* Shop */, + ); + path = Scenes; + sourceTree = ""; + }; + 278BFA4A1DD4AB950065BACA /* Shop */ = { + isa = PBXGroup; + children = ( + 278BFA4D1DD4ABE80065BACA /* ShopViewController.swift */, + 27120DCA1DD9AB2100123E7E /* ShopCellNode.swift */, + ); + path = Shop; + sourceTree = ""; + }; + 278BFA4B1DD4AB9F0065BACA /* Products */ = { + isa = PBXGroup; + children = ( + 2793001F1DDAFA06000E9596 /* ProductsTableViewController.swift */, + 279300211DDAFE40000E9596 /* ProductsCollectionViewController.swift */, + 279300251DDB0936000E9596 /* ProductTableNode.swift */, + 279300271DDB094C000E9596 /* ProductCollectionNode.swift */, + 27068CAA1DDC5F4400F1A191 /* ProductsLayout.swift */, + ); + path = Products; + sourceTree = ""; + }; + 278BFA4C1DD4ABA90065BACA /* Product */ = { + isa = PBXGroup; + children = ( + 278BFA4F1DD4AC0C0065BACA /* ProductViewController.swift */, + 279300231DDAFF5C000E9596 /* ProductNode.swift */, + 279300291DDB0D9D000E9596 /* StarRatingNode.swift */, + 2793002D1DDB7819000E9596 /* ProductCellNode.swift */, + ); + path = Product; + sourceTree = ""; + }; + 278BFA591DD4BA590065BACA /* Extensions */ = { + isa = PBXGroup; + children = ( + 278BFA5A1DD4BA720065BACA /* UIColor.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 8B843E1CE2A6C2C590C60825 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9DC5B45681E16CC3AB224974 /* libPods-Shop.a */, + B0D288DC1A9E61BEAAD7610A /* libPods-ShopTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + D6E142ADAC3713000502DF84 /* Pods */ = { + isa = PBXGroup; + children = ( + 9BCAE8CB2E27DCA70B4C59A2 /* Pods-Shop.debug.xcconfig */, + 6B92A48D8DC2366AED7058DF /* Pods-Shop.release.xcconfig */, + 5D73AC819C6A1A66142C6E20 /* Pods-ShopTests.debug.xcconfig */, + 9F7CBB92CF3D36E2CB9ADD7E /* Pods-ShopTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 278BFA1D1DD4A7B80065BACA /* Shop */ = { + isa = PBXNativeTarget; + buildConfigurationList = 278BFA3B1DD4A7B80065BACA /* Build configuration list for PBXNativeTarget "Shop" */; + buildPhases = ( + 53B8D1D4907B5E11F1FB4D84 /* [CP] Check Pods Manifest.lock */, + 278BFA1A1DD4A7B80065BACA /* Sources */, + 278BFA1B1DD4A7B80065BACA /* Frameworks */, + 278BFA1C1DD4A7B80065BACA /* Resources */, + 7A9094E163FF7B834F7D5B76 /* [CP] Embed Pods Frameworks */, + 89C7C85237928A7AA1AFD80E /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Shop; + productName = Shop; + productReference = 278BFA1E1DD4A7B80065BACA /* Shop.app */; + productType = "com.apple.product-type.application"; + }; + 278BFA311DD4A7B80065BACA /* ShopTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 278BFA3E1DD4A7B80065BACA /* Build configuration list for PBXNativeTarget "ShopTests" */; + buildPhases = ( + 21A817F76279E69128E48719 /* [CP] Check Pods Manifest.lock */, + 278BFA2E1DD4A7B80065BACA /* Sources */, + 278BFA2F1DD4A7B80065BACA /* Frameworks */, + 278BFA301DD4A7B80065BACA /* Resources */, + 4E2E9451B168505B69D5EA0F /* [CP] Embed Pods Frameworks */, + D97894A2A1E9AC5B6FFB9271 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 278BFA341DD4A7B80065BACA /* PBXTargetDependency */, + ); + name = ShopTests; + productName = ShopTests; + productReference = 278BFA321DD4A7B80065BACA /* ShopTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 278BFA161DD4A7B80065BACA /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0810; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = Dimitri; + TargetAttributes = { + 278BFA1D1DD4A7B80065BACA = { + CreatedOnToolsVersion = 8.1; + DevelopmentTeam = K3L9PW54G7; + LastSwiftMigration = 0810; + ProvisioningStyle = Automatic; + }; + 278BFA311DD4A7B80065BACA = { + CreatedOnToolsVersion = 8.1; + DevelopmentTeam = K3L9PW54G7; + ProvisioningStyle = Automatic; + TestTargetID = 278BFA1D1DD4A7B80065BACA; + }; + }; + }; + buildConfigurationList = 278BFA191DD4A7B80065BACA /* Build configuration list for PBXProject "Shop" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 278BFA151DD4A7B80065BACA; + productRefGroup = 278BFA1F1DD4A7B80065BACA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 278BFA1D1DD4A7B80065BACA /* Shop */, + 278BFA311DD4A7B80065BACA /* ShopTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 278BFA1C1DD4A7B80065BACA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 278BFA2C1DD4A7B80065BACA /* LaunchScreen.storyboard in Resources */, + 278BFA291DD4A7B80065BACA /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 278BFA301DD4A7B80065BACA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 21A817F76279E69128E48719 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ShopTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 4E2E9451B168505B69D5EA0F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ShopTests/Pods-ShopTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 53B8D1D4907B5E11F1FB4D84 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Shop-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7A9094E163FF7B834F7D5B76 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Shop/Pods-Shop-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 89C7C85237928A7AA1AFD80E /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Shop/Pods-Shop-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + D97894A2A1E9AC5B6FFB9271 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ShopTests/Pods-ShopTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 278BFA1A1DD4A7B80065BACA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2793002A1DDB0D9D000E9596 /* StarRatingNode.swift in Sources */, + 27068CAB1DDC5F4400F1A191 /* ProductsLayout.swift in Sources */, + 278BFA501DD4AC0C0065BACA /* ProductViewController.swift in Sources */, + 27120DCD1DD9BA1100123E7E /* DummyGenerator.swift in Sources */, + 278BFA5B1DD4BA720065BACA /* UIColor.swift in Sources */, + 278BFA4E1DD4ABE80065BACA /* ShopViewController.swift in Sources */, + 279300261DDB0936000E9596 /* ProductTableNode.swift in Sources */, + 279300201DDAFA06000E9596 /* ProductsTableViewController.swift in Sources */, + 2793002E1DDB7819000E9596 /* ProductCellNode.swift in Sources */, + 278BFA581DD4B91E0065BACA /* Product.swift in Sources */, + 279300221DDAFE40000E9596 /* ProductsCollectionViewController.swift in Sources */, + 27120DCB1DD9AB2100123E7E /* ShopCellNode.swift in Sources */, + 279300241DDAFF5C000E9596 /* ProductNode.swift in Sources */, + 278BFA221DD4A7B80065BACA /* AppDelegate.swift in Sources */, + 278BFA5D1DD4BB270065BACA /* Category.swift in Sources */, + 279300281DDB094C000E9596 /* ProductCollectionNode.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 278BFA2E1DD4A7B80065BACA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 278BFA371DD4A7B80065BACA /* ShopTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 278BFA341DD4A7B80065BACA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 278BFA1D1DD4A7B80065BACA /* Shop */; + targetProxy = 278BFA331DD4A7B80065BACA /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 278BFA2A1DD4A7B80065BACA /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 278BFA2B1DD4A7B80065BACA /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 278BFA391DD4A7B80065BACA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 278BFA3A1DD4A7B80065BACA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 278BFA3C1DD4A7B80065BACA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9BCAE8CB2E27DCA70B4C59A2 /* Pods-Shop.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = K3L9PW54G7; + INFOPLIST_FILE = Shop/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.sample.Shop; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Shop/Shop-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 278BFA3D1DD4A7B80065BACA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6B92A48D8DC2366AED7058DF /* Pods-Shop.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = K3L9PW54G7; + INFOPLIST_FILE = Shop/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.sample.Shop; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Shop/Shop-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 278BFA3F1DD4A7B80065BACA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5D73AC819C6A1A66142C6E20 /* Pods-ShopTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = K3L9PW54G7; + INFOPLIST_FILE = ShopTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.sample.ShopTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Shop.app/Shop"; + }; + name = Debug; + }; + 278BFA401DD4A7B80065BACA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9F7CBB92CF3D36E2CB9ADD7E /* Pods-ShopTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = K3L9PW54G7; + INFOPLIST_FILE = ShopTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.sample.ShopTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Shop.app/Shop"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 278BFA191DD4A7B80065BACA /* Build configuration list for PBXProject "Shop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 278BFA391DD4A7B80065BACA /* Debug */, + 278BFA3A1DD4A7B80065BACA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 278BFA3B1DD4A7B80065BACA /* Build configuration list for PBXNativeTarget "Shop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 278BFA3C1DD4A7B80065BACA /* Debug */, + 278BFA3D1DD4A7B80065BACA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 278BFA3E1DD4A7B80065BACA /* Build configuration list for PBXNativeTarget "ShopTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 278BFA3F1DD4A7B80065BACA /* Debug */, + 278BFA401DD4A7B80065BACA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 278BFA161DD4A7B80065BACA /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..951e8297e2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a8472b4e8a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/AppDelegate.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/AppDelegate.swift new file mode 100644 index 0000000000..ebae7efc3a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/AppDelegate.swift @@ -0,0 +1,55 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + + UINavigationBar.appearance().tintColor = UIColor.white + UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white] + UINavigationBar.appearance().barTintColor = UIColor.primaryBarTintColor() + + self.window = UIWindow(frame: UIScreen.main.bounds) + self.window?.backgroundColor = UIColor.white + self.window?.rootViewController = UINavigationController(rootViewController: ShopViewController()) + self.window?.makeKeyAndVisible() + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // 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 invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // 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. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // 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. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..b8236c6534 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,48 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Contents.json b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/Contents.json b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/Contents.json new file mode 100644 index 0000000000..bf7a318771 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "filled_star.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "filled_star-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "filled_star-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star-1.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star-1.png new file mode 100644 index 0000000000..21884f8fe8 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star-1.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star-2.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star-2.png new file mode 100644 index 0000000000..21884f8fe8 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star-2.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star.png new file mode 100644 index 0000000000..21884f8fe8 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/filled_star.imageset/filled_star.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/Contents.json new file mode 100644 index 0000000000..70a59709db --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "category_placeholder-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "category_placeholder-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "category_placeholder.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder-1.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder-1.png new file mode 100644 index 0000000000..8dec56ca18 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder-1.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder-2.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder-2.png new file mode 100644 index 0000000000..8dec56ca18 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder-2.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder.png new file mode 100644 index 0000000000..8dec56ca18 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/placeholder.imageset/category_placeholder.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/Contents.json b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/Contents.json new file mode 100644 index 0000000000..8a56ccde81 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "unfilled_star.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "unfilled_star-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "unfilled_star-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star-1.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star-1.png new file mode 100644 index 0000000000..a8c225bf04 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star-1.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star-2.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star-2.png new file mode 100644 index 0000000000..a8c225bf04 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star-2.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star.png b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star.png new file mode 100644 index 0000000000..a8c225bf04 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Assets.xcassets/Shop/unfilled_star.imageset/unfilled_star.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..fdf3f97d1b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Extensions/UIColor.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Extensions/UIColor.swift new file mode 100644 index 0000000000..4d6444f131 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Extensions/UIColor.swift @@ -0,0 +1,29 @@ +// +// UIColor.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +extension UIColor { + + class func primaryBackgroundColor() -> UIColor { + return UIColor.init(red: 237/255, green: 239/255, blue: 242/255, alpha: 1.0) + } + + class func primaryBarTintColor() -> UIColor { + return UIColor.init(red: 57/255, green: 59/255, blue: 63/255, alpha: 1.0) + } + + class func containerBackgroundColor() -> UIColor { + return UIColor.init(red: 255/255, green: 255/255, blue: 255/255, alpha: 1.0) + } + + class func containerBorderColor() -> UIColor { + return UIColor.init(red: 231/255, green: 232/255, blue: 235/255, alpha: 1.0) + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Info.plist b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Info.plist new file mode 100644 index 0000000000..ce18bd2acf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Models/Category.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Models/Category.swift new file mode 100644 index 0000000000..d5e4949230 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Models/Category.swift @@ -0,0 +1,26 @@ +// +// Category.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +struct Category { + + var id: String = UUID().uuidString + var imageURL: String + var numberOfProducts: Int = 0 + var title: String + var products: [Product] + + init(title: String, imageURL: String, products: [Product]) { + self.title = title + self.imageURL = imageURL + self.products = products + self.numberOfProducts = products.count + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Models/DummyGenerator.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Models/DummyGenerator.swift new file mode 100644 index 0000000000..d68e43751f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Models/DummyGenerator.swift @@ -0,0 +1,160 @@ +// +// DummyGenerator.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation + +class DummyGenerator { + + static let sharedGenerator = DummyGenerator() + + // MARK: - Variables + + private let numberOfCategories = 15 + private let imageURLs = ["https://placebear.com/200/200", + "https://placebear.com/200/250", + "https://placebear.com/250/250", + "https://placebear.com/300/200", + "https://placebear.com/300/250", + "https://placebear.com/300/300", + "https://placebear.com/350/200", + "https://placebear.com/350/250", + "https://placebear.com/350/300"] + + // MARK: - Private initializer + + private init() { + + } + + // MARK: - Generate random categories + + func randomCategories() -> [Category] { + var categories: [Category] = [] + for _ in 0.. [Product] { + var products: [Product] = [] + for _ in 0.. String { + return compose(provider: { word }, count: count, middleSeparator: .Space) + } + + public static var sentence: String { + let numberOfWordsInSentence = Int.random(min: 8, max: 16) + let capitalizeFirstLetterDecorator: (String) -> String = { $0.stringWithCapitalizedFirstLetter } + return compose(provider: { word }, count: numberOfWordsInSentence, middleSeparator: .Space, endSeparator: .Dot, decorator: capitalizeFirstLetterDecorator) + } + + public static func sentences(count: Int) -> String { + return compose(provider: { sentence }, count: count, middleSeparator: .Space) + } + + public static var paragraph: String { + let numberOfSentencesInParagraph = Int.random(min: 4, max: 10) + return sentences(count: numberOfSentencesInParagraph) + } + + public static func paragraphs(count: Int) -> String { + return compose(provider: { paragraph }, count: count, middleSeparator: .NewLine) + } + + public static var title: String { + let numberOfWordsInTitle = Int.random(min: 1, max: 2) + let capitalizeStringDecorator: (String) -> String = { $0.capitalized } + return compose(provider: { word }, count: numberOfWordsInTitle, middleSeparator: .Space, decorator: capitalizeStringDecorator) + } + + private enum Separator: String { + case None = "" + case Space = " " + case Dot = "." + case NewLine = "\n" + } + + private static func compose(provider: () -> String, count: Int, middleSeparator: Separator, endSeparator: Separator = .None, decorator: ((String) -> String)? = nil) -> String { + var composedString = "" + + for index in 0.. Element { + let index = Int(arc4random_uniform(UInt32(self.count))) + return self[index] + } +} + +private extension Int { + static func random(min: Int = 0, max: Int) -> Int { + assert(min >= 0) + assert(min < max) + + return Int(arc4random_uniform(UInt32((max - min) + 1))) + min + } +} + +private extension Array { + var randomElement: Element { + return self[Int.random(max: count - 1)] + } +} + +private extension String { + var stringWithCapitalizedFirstLetter: String { + let firstLetterRange = startIndex.. ASLayoutSpec { + return ASInsetLayoutSpec(insets: UIEdgeInsets.zero, child: self.productNode) + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift new file mode 100644 index 0000000000..8dda39d137 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift @@ -0,0 +1,118 @@ +// +// ProductNode.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductNode: ASDisplayNode { + + // MARK: - Variables + + private let imageNode: ASNetworkImageNode + private let titleNode: ASTextNode + private let priceNode: ASTextNode + private let starRatingNode: StarRatingNode + private let reviewsNode: ASTextNode + private let descriptionNode: ASTextNode + + private let product: Product + + // MARK: - Object life cycle + + init(product: Product) { + self.product = product + + imageNode = ASNetworkImageNode() + titleNode = ASTextNode() + starRatingNode = StarRatingNode(rating: product.starRating) + priceNode = ASTextNode() + reviewsNode = ASTextNode() + descriptionNode = ASTextNode() + + super.init() + self.setupNodes() + self.buildNodeHierarchy() + } + + // MARK: - Setup nodes + + private func setupNodes() { + self.setupImageNode() + self.setupTitleNode() + self.setupDescriptionNode() + self.setupPriceNode() + self.setupReviewsNode() + } + + private func setupImageNode() { + self.imageNode.url = URL(string: self.product.imageURL) + self.imageNode.style.preferredSize = CGSize(width: UIScreen.main.bounds.width, height: 300) + } + + private func setupTitleNode() { + self.titleNode.attributedText = NSAttributedString(string: self.product.title, attributes: self.titleTextAttributes()) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + } + + private var titleTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.black, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 16)] + } + + private func setupDescriptionNode() { + self.descriptionNode.attributedText = NSAttributedString(string: self.product.descriptionText, attributes: self.descriptionTextAttributes()) + self.descriptionNode.maximumNumberOfLines = 0 + } + + private var descriptionTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.darkGray, NSFontAttributeName: UIFont.systemFont(ofSize: 14)] + } + + private func setupPriceNode() { + self.priceNode.attributedText = NSAttributedString(string: self.product.currency + " \(self.product.price)", attributes: self.priceTextAttributes()) + } + + private var priceTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.red, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 15)] + } + + private func setupReviewsNode() { + self.reviewsNode.attributedText = NSAttributedString(string: "\(self.product.numberOfReviews) reviews", attributes: self.reviewsTextAttributes()) + } + + private var reviewsTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.lightGray, NSFontAttributeName: UIFont.systemFont(ofSize: 14)] + } + + // MARK: - Build node hierarchy + + private func buildNodeHierarchy() { + self.addSubnode(imageNode) + self.addSubnode(titleNode) + self.addSubnode(descriptionNode) + self.addSubnode(starRatingNode) + self.addSubnode(priceNode) + self.addSubnode(reviewsNode) + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1 + self.titleNode.style.flexShrink = 1 + let titlePriceSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 2.0, justifyContent: .start, alignItems: .center, children: [self.titleNode, spacer, self.priceNode]) + titlePriceSpec.style.alignSelf = .stretch + let starRatingReviewsSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 25.0, justifyContent: .start, alignItems: .center, children: [self.starRatingNode, self.reviewsNode]) + let contentSpec = ASStackLayoutSpec(direction: .vertical, spacing: 8.0, justifyContent: .start, alignItems: .stretch, children: [titlePriceSpec, starRatingReviewsSpec, self.descriptionNode]) + contentSpec.style.flexShrink = 1 + let insetSpec = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(12.0, 12.0, 12.0, 12.0), child: contentSpec) + let finalSpec = ASStackLayoutSpec(direction: .vertical, spacing: 5.0, justifyContent: .start, alignItems: .center, children: [self.imageNode, insetSpec]) + return finalSpec + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/ProductViewController.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/ProductViewController.swift new file mode 100644 index 0000000000..9d36298e1d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/ProductViewController.swift @@ -0,0 +1,62 @@ +// +// ProductViewController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductViewController: ASViewController { + + // MARK: - Variables + + let product: Product + + private var tableNode: ASTableNode { + return node + } + + // MARK: - Object life cycle + + init(product: Product) { + self.product = product + super.init(node: ASTableNode()) + tableNode.delegate = self + tableNode.dataSource = self + tableNode.backgroundColor = UIColor.primaryBackgroundColor() + tableNode.view.separatorStyle = .none + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View life cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setupTitle() + } + +} + +extension ProductViewController: ASTableDataSource, ASTableDelegate { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + func tableView(_ tableView: ASTableView, nodeForRowAt indexPath: IndexPath) -> ASCellNode { + let node = ProductCellNode(product: self.product) + return node + } + +} + +extension ProductViewController { + func setupTitle() { + self.title = self.product.title + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift new file mode 100644 index 0000000000..21c373c858 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift @@ -0,0 +1,59 @@ +// +// StarRatingNode.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class StarRatingNode: ASDisplayNode { + + // MARK: - Variable + + private lazy var starSize: CGSize = { + return CGSize(width: 15, height: 15) + }() + + private let rating: Int + + private var starImageNodes: [ASDisplayNode] = [] + + // MARK: - Object life cycle + + init(rating: Int) { + self.rating = rating + super.init() + + self.setupStarNodes() + self.buildNodeHierarchy() + } + + // MARK: - Star nodes setup + + private func setupStarNodes() { + for i in 0..<5 { + let imageNode = ASImageNode() + imageNode.image = i <= self.rating ? UIImage(named: "filled_star") : UIImage(named: "unfilled_star") + imageNode.style.preferredSize = self.starSize + self.starImageNodes.append(imageNode) + } + } + + // MARK: - Build node hierarchy + + private func buildNodeHierarchy() { + for node in self.starImageNodes { + self.addSubnode(node) + } + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let layoutSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 5, justifyContent: .start, alignItems: .stretch, children: self.starImageNodes) + return layoutSpec + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductCollectionNode.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductCollectionNode.swift new file mode 100644 index 0000000000..5c95586fed --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductCollectionNode.swift @@ -0,0 +1,72 @@ +// +// ProductCollectionNode.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductCollectionNode: ASCellNode { + + // MARK: - Variables + + private let containerNode: ContainerNode + + // MARK: - Object life cycle + + init(product: Product) { + self.containerNode = ContainerNode(node: ProductContentNode(product: product)) + super.init() + self.selectionStyle = .none + self.addSubnode(self.containerNode) + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let insets = UIEdgeInsetsMake(2, 2, 2, 2) + return ASInsetLayoutSpec(insets: insets, child: self.containerNode) + } + +} + +class ProductContentNode: ASDisplayNode { + + // MARK: - Variables + + private let imageNode: ASNetworkImageNode + private let titleNode: ASTextNode + private let subtitleNode: ASTextNode + + // MARK: - Object life cycle + + init(product: Product) { + imageNode = ASNetworkImageNode() + imageNode.url = URL(string: product.imageURL) + + titleNode = ASTextNode() + let title = NSAttributedString(string: product.title, attributes: [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 17)]) + titleNode.attributedText = title + + subtitleNode = ASTextNode() + let subtitle = NSAttributedString(string: product.currency + " \(product.price)", attributes: [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 15)]) + subtitleNode.attributedText = subtitle + + super.init() + + self.imageNode.addSubnode(self.titleNode) + self.imageNode.addSubnode(self.subtitleNode) + self.addSubnode(self.imageNode) + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let textNodesStack = ASStackLayoutSpec(direction: .vertical, spacing: 5, justifyContent: .end, alignItems: .stretch, children: [self.titleNode, self.subtitleNode]) + let insetStack = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(CGFloat.infinity, 10, 10, 10), child: textNodesStack) + return ASOverlayLayoutSpec(child: self.imageNode, overlay: insetStack) + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift new file mode 100644 index 0000000000..816d405dcf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift @@ -0,0 +1,123 @@ +// +// ProductTableNode.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductTableNode: ASCellNode { + + // MARK: - Variables + + private lazy var imageSize: CGSize = { + return CGSize(width: 80, height: 80) + }() + + private let product: Product + + private let imageNode: ASNetworkImageNode + private let titleNode: ASTextNode + private let subtitleNode: ASTextNode + private let starRatingNode: StarRatingNode + private let priceNode: ASTextNode + private let separatorNode: ASDisplayNode + + // MARK: - Object life cycle + + init(product: Product) { + self.product = product + + imageNode = ASNetworkImageNode() + titleNode = ASTextNode() + subtitleNode = ASTextNode() + starRatingNode = StarRatingNode(rating: product.starRating) + priceNode = ASTextNode() + separatorNode = ASDisplayNode() + + super.init() + self.setupNodes() + self.buildNodeHierarchy() + } + + // MARK: - Setup nodes + + private func setupNodes() { + self.setupImageNode() + self.setupTitleNode() + self.setupSubtitleNode() + self.setupPriceNode() + self.setupSeparatorNode() + } + + private func setupImageNode() { + self.imageNode.url = URL(string: self.product.imageURL) + self.imageNode.style.preferredSize = self.imageSize + } + + private func setupTitleNode() { + self.titleNode.attributedText = NSAttributedString(string: self.product.title, attributes: self.titleTextAttributes()) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + } + + private var titleTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.black, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 16)] + } + + private func setupSubtitleNode() { + self.subtitleNode.attributedText = NSAttributedString(string: self.product.descriptionText, attributes: self.subtitleTextAttributes()) + self.subtitleNode.maximumNumberOfLines = 2 + self.subtitleNode.truncationMode = .byTruncatingTail + } + + private var subtitleTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.darkGray, NSFontAttributeName: UIFont.systemFont(ofSize: 14)] + } + + private func setupPriceNode() { + self.priceNode.attributedText = NSAttributedString(string: self.product.currency + " \(self.product.price)", attributes: self.priceTextAttributes()) + } + + private var priceTextAttributes = { + return [NSForegroundColorAttributeName: UIColor.red, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 15)] + } + + private func setupSeparatorNode() { + self.separatorNode.backgroundColor = UIColor.lightGray + } + + // MARK: - Build node hierarchy + + private func buildNodeHierarchy() { + self.addSubnode(imageNode) + self.addSubnode(titleNode) + self.addSubnode(subtitleNode) + self.addSubnode(starRatingNode) + self.addSubnode(priceNode) + self.addSubnode(separatorNode) + } + + // MARK: - Layout + + override func layout() { + super.layout() + let separatorHeight = 1 / UIScreen.main.scale + self.separatorNode.frame = CGRect(x: 0.0, y: 0.0, width: self.calculatedSize.width, height: separatorHeight) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1 + self.titleNode.style.flexShrink = 1 + let titlePriceSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 2.0, justifyContent: .start, alignItems: .center, children: [self.titleNode, spacer, self.priceNode]) + titlePriceSpec.style.alignSelf = .stretch + let contentSpec = ASStackLayoutSpec(direction: .vertical, spacing: 4.0, justifyContent: .start, alignItems: .stretch, children: [titlePriceSpec, self.subtitleNode, self.starRatingNode]) + contentSpec.style.flexShrink = 1 + let finalSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 10.0, justifyContent: .start, alignItems: .start, children: [self.imageNode, contentSpec]) + return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0), child: finalSpec) + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsCollectionViewController.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsCollectionViewController.swift new file mode 100644 index 0000000000..caa5278f07 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsCollectionViewController.swift @@ -0,0 +1,69 @@ +// +// ProductsCollectionViewController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductsCollectionViewController: ASViewController { + + // MARK: - Variables + + var products: [Product] + + private var collectionNode: ASCollectionNode { + return node + } + + // MARK: - Object life cycle + + init(products: [Product]) { + self.products = products + super.init(node: ASCollectionNode(collectionViewLayout: ProductsLayout())) + collectionNode.delegate = self + collectionNode.dataSource = self + collectionNode.backgroundColor = UIColor.primaryBackgroundColor() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View life cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setupTitle() + } + +} + +extension ProductsCollectionViewController: ASCollectionDataSource, ASCollectionDelegate { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.products.count + } + + func collectionView(_ collectionView: ASCollectionView, nodeForItemAt indexPath: IndexPath) -> ASCellNode { + let product = self.products[indexPath.row] + return ProductCollectionNode(product: product) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let product = self.products[indexPath.row] + let viewController = ProductViewController(product: product) + self.navigationController?.pushViewController(viewController, animated: true) + } + +} + +extension ProductsCollectionViewController { + + func setupTitle() { + self.title = "Bears" + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsLayout.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsLayout.swift new file mode 100644 index 0000000000..67f4a77837 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsLayout.swift @@ -0,0 +1,55 @@ +// +// ProductsLayout.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductsLayout: UICollectionViewFlowLayout { + + // MARK: - Variables + + let itemHeight: CGFloat = 220 + let numberOfColumns: CGFloat = 2 + + // MARK: - Object life cycle + + override init() { + super.init() + self.setupLayout() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupLayout() + } + + // MARK: - Layout + + private func setupLayout() { + self.minimumInteritemSpacing = 0 + self.minimumLineSpacing = 0 + self.scrollDirection = .vertical + } + + func itemWidth() -> CGFloat { + return (collectionView!.frame.width / numberOfColumns) + } + + override var itemSize: CGSize { + set { + self.itemSize = CGSize(width: itemWidth(), height: itemHeight) + } + get { + return CGSize(width: itemWidth(), height: itemHeight) + } + } + + override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { + return self.collectionView!.contentOffset + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift new file mode 100644 index 0000000000..bfb851a3f6 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift @@ -0,0 +1,78 @@ +// +// ProductsTableViewController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ProductsTableViewController: ASViewController { + + // MARK: - Variables + + var products: [Product] + + private var tableNode: ASTableNode { + return node + } + + // MARK: - Object life cycle + + init(products: [Product]) { + self.products = products + super.init(node: ASTableNode()) + tableNode.delegate = self + tableNode.dataSource = self + tableNode.backgroundColor = UIColor.white + tableNode.view.separatorStyle = .none + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View life cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setupTitle() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let indexPath = self.tableNode.indexPathForSelectedRow { + self.tableNode.view.deselectRow(at: indexPath, animated: true) + } + } + +} + +extension ProductsTableViewController: ASTableDataSource, ASTableDelegate { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.products.count + } + + func tableView(_ tableView: ASTableView, nodeForRowAt indexPath: IndexPath) -> ASCellNode { + let product = self.products[indexPath.row] + let node = ProductTableNode(product: product) + return node + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let product = self.products[indexPath.row] + let viewController = ProductViewController(product: product) + self.navigationController?.pushViewController(viewController, animated: true) + } + +} + +extension ProductsTableViewController { + + func setupTitle() { + self.title = "Bears" + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Shop/ShopCellNode.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Shop/ShopCellNode.swift new file mode 100644 index 0000000000..5e71328296 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Shop/ShopCellNode.swift @@ -0,0 +1,105 @@ +// +// ShopCellNode.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ShopCellNode: ASCellNode { + + // MARK: - Variables + + private let containerNode: ContainerNode + private let categoryNode: CategoryNode + + // MARK: - Object life cycle + + init(category: Category) { + categoryNode = CategoryNode(category: category) + containerNode = ContainerNode(node: categoryNode) + super.init() + self.selectionStyle = .none + self.addSubnode(self.containerNode) + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(5, 10, 5, 10), child: self.containerNode) + } + +} + +class ContainerNode: ASDisplayNode { + + // MARK: - Variables + + private let contentNode: ASDisplayNode + + // MARK: - Object life cycle + + init(node: ASDisplayNode) { + contentNode = node + super.init() + self.backgroundColor = UIColor.containerBackgroundColor() + self.addSubnode(self.contentNode) + } + + // MARK: - Node life cycle + + override func didLoad() { + super.didLoad() + self.layer.cornerRadius = 5.0 + self.layer.borderColor = UIColor.containerBorderColor().cgColor + self.layer.borderWidth = 1.0 + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(8, 8, 8, 8), child: self.contentNode) + } + +} + +class CategoryNode: ASDisplayNode { + + // MARK: - Variables + + private let imageNode: ASNetworkImageNode + private let titleNode: ASTextNode + private let subtitleNode: ASTextNode + + // MARK: - Object life cycle + + init(category: Category) { + imageNode = ASNetworkImageNode() + imageNode.url = URL(string: category.imageURL) + + titleNode = ASTextNode() + let title = NSAttributedString(string: category.title, attributes: [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 17)]) + titleNode.attributedText = title + + subtitleNode = ASTextNode() + let subtitle = NSAttributedString(string: "\(category.numberOfProducts) products", attributes: [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 15)]) + subtitleNode.attributedText = subtitle + + super.init() + + self.imageNode.addSubnode(self.titleNode) + self.imageNode.addSubnode(self.subtitleNode) + self.addSubnode(self.imageNode) + } + + // MARK: - Layout + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let textNodesStack = ASStackLayoutSpec(direction: .vertical, spacing: 5, justifyContent: .end, alignItems: .stretch, children: [self.titleNode, self.subtitleNode]) + let insetStack = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(CGFloat.infinity, 10, 10, 10), child: textNodesStack) + return ASOverlayLayoutSpec(child: self.imageNode, overlay: insetStack) + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift new file mode 100644 index 0000000000..8feefa3f25 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift @@ -0,0 +1,91 @@ +// +// ShopViewController.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +class ShopViewController: ASViewController { + + // MARK: - Variables + + lazy var categories: [Category] = { + return DummyGenerator.sharedGenerator.randomCategories() + }() + + private var tableNode: ASTableNode { + return node + } + + // MARK: - Object life cycle + + init() { + super.init(node: ASTableNode()) + tableNode.delegate = self + tableNode.dataSource = self + tableNode.backgroundColor = UIColor.primaryBackgroundColor() + tableNode.view.separatorStyle = .none + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View life cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setupTitle() + } + +} + +extension ShopViewController: ASTableDataSource, ASTableDelegate { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.categories.count + } + + func tableView(_ tableView: ASTableView, nodeForRowAt indexPath: IndexPath) -> ASCellNode { + let category = self.categories[indexPath.row] + let node = ShopCellNode(category: category) + return node + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let products = self.categories[indexPath.row].products + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + let tableViewAction = UIAlertAction(title: "ASTableNode", style: .default, handler: { (action) in + let viewController = ProductsTableViewController(products: products) + self.navigationController?.pushViewController(viewController, animated: true) + }) + let collectionViewAction = UIAlertAction(title: "ASCollectionNode", style: .default, handler: { (action) in + let viewController = ProductsCollectionViewController(products: products) + self.navigationController?.pushViewController(viewController, animated: true) + }) + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(tableViewAction) + alertController.addAction(collectionViewAction) + alertController.addAction(cancelAction) + DispatchQueue.main.async { + self.present(alertController, animated: true, completion: nil) + } + } + + func tableView(_ tableView: ASTableView, constrainedSizeForRowAt indexPath: IndexPath) -> ASSizeRange { + let width = UIScreen.main.bounds.width + return ASSizeRangeMake(CGSize(width: width, height: 175)) + } + +} + +extension ShopViewController { + + func setupTitle() { + self.title = "Bear Shop" + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Shop-Bridging-Header.h b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Shop-Bridging-Header.h new file mode 100644 index 0000000000..943168eead --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/Shop/Shop-Bridging-Header.h @@ -0,0 +1,9 @@ +// +// Shop-Bridging-Header.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/ShopTests/Info.plist b/submodules/AsyncDisplayKit/examples_extra/Shop/ShopTests/Info.plist new file mode 100644 index 0000000000..6c6c23c43a --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/ShopTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/submodules/AsyncDisplayKit/examples_extra/Shop/ShopTests/ShopTests.swift b/submodules/AsyncDisplayKit/examples_extra/Shop/ShopTests/ShopTests.swift new file mode 100644 index 0000000000..b42098bc4c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/Shop/ShopTests/ShopTests.swift @@ -0,0 +1,36 @@ +// +// ShopTests.swift +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import XCTest +@testable import Shop + +class ShopTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Podfile b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..f3964a9562 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,378 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* AsyncTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* AsyncTableViewController.m */; }; + 18748FDB1BB727B20053A9C1 /* AsyncViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 18748FDA1BB727B20053A9C1 /* AsyncViewController.m */; }; + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */; }; + 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 */; }; + C3B2A32888B988D317F5DDE1 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D3384C58256708C51C64523 /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* AsyncTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncTableViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* AsyncTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncTableViewController.m; sourceTree = ""; }; + 18748FD91BB727B20053A9C1 /* AsyncViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AsyncViewController.h; sourceTree = ""; }; + 18748FDA1BB727B20053A9C1 /* AsyncViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AsyncViewController.m; sourceTree = ""; }; + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RandomCoreGraphicsNode.h; sourceTree = ""; }; + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RandomCoreGraphicsNode.m; sourceTree = ""; }; + 3673DB8C60BCB89039CAD924 /* 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 = ""; }; + 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; }; + 7D3384C58256708C51C64523 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 86D5AE7D8306374F99D2E0F7 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C3B2A32888B988D317F5DDE1 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* AsyncTableViewController.h */, + 05E2128C19D4DB510098F589 /* AsyncTableViewController.m */, + 18748FD91BB727B20053A9C1 /* AsyncViewController.h */, + 18748FDA1BB727B20053A9C1 /* AsyncViewController.m */, + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */, + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7D3384C58256708C51C64523 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 86D5AE7D8306374F99D2E0F7 /* Pods-Sample.debug.xcconfig */, + 3673DB8C60BCB89039CAD924 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 0342F7A1563F38A62746D4B8 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0342F7A1563F38A62746D4B8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */, + 18748FDB1BB727B20053A9C1 /* AsyncViewController.m in Sources */, + 05E2128D19D4DB510098F589 /* AsyncTableViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 86D5AE7D8306374F99D2E0F7 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3673DB8C60BCB89039CAD924 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AppDelegate.m new file mode 100644 index 0000000000..662357cb71 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AppDelegate.m @@ -0,0 +1,31 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "AsyncTableViewController.h" +#import "AsyncViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + + UITabBarController *tabBarController = [[UITabBarController alloc] initWithNibName:nil bundle:nil]; + self.window.rootViewController = tabBarController; + + [tabBarController setViewControllers:@[[[AsyncTableViewController alloc] init], [[AsyncViewController alloc] init]]]; + + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.h new file mode 100644 index 0000000000..2b8fe4ed9f --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.h @@ -0,0 +1,14 @@ +// +// AsyncTableViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AsyncTableViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m new file mode 100644 index 0000000000..2cacef05ea --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m @@ -0,0 +1,80 @@ +// +// AsyncTableViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import "AsyncTableViewController.h" +#import "RandomCoreGraphicsNode.h" + +@interface AsyncTableViewController () +{ + ASTableView *_tableView; +} + +@end + +@implementation AsyncTableViewController + +#pragma mark - UIViewController. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + self.tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:UITabBarSystemItemFeatured tag:0]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRedo + target:self + action:@selector(reloadEverything)]; + + return self; +} + +- (void)reloadEverything +{ + [_tableView reloadData]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _tableView = [[ASTableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; + _tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + _tableView.asyncDataSource = self; + _tableView.asyncDelegate = self; + + ASRangeTuningParameters tuningParameters; + tuningParameters.leadingBufferScreenfuls = 0.5; + tuningParameters.trailingBufferScreenfuls = 1.0; + [_tableView setTuningParameters:tuningParameters forRangeType:ASLayoutRangeTypePreload]; + [_tableView setTuningParameters:tuningParameters forRangeType:ASLayoutRangeTypeDisplay]; + + [self.view addSubview:_tableView]; +} + +#pragma mark - ASTableView. + +- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + RandomCoreGraphicsNode *elementNode = [[RandomCoreGraphicsNode alloc] init]; + elementNode.style.preferredSize = CGSizeMake(320, 100); + return elementNode; + }; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return 100; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h new file mode 100644 index 0000000000..2c20ade6e9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h @@ -0,0 +1,14 @@ +// +// AsyncViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AsyncViewController : ASViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.m new file mode 100644 index 0000000000..de9b71cc89 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.m @@ -0,0 +1,38 @@ +// +// AsyncViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AsyncViewController.h" +#import "RandomCoreGraphicsNode.h" + +@implementation AsyncViewController + +- (instancetype)init +{ + if (!(self = [super initWithNode:[[RandomCoreGraphicsNode alloc] init]])) { + return nil; + } + + self.neverShowPlaceholders = YES; + self.tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:UITabBarSystemItemFavorites tag:0]; + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + // FIXME: This is only being called on the first time the UITabBarController shows us. + [super viewWillAppear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [self.node recursivelyClearContents]; + [super viewDidDisappear:animated]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.h new file mode 100644 index 0000000000..cdd60a3465 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.h @@ -0,0 +1,17 @@ +// +// RandomCoreGraphicsNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface RandomCoreGraphicsNode : ASCellNode +{ + ASTextNode *_textNode; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m new file mode 100644 index 0000000000..d80c51caf2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m @@ -0,0 +1,96 @@ +// +// RandomCoreGraphicsNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "RandomCoreGraphicsNode.h" +#import + +@implementation RandomCoreGraphicsNode + ++ (UIColor *)randomColor +{ + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + CGFloat locations[3]; + NSMutableArray *colors = [NSMutableArray arrayWithCapacity:3]; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[0] = 0.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[1] = 1.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[2] = ( arc4random() % 256 / 256.0 ); + + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)colors, locations); + + CGGradientDrawingOptions drawingOptions; + CGContextDrawLinearGradient(ctx, gradient, CGPointZero, CGPointMake(bounds.size.width, bounds.size.height), drawingOptions); + + CGColorSpaceRelease(colorSpace); +} + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + return [self description]; +} + +- (NSDictionary *)textStyle +{ + UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:36.0f]; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + style.paragraphSpacing = 0.5 * font.lineHeight; + style.hyphenationFactor = 1.0; + + return @{ NSFontAttributeName: font, + NSParagraphStyleAttributeName: style }; +} + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _textNode = [[ASTextNode alloc] init]; + _textNode.placeholderEnabled = NO; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Hello, ASDK!" + attributes:[self textStyle]]; + [self addSubnode:_textNode]; + + return self; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)]; + return CGSizeMake(constrainedSize.width, 100); +} + +- (void)layout +{ + [super layout]; + + CGSize boundsSize = self.bounds.size; + CGSize textSize = _textNode.calculatedSize; + CGRect textRect = CGRectMake(roundf((boundsSize.width - textSize.width) / 2.0), + roundf((boundsSize.height - textSize.height) / 2.0), + textSize.width, + textSize.height); + _textNode.frame = textRect; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousConcurrency/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Podfile b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..05a646e3c3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,375 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 05561CF919D4E77700CBA93C /* BlurbNode.m */; }; + 05561CFD19D4F94A00CBA93C /* KittenNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* KittenNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 4186058E3E168D53D99777F3 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 77BD0D94BEDD0C95E94180C7 /* 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CF819D4E77700CBA93C /* BlurbNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurbNode.h; sourceTree = ""; }; + 05561CF919D4E77700CBA93C /* BlurbNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurbNode.m; sourceTree = ""; }; + 05561CFB19D4F94A00CBA93C /* KittenNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KittenNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* KittenNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KittenNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 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; }; + 77BD0D94BEDD0C95E94180C7 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + BE330B4179344E0F8E899043 /* 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 = ""; }; + F51793F3B0AD498E5C28A426 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4186058E3E168D53D99777F3 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* KittenNode.h */, + 05561CFC19D4F94A00CBA93C /* KittenNode.mm */, + 05561CF819D4E77700CBA93C /* BlurbNode.h */, + 05561CF919D4E77700CBA93C /* BlurbNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 77BD0D94BEDD0C95E94180C7 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + BE330B4179344E0F8E899043 /* Pods-Sample.debug.xcconfig */, + F51793F3B0AD498E5C28A426 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + 626F666C417D1641EB1FF73D /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 626F666C417D1641EB1FF73D /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05561CFD19D4F94A00CBA93C /* KittenNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BE330B4179344E0F8E899043 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51793F3B0AD498E5C28A426 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/AppDelegate.m new file mode 100644 index 0000000000..f8437855b0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/BlurbNode.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/BlurbNode.h new file mode 100644 index 0000000000..5451fb39f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/BlurbNode.h @@ -0,0 +1,17 @@ +// +// BlurbNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Simple node that displays a placekitten.com attribution. + */ +@interface BlurbNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/BlurbNode.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/BlurbNode.m new file mode 100644 index 0000000000..6014711d33 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/BlurbNode.m @@ -0,0 +1,120 @@ +// +// BlurbNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "BlurbNode.h" +#import "AppDelegate.h" + +#import +#import + +#import +#import + +static CGFloat kTextPadding = 10.0f; +static NSString *kLinkAttributeName = @"PlaceKittenNodeLinkAttributeName"; + +@interface BlurbNode () +{ + ASTextNode *_textNode; +} + +@end + + +@implementation BlurbNode + +#pragma mark - +#pragma mark ASCellNode. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // create a text node + _textNode = [[ASTextNode alloc] init]; + + // configure the node to support tappable links + _textNode.delegate = self; + _textNode.userInteractionEnabled = YES; + _textNode.linkAttributeNames = @[ kLinkAttributeName ]; + + // generate an attributed string using the custom link attribute specified above + NSString *blurb = @"kittens courtesy placekitten.com \U0001F638"; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)]; + [string addAttributes:@{ + kLinkAttributeName: [NSURL URLWithString:@"http://placekitten.com/"], + NSForegroundColorAttributeName: [UIColor grayColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } + range:[blurb rangeOfString:@"placekitten.com"]]; + _textNode.attributedText = string; + + // add it as a subnode, and we're done + [self addSubnode:_textNode]; + + return self; +} + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // called on a background thread. custom nodes must call -measure: on their subnodes in -calculateSizeThatFits: + CGSize measuredSize = [_textNode measure:CGSizeMake(constrainedSize.width - 2 * kTextPadding, + constrainedSize.height - 2 * kTextPadding)]; + return CGSizeMake(constrainedSize.width, measuredSize.height + 2 * kTextPadding); +} + +- (void)layout +{ + // called on the main thread. we'll use the stashed size from above, instead of blocking on text sizing + CGSize textNodeSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(roundf((self.calculatedSize.width - textNodeSize.width) / 2.0f), + kTextPadding, + textNodeSize.width, + textNodeSize.height); +} +#endif + +#pragma mark - +#pragma mark ASTextNodeDelegate methods. + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // the node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/KittenNode.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/KittenNode.h new file mode 100644 index 0000000000..5b0b04203e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/KittenNode.h @@ -0,0 +1,22 @@ +// +// KittenNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Social media-style node that displays a kitten picture and a random length + * of lorem ipsum text. Uses a placekitten.com kitten of the specified size. + */ +@interface KittenNode : ASCellNode + +- (instancetype)initWithKittenOfSize:(CGSize)size; + +- (void)toggleImageEnlargement; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/KittenNode.mm b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/KittenNode.mm new file mode 100644 index 0000000000..5fcebf393e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/KittenNode.mm @@ -0,0 +1,197 @@ +// +// KittenNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "KittenNode.h" +#import "AppDelegate.h" + +#import + +#import +#import + +static const CGFloat kImageSize = 80.0f; +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + + +@interface KittenNode () +{ + CGSize _kittenSize; + + ASNetworkImageNode *_imageNode; + ASTextNode *_textNode; + ASDisplayNode *_divider; + BOOL _isImageEnlarged; + BOOL _swappedTextAndImage; +} + +@end + + +@implementation KittenNode + +// lorem ipsum text courtesy https://kittyipsum.com/ <3 ++ (NSArray *)placeholders +{ + static NSArray *placeholders = nil; + + static dispatch_once_t once; + dispatch_once(&once, ^{ + placeholders = @[ + @"Kitty ipsum dolor sit amet, purr sleep on your face lay down in your way biting, sniff tincidunt a etiam fluffy fur judging you stuck in a tree kittens.", + @"Lick tincidunt a biting eat the grass, egestas enim ut lick leap puking climb the curtains lick.", + @"Lick quis nunc toss the mousie vel, tortor pellentesque sunbathe orci turpis non tail flick suscipit sleep in the sink.", + @"Orci turpis litter box et stuck in a tree, egestas ac tempus et aliquam elit.", + @"Hairball iaculis dolor dolor neque, nibh adipiscing vehicula egestas dolor aliquam.", + @"Sunbathe fluffy fur tortor faucibus pharetra jump, enim jump on the table I don't like that food catnip toss the mousie scratched.", + @"Quis nunc nam sleep in the sink quis nunc purr faucibus, chase the red dot consectetur bat sagittis.", + @"Lick tail flick jump on the table stretching purr amet, rhoncus scratched jump on the table run.", + @"Suspendisse aliquam vulputate feed me sleep on your keyboard, rip the couch faucibus sleep on your keyboard tristique give me fish dolor.", + @"Rip the couch hiss attack your ankles biting pellentesque puking, enim suspendisse enim mauris a.", + @"Sollicitudin iaculis vestibulum toss the mousie biting attack your ankles, puking nunc jump adipiscing in viverra.", + @"Nam zzz amet neque, bat tincidunt a iaculis sniff hiss bibendum leap nibh.", + @"Chase the red dot enim puking chuf, tristique et egestas sniff sollicitudin pharetra enim ut mauris a.", + @"Sagittis scratched et lick, hairball leap attack adipiscing catnip tail flick iaculis lick.", + @"Neque neque sleep in the sink neque sleep on your face, climb the curtains chuf tail flick sniff tortor non.", + @"Ac etiam kittens claw toss the mousie jump, pellentesque rhoncus litter box give me fish adipiscing mauris a.", + @"Pharetra egestas sunbathe faucibus ac fluffy fur, hiss feed me give me fish accumsan.", + @"Tortor leap tristique accumsan rutrum sleep in the sink, amet sollicitudin adipiscing dolor chase the red dot.", + @"Knock over the lamp pharetra vehicula sleep on your face rhoncus, jump elit cras nec quis quis nunc nam.", + @"Sollicitudin feed me et ac in viverra catnip, nunc eat I don't like that food iaculis give me fish.", + ]; + }); + + return placeholders; +} + +- (instancetype)initWithKittenOfSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _kittenSize = size; + + // kitten image, with a solid background colour serving as placeholder + _imageNode = [[ASNetworkImageNode alloc] init]; + _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _imageNode.URL = [NSURL URLWithString:[NSString stringWithFormat:@"https://placekitten.com/%zd/%zd", + (NSInteger)roundl(_kittenSize.width), + (NSInteger)roundl(_kittenSize.height)]]; +// _imageNode.contentMode = UIViewContentModeCenter; + [_imageNode addTarget:self action:@selector(toggleNodesSwap) forControlEvents:ASControlNodeEventTouchUpInside]; + [self addSubnode:_imageNode]; + + // lorem ipsum text, plus some nice styling + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:[self kittyIpsum] + attributes:[self textStyle]]; + [self addSubnode:_textNode]; + + // hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + _divider.backgroundColor = [UIColor lightGrayColor]; + [self addSubnode:_divider]; + + return self; +} + +- (NSString *)kittyIpsum +{ + NSArray *placeholders = [KittenNode placeholders]; + u_int32_t ipsumCount = (u_int32_t)[placeholders count]; + u_int32_t location = arc4random_uniform(ipsumCount); + u_int32_t length = arc4random_uniform(ipsumCount - location); + + NSMutableString *string = [placeholders[location] mutableCopy]; + for (u_int32_t i = location + 1; i < location + length; i++) { + [string appendString:(i % 2 == 0) ? @"\n" : @" "]; + [string appendString:placeholders[i]]; + } + + return string; +} + +- (NSDictionary *)textStyle +{ + UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:12.0f]; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + style.paragraphSpacing = 0.5 * font.lineHeight; + style.hyphenationFactor = 1.0; + + return @{ NSFontAttributeName: font, + NSParagraphStyleAttributeName: style }; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGSize imageSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize) + : CGSizeMake(kImageSize, kImageSize); + _imageNode.size = ASRelativeSizeRangeMakeWithExactCGSize(imageSize); + _textNode.flexShrink = 1.0; + + ASStackLayoutSpec *stackSpec = [[ASStackLayoutSpec alloc] init]; + stackSpec.direction = ASStackLayoutDirectionHorizontal; + stackSpec.spacing = kInnerPadding; + [stackSpec setChildren:!_swappedTextAndImage ? @[_imageNode, _textNode] : @[_textNode, _imageNode]]; + + ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding); + insetSpec.child = stackSpec; + + return insetSpec; +} + +// With box model, you don't need to override this method, unless you want to add custom logic. +- (void)layout +{ + [super layout]; + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize imageSize = CGSizeMake(kImageSize, kImageSize); + CGSize textSize = [_textNode measure:CGSizeMake(constrainedSize.width - kImageSize - 2 * kOuterPadding - kInnerPadding, + constrainedSize.height)]; + + // ensure there's room for the text + CGFloat requiredHeight = MAX(textSize.height, imageSize.height); + return CGSizeMake(constrainedSize.width, requiredHeight + 2 * kOuterPadding); +} + +- (void)layout +{ + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); + + _imageNode.frame = CGRectMake(kOuterPadding, kOuterPadding, kImageSize, kImageSize); + + CGSize textSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(kOuterPadding + kImageSize + kInnerPadding, kOuterPadding, textSize.width, textSize.height); +} +#endif + +- (void)toggleImageEnlargement +{ + _isImageEnlarged = !_isImageEnlarged; + [self setNeedsLayout]; +} + +- (void)toggleNodesSwap +{ + _swappedTextAndImage = !_swappedTextAndImage; + [self setNeedsLayout]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/ViewController.m new file mode 100644 index 0000000000..1757807a1d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/ViewController.m @@ -0,0 +1,209 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import + +#import "BlurbNode.h" +#import "KittenNode.h" + + +static const NSInteger kLitterSize = 20; // intial number of kitten cells in ASTableView +static const NSInteger kLitterBatchSize = 10; // number of kitten cells to add to ASTableView +static const NSInteger kMaxLitterSize = 100; // max number of kitten cells allowed in ASTableView + +@interface ViewController () +{ + ASTableView *_tableView; + + // array of boxed CGSizes corresponding to placekitten.com kittens + NSMutableArray *_kittenDataSource; + + BOOL _dataSourceLocked; + NSIndexPath *_blurbNodeIndexPath; +} + +@property (nonatomic, strong) NSMutableArray *kittenDataSource; +@property (atomic, assign) BOOL dataSourceLocked; + +@end + + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator + _tableView.asyncDataSource = self; + _tableView.asyncDelegate = self; + + // populate our "data source" with some random kittens + _kittenDataSource = [self createLitterWithSize:kLitterSize]; + + _blurbNodeIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + self.title = @"Kittens"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit + target:self + action:@selector(toggleEditingMode)]; + + return self; +} + +- (NSMutableArray *)createLitterWithSize:(NSInteger)litterSize +{ + NSMutableArray *kittens = [NSMutableArray arrayWithCapacity:litterSize]; + for (NSInteger i = 0; i < litterSize; i++) { + + // placekitten.com will return the same kitten picture if the same pixel height & width are requested, + // so generate kittens with different width & height values. + u_int32_t deltaX = arc4random_uniform(10) - 5; + u_int32_t deltaY = arc4random_uniform(10) - 5; + CGSize size = CGSizeMake(350 + 2 * deltaX, 350 + 4 * deltaY); + + [kittens addObject:[NSValue valueWithCGSize:size]]; + } + return kittens; +} + +- (void)setKittenDataSource:(NSMutableArray *)kittenDataSource { + ASDisplayNodeAssert(!self.dataSourceLocked, @"Could not update data source when it is locked !"); + + _kittenDataSource = kittenDataSource; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_tableView]; + + [_tableView reloadDataImmediately]; +} + +- (void)viewWillLayoutSubviews +{ + _tableView.frame = self.view.bounds; +} + +- (void)toggleEditingMode +{ + [_tableView setEditing:!_tableView.editing animated:YES]; +} + + +#pragma mark - +#pragma mark ASTableView. + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [_tableView deselectRowAtIndexPath:indexPath animated:YES]; + [_tableView beginUpdates]; + // Assume only kitten nodes are selectable (see -tableView:shouldHighlightRowAtIndexPath:). + KittenNode *node = (KittenNode *)[_tableView nodeForRowAtIndexPath:indexPath]; + [node toggleImageEnlargement]; + [_tableView endUpdates]; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // special-case the first row + if ([_blurbNodeIndexPath compare:indexPath] == NSOrderedSame) { + BlurbNode *node = [[BlurbNode alloc] init]; + return node; + } + + NSValue *size = _kittenDataSource[indexPath.row - 1]; + KittenNode *node = [[KittenNode alloc] initWithKittenOfSize:size.CGSizeValue]; + return node; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + // blurb node + kLitterSize kitties + return 1 + _kittenDataSource.count; +} + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable selection for kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableViewLockDataSource:(ASTableView *)tableView +{ + self.dataSourceLocked = YES; +} + +- (void)tableViewUnlockDataSource:(ASTableView *)tableView +{ + self.dataSourceLocked = NO; +} + +- (BOOL)shouldBatchFetchForTableView:(UITableView *)tableView +{ + return _kittenDataSource.count < kMaxLitterSize; +} + +- (void)tableView:(UITableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + NSLog(@"adding kitties"); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + sleep(1); + dispatch_async(dispatch_get_main_queue(), ^{ + + // populate a new array of random-sized kittens + NSArray *moarKittens = [self createLitterWithSize:kLitterBatchSize]; + + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + + // find number of kittens in the data source and create their indexPaths + NSInteger existingRows = _kittenDataSource.count + 1; + + for (NSInteger i = 0; i < moarKittens.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:existingRows + i inSection:0]]; + } + + // add new kittens to the data source & notify table of new indexpaths + [_kittenDataSource addObjectsFromArray:moarKittens]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + + [context completeBatchFetching:YES]; + + NSLog(@"kittens added"); + }); + }); +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable editing for Kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Assume only kitten nodes are editable (see -tableView:canEditRowAtIndexPath:). + [_kittenDataSource removeObjectAtIndex:indexPath.row - 1]; + [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/SynchronousKittens/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Podfile b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Podfile new file mode 100644 index 0000000000..73e26195cd --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture/Yoga', :path => '../..' +end + diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..806d9a2e8e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,385 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 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 */; }; + 92F1263CECFE3FFCC7A5F936 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8EC8300ABAAEA079224272A /* libPods-Sample.a */; }; + C081EE8D1F85AEEC00F0B5F1 /* TabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */; }; + C081EE921F85AFB800F0B5F1 /* CollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */; }; + C081EE931F85AFB800F0B5F1 /* TextCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE911F85AFB800F0B5F1 /* TextCellNode.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 0CDEE995962D3E4584D302EE /* 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 = ""; }; + 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; }; + A950870A2154F92D5DC91F1A /* 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 = ""; }; + C081EE8B1F85AEEC00F0B5F1 /* TabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TabBarController.h; sourceTree = ""; }; + C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TabBarController.m; sourceTree = ""; }; + C081EE8E1F85AFB800F0B5F1 /* CollectionViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CollectionViewController.h; sourceTree = ""; }; + C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CollectionViewController.m; sourceTree = ""; }; + C081EE901F85AFB800F0B5F1 /* TextCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextCellNode.h; sourceTree = ""; }; + C081EE911F85AFB800F0B5F1 /* TextCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextCellNode.m; sourceTree = ""; }; + E8EC8300ABAAEA079224272A /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 92F1263CECFE3FFCC7A5F936 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 6DE0E5D094594AB09140EF84 /* Pods */, + F5DF5EAD6C1B97F91D1C830F /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + C081EE8E1F85AFB800F0B5F1 /* CollectionViewController.h */, + C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */, + C081EE901F85AFB800F0B5F1 /* TextCellNode.h */, + C081EE911F85AFB800F0B5F1 /* TextCellNode.m */, + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + C081EE8B1F85AEEC00F0B5F1 /* TabBarController.h */, + C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 6DE0E5D094594AB09140EF84 /* Pods */ = { + isa = PBXGroup; + children = ( + 0CDEE995962D3E4584D302EE /* Pods-Sample.debug.xcconfig */, + A950870A2154F92D5DC91F1A /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F5DF5EAD6C1B97F91D1C830F /* Frameworks */ = { + isa = PBXGroup; + children = ( + E8EC8300ABAAEA079224272A /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 77F6A2B5E8DA12933E6365CE /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + 96436DA0C1AFF84D8041B522 /* [CP] Embed Pods Frameworks */, + D17B5BD4AA634EFE93D71E9F /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 77F6A2B5E8DA12933E6365CE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 96436DA0C1AFF84D8041B522 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D17B5BD4AA634EFE93D71E9F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C081EE931F85AFB800F0B5F1 /* TextCellNode.m in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + C081EE8D1F85AEEC00F0B5F1 /* TabBarController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + C081EE921F85AFB800F0B5F1 /* CollectionViewController.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0CDEE995962D3E4584D302EE /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A950870A2154F92D5DC91F1A /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..d41d58c5d8 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/AppDelegate.h new file mode 100644 index 0000000000..e4a477eb23 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/AppDelegate.h @@ -0,0 +1,15 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/AppDelegate.m new file mode 100644 index 0000000000..c54a53505e --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/AppDelegate.m @@ -0,0 +1,37 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "TabBarController.h" +#import "CollectionViewController.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]; + + ViewController *viewController = [[ViewController alloc] init]; + viewController.tabBarItem.title = @"TextStress"; + + CollectionViewController *cvc = [[CollectionViewController alloc] init]; + cvc.tabBarItem.title = @"Flexbox"; + + TabBarController *tabBarController = [[TabBarController alloc] init]; + tabBarController.viewControllers = @[cvc, viewController]; + + self.window.rootViewController = tabBarController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/CollectionViewController.h b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/CollectionViewController.h new file mode 100644 index 0000000000..d9329bdb07 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/CollectionViewController.h @@ -0,0 +1,13 @@ +// +// CollectionViewController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface CollectionViewController : ASViewController +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/CollectionViewController.m b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/CollectionViewController.m new file mode 100644 index 0000000000..0464c376fa --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/CollectionViewController.m @@ -0,0 +1,63 @@ +// +// CollectionViewController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "CollectionViewController.h" +#import "TextCellNode.h" + +@interface CollectionViewController() +{ + ASCollectionNode *_collectionNode; + NSArray *_labels; + TextCellNode *_cellNode; +} + +@end + +@implementation CollectionViewController + +- (instancetype)init +{ + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; + CGRect rect = [[UIApplication sharedApplication] statusBarFrame]; + _collectionNode.contentInset = UIEdgeInsetsMake(rect.size.height, 0, 0, 0); + self = [super initWithNode:_collectionNode]; + if (self) { + _collectionNode.delegate = self; + _collectionNode.dataSource = self; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + _collectionNode.backgroundColor = [UIColor whiteColor]; + _labels = @[@"Fight of the Living Dead: Experiment Fight of the Living Dead: Experiment", @"S1 • E1"]; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return 1; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + _cellNode = [[TextCellNode alloc] initWithText1:_labels[0] text2:_labels[1]]; + return _cellNode; + }; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat width = collectionNode.view.bounds.size.width; + return ASSizeRangeMake(CGSizeMake(width, 0.0f), CGSizeMake(width, CGFLOAT_MAX)); +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TabBarController.h b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TabBarController.h new file mode 100644 index 0000000000..a8d7f80512 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TabBarController.h @@ -0,0 +1,12 @@ +// +// TabBarController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface TabBarController : ASTabBarController +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TabBarController.m b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TabBarController.m new file mode 100644 index 0000000000..d056f1f679 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TabBarController.m @@ -0,0 +1,15 @@ +// +// TabBarController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "TabBarController.h" + +@interface TabBarController () +@end + +@implementation TabBarController +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TextCellNode.h b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TextCellNode.h new file mode 100644 index 0000000000..2ae855e5fb --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TextCellNode.h @@ -0,0 +1,13 @@ +// +// TextCellNode.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface TextCellNode : ASCellNode +- (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2; +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TextCellNode.m b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TextCellNode.m new file mode 100644 index 0000000000..4b777b5677 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/TextCellNode.m @@ -0,0 +1,96 @@ +// +// TextCellNode.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "TextCellNode.h" +#import +#import + +#ifndef USE_ASTEXTNODE_2 +#define USE_ASTEXTNODE_2 1 +#endif + +@interface TextCellNode() +{ +#if USE_ASTEXTNODE_2 + ASTextNode2 *_label1; + ASTextNode2 *_label2; +#else + ASTextNode *_label1; + ASTextNode *_label2; +#endif +} +@end + +@implementation TextCellNode + +- (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2 +{ + self = [super init]; + if (self) { + self.automaticallyManagesSubnodes = YES; + self.clipsToBounds = YES; +#if USE_ASTEXTNODE_2 + _label1 = [[ASTextNode2 alloc] init]; + _label2 = [[ASTextNode2 alloc] init]; +#else + _label1 = [[ASTextNode alloc] init]; + _label2 = [[ASTextNode alloc] init]; +#endif + + _label1.attributedText = [[NSAttributedString alloc] initWithString:text1]; + _label2.attributedText = [[NSAttributedString alloc] initWithString:text2]; + + _label1.maximumNumberOfLines = 1; + _label1.truncationMode = NSLineBreakByTruncatingTail; + _label2.maximumNumberOfLines = 1; + _label2.truncationMode = NSLineBreakByTruncatingTail; + + [self simpleSetupYogaLayout]; + } + return self; +} + +/** + This is to text a row with two labels, the first should be truncated with "...". + Layout is like: [l1Container[_label1], label2]. + This shows a bug of ASTextNode2. + */ +- (void)simpleSetupYogaLayout +{ + [self.style yogaNodeCreateIfNeeded]; + [_label1.style yogaNodeCreateIfNeeded]; + [_label2.style yogaNodeCreateIfNeeded]; + + _label1.style.flexGrow = 0; + _label1.style.flexShrink = 1; + _label1.backgroundColor = [UIColor lightGrayColor]; + + _label2.style.flexGrow = 0; + _label2.style.flexShrink = 0; + _label2.backgroundColor = [UIColor greenColor]; + + ASDisplayNode *l1Container = [ASDisplayNode yogaVerticalStack]; + + // TODO(fix ASTextNode2): next two line will show the bug of TextNode2 + // which works for ASTextNode though + // see discussion here: https://github.com/TextureGroup/Texture/pull/553 + l1Container.style.alignItems = ASStackLayoutAlignItemsCenter; + _label1.style.alignSelf = ASStackLayoutAlignSelfStart; + + l1Container.style.flexGrow = 0; + l1Container.style.flexShrink = 1; + + l1Container.yogaChildren = @[_label1]; + + self.style.justifyContent = ASStackLayoutJustifyContentSpaceBetween; + self.style.alignItems = ASStackLayoutAlignItemsStart; + self.style.flexDirection = ASStackLayoutDirectionHorizontal; + self.yogaChildren = @[l1Container, _label2]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/ViewController.h new file mode 100644 index 0000000000..8c2d4162b9 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/ViewController.h @@ -0,0 +1,13 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/ViewController.m new file mode 100644 index 0000000000..f14bba18b3 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/ViewController.m @@ -0,0 +1,162 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" +#import + +#define NUMBER_ELEMENTS 2 + +@interface ViewController () +{ + NSMutableArray *_textNodes; + NSMutableArray *_textLabels; + UIScrollView *_scrollView; +} + +@end + + +@implementation ViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _textNodes = [NSMutableArray array]; + _textLabels = [NSMutableArray array]; + + _scrollView = [[UIScrollView alloc] init]; + [self.view addSubview:_scrollView]; + + for (int i = 0; i < NUMBER_ELEMENTS; i++) { + + ASTextNode *node = [self createNodeForIndex:i]; + [_textNodes addObject:node]; + [_scrollView addSubnode:node]; + + UILabel *label = [self createLabelForIndex:i]; + [_textLabels addObject:label]; + [_scrollView addSubview:label]; + } +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + CGFloat maxWidth = 0; + CGFloat maxHeight = 0; + + CGRect frame = CGRectMake(50, 50, 0, 0); + + for (int i = 0; i < NUMBER_ELEMENTS; i++) { + frame.size = [self sizeForIndex:i]; + [[_textNodes objectAtIndex:i] setFrame:frame]; + + frame.origin.x += frame.size.width + 50; + + [[_textLabels objectAtIndex:i] setFrame:frame]; + + if (frame.size.width > maxWidth) { + maxWidth = frame.size.width; + } + if ((frame.size.height + frame.origin.y) > maxHeight) { + maxHeight = frame.size.height + frame.origin.y; + } + + frame.origin.x -= frame.size.width + 50; + frame.origin.y += frame.size.height + 20; + } + + _scrollView.frame = self.view.bounds; + _scrollView.contentSize = CGSizeMake(maxWidth, maxHeight); +} + +- (ASTextNode *)createNodeForIndex:(NSUInteger)index +{ + ASTextNode *node = [[ASTextNode alloc] init]; + node.attributedText = [self textForIndex:index]; + node.backgroundColor = [UIColor orangeColor]; + + NSMutableAttributedString *string = [node.attributedText mutableCopy]; + + switch (index) { + case 0: // top justification (ASDK) vs. center justification (UILabel) + node.maximumNumberOfLines = 3; + return node; + + case 1: // default truncation attributed string color shouldn't match attributed text color (ASDK) vs. match (UIKit) + node.maximumNumberOfLines = 3; + [string addAttribute:NSForegroundColorAttributeName + value:[UIColor redColor] + range:NSMakeRange(0, [string length])]; + node.attributedText = string; + return node; + + default: + return nil; + } +} + +- (UILabel *)createLabelForIndex:(NSUInteger)index +{ + UILabel *label = [[UILabel alloc] init]; + label.attributedText = [self textForIndex:index]; + label.backgroundColor = [UIColor greenColor]; + + NSMutableAttributedString *string = [label.attributedText mutableCopy]; + + switch (index) { + case 0: + label.numberOfLines = 3; + return label; + + case 1: + label.numberOfLines = 3; + [string addAttribute:NSForegroundColorAttributeName + value:[UIColor redColor] + range:NSMakeRange(0, [string length])]; + label.attributedText = string; + return label; + + default: + return nil; + } +} + +- (NSAttributedString *)textForIndex:(NSUInteger)index +{ + NSDictionary *attrs = @{ NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue" size:12.0f] }; + + switch (index) { + case 0: + return [[NSAttributedString alloc] initWithString:@"1\n2\n3\n4\n5" attributes:attrs]; + + case 1: + return [[NSAttributedString alloc] initWithString:@"1\n2\n3\n4\n5" attributes:attrs]; + + default: + return nil; + } +} + +- (CGSize)sizeForIndex:(NSUInteger)index +{ + switch (index) { + case 0: + return CGSizeMake(40, 100); + + case 1: + return CGSizeMake(40, 100); + + default: + return CGSizeZero; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/main.m new file mode 100644 index 0000000000..1745cc192d --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/TextStressTest/Sample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-568h@2x.png b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-667h@2x.png b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-736h@3x.png b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Podfile b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Podfile new file mode 100644 index 0000000000..71a7f2c4b2 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c7a68ff0bf --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,375 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 05561CF919D4E77700CBA93C /* BlurbNode.m */; }; + 05561CFD19D4F94A00CBA93C /* NicCageNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* NicCageNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 67ADD7A0A11DF37B2D73094B /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 12CBA11A5870DDF5A5626B7B /* 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CF819D4E77700CBA93C /* BlurbNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurbNode.h; sourceTree = ""; }; + 05561CF919D4E77700CBA93C /* BlurbNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurbNode.m; sourceTree = ""; }; + 05561CFB19D4F94A00CBA93C /* NicCageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NicCageNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* NicCageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NicCageNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 12CBA11A5870DDF5A5626B7B /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 246853115611E4007B767EA5 /* 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 = ""; }; + 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; }; + C822FB25F4C6DBD8EA3CE6EC /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 67ADD7A0A11DF37B2D73094B /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* NicCageNode.h */, + 05561CFC19D4F94A00CBA93C /* NicCageNode.mm */, + 05561CF819D4E77700CBA93C /* BlurbNode.h */, + 05561CF919D4E77700CBA93C /* BlurbNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12CBA11A5870DDF5A5626B7B /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 246853115611E4007B767EA5 /* Pods-Sample.debug.xcconfig */, + C822FB25F4C6DBD8EA3CE6EC /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */, + EBE12F047824F0A2C6353B54 /* 📦 Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + EBE12F047824F0A2C6353B54 /* 📦 Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05561CFD19D4F94A00CBA93C /* NicCageNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 246853115611E4007B767EA5 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C822FB25F4C6DBD8EA3CE6EC /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/AppDelegate.h b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/AppDelegate.h new file mode 100644 index 0000000000..c30a27f4dc --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/AppDelegate.m b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/AppDelegate.m new file mode 100644 index 0000000000..f8437855b0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/AppDelegate.m @@ -0,0 +1,25 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/BlurbNode.h b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/BlurbNode.h new file mode 100644 index 0000000000..5451fb39f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/BlurbNode.h @@ -0,0 +1,17 @@ +// +// BlurbNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Simple node that displays a placekitten.com attribution. + */ +@interface BlurbNode : ASCellNode + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/BlurbNode.m b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/BlurbNode.m new file mode 100644 index 0000000000..ba39439157 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/BlurbNode.m @@ -0,0 +1,113 @@ +// +// BlurbNode.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "BlurbNode.h" +#import "AppDelegate.h" + +#import +#import + +#import +#import + +static CGFloat kTextPadding = 10.0f; +static NSString *kLinkAttributeName = @"PlaceKittenNodeLinkAttributeName"; + +@interface BlurbNode () +{ + ASTextNode *_textNode; +} + +@end + + +@implementation BlurbNode + +#pragma mark - +#pragma mark ASCellNode. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // create a text node + _textNode = [[ASTextNode alloc] init]; + + // configure the node to support tappable links + _textNode.delegate = self; + _textNode.userInteractionEnabled = YES; + _textNode.linkAttributeNames = @[ kLinkAttributeName ]; + + // generate an attributed string using the custom link attribute specified above + NSString *blurb = @"Nic Cage courtesy of himself."; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + _textNode.attributedText = string; + + // add it as a subnode, and we're done + [self addSubnode:_textNode]; + + return self; +} + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // called on a background thread. custom nodes must call -measure: on their subnodes in -calculateSizeThatFits: + CGSize measuredSize = [_textNode measure:CGSizeMake(constrainedSize.width - 2 * kTextPadding, + constrainedSize.height - 2 * kTextPadding)]; + return CGSizeMake(constrainedSize.width, measuredSize.height + 2 * kTextPadding); +} + +- (void)layout +{ + // called on the main thread. we'll use the stashed size from above, instead of blocking on text sizing + CGSize textNodeSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(roundf((self.calculatedSize.width - textNodeSize.width) / 2.0f), + kTextPadding, + textNodeSize.width, + textNodeSize.height); +} +#endif + +#pragma mark - +#pragma mark ASTextNodeDelegate methods. + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // the node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/Info.plist b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/NicCageNode.h b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/NicCageNode.h new file mode 100644 index 0000000000..b2937f6a44 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/NicCageNode.h @@ -0,0 +1,22 @@ +// +// NicCageNode.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * Social media-style node that displays a kitten picture and a random length + * of lorem ipsum text. Uses a placekitten.com kitten of the specified size. + */ +@interface NicCageNode : ASCellNode + +- (instancetype)initWithKittenOfSize:(CGSize)size; + +- (void)toggleImageEnlargement; + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/NicCageNode.mm b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/NicCageNode.mm new file mode 100644 index 0000000000..5e960ee013 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/NicCageNode.mm @@ -0,0 +1,260 @@ +// +// NicCageNode.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "NicCageNode.h" +#import "AppDelegate.h" + +#import + +#import +#import +#import +#import + +static const CGFloat kImageSize = 80.0f; +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + +#define kVideoURL @"https://www.w3schools.com/html/mov_bbb.mp4" +#define kVideoStreamURL @"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" + +@interface NicCageNode () +{ + CGSize _kittenSize; + +// ASNetworkImageNode *_imageNode; + ASVideoNode *_videoNode; + ASTextNode *_textNode; + ASDisplayNode *_divider; + BOOL _isImageEnlarged; + BOOL _swappedTextAndImage; +} + +@end + + +@implementation NicCageNode + +// lorem ipsum text courtesy https://kittyipsum.com/ <3 ++ (NSArray *)placeholders +{ + static NSArray *placeholders = nil; + + static dispatch_once_t once; + dispatch_once(&once, ^{ + placeholders = @[ + @"Kitty ipsum dolor sit amet, purr sleep on your face lay down in your way biting, sniff tincidunt a etiam fluffy fur judging you stuck in a tree kittens.", + @"Lick tincidunt a biting eat the grass, egestas enim ut lick leap puking climb the curtains lick.", + @"Lick quis nunc toss the mousie vel, tortor pellentesque sunbathe orci turpis non tail flick suscipit sleep in the sink.", + @"Orci turpis litter box et stuck in a tree, egestas ac tempus et aliquam elit.", + @"Hairball iaculis dolor dolor neque, nibh adipiscing vehicula egestas dolor aliquam.", + @"Sunbathe fluffy fur tortor faucibus pharetra jump, enim jump on the table I don't like that food catnip toss the mousie scratched.", + @"Quis nunc nam sleep in the sink quis nunc purr faucibus, chase the red dot consectetur bat sagittis.", + @"Lick tail flick jump on the table stretching purr amet, rhoncus scratched jump on the table run.", + @"Suspendisse aliquam vulputate feed me sleep on your keyboard, rip the couch faucibus sleep on your keyboard tristique give me fish dolor.", + @"Rip the couch hiss attack your ankles biting pellentesque puking, enim suspendisse enim mauris a.", + @"Sollicitudin iaculis vestibulum toss the mousie biting attack your ankles, puking nunc jump adipiscing in viverra.", + @"Nam zzz amet neque, bat tincidunt a iaculis sniff hiss bibendum leap nibh.", + @"Chase the red dot enim puking chuf, tristique et egestas sniff sollicitudin pharetra enim ut mauris a.", + @"Sagittis scratched et lick, hairball leap attack adipiscing catnip tail flick iaculis lick.", + @"Neque neque sleep in the sink neque sleep on your face, climb the curtains chuf tail flick sniff tortor non.", + @"Ac etiam kittens claw toss the mousie jump, pellentesque rhoncus litter box give me fish adipiscing mauris a.", + @"Pharetra egestas sunbathe faucibus ac fluffy fur, hiss feed me give me fish accumsan.", + @"Tortor leap tristique accumsan rutrum sleep in the sink, amet sollicitudin adipiscing dolor chase the red dot.", + @"Knock over the lamp pharetra vehicula sleep on your face rhoncus, jump elit cras nec quis quis nunc nam.", + @"Sollicitudin feed me et ac in viverra catnip, nunc eat I don't like that food iaculis give me fish.", + ]; + }); + + return placeholders; +} + +- (instancetype)initWithKittenOfSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _kittenSize = size; + + u_int32_t videoInitMethod = arc4random_uniform(3); + u_int32_t autoPlay = arc4random_uniform(2); + NSArray* methodArray = @[@"AVAsset", @"File URL", @"HLS URL"]; + NSArray* autoPlayArray = @[@"paused", @"auto play"]; + + switch (videoInitMethod) { + case 0: + // Construct an AVAsset from a URL + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:kVideoURL]]; + break; + + case 1: + // Construct the video node directly from the .mp4 URL + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:kVideoURL]]; + break; + + case 2: + // Construct the video node from an HTTP Live Streaming URL + // URL from https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/02_Playback.html + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:kVideoStreamURL]]; + break; + } + + if (autoPlay == 1) + _videoNode.shouldAutoplay = YES; + + _videoNode.shouldAutorepeat = YES; + _videoNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + + [self addSubnode:_videoNode]; + + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ %@ %@", methodArray[videoInitMethod], autoPlayArray[autoPlay], [self kittyIpsum]] + attributes:[self textStyle]]; + [self addSubnode:_textNode]; + + // hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + _divider.backgroundColor = [UIColor lightGrayColor]; + [self addSubnode:_divider]; + + return self; +} + +- (NSString *)kittyIpsum +{ + NSArray *placeholders = [NicCageNode placeholders]; + u_int32_t ipsumCount = (u_int32_t)[placeholders count]; + u_int32_t location = arc4random_uniform(ipsumCount); + u_int32_t length = arc4random_uniform(ipsumCount - location); + + NSMutableString *string = [placeholders[location] mutableCopy]; + for (u_int32_t i = location + 1; i < location + length; i++) { + [string appendString:(i % 2 == 0) ? @"\n" : @" "]; + [string appendString:placeholders[i]]; + } + + return string; +} + +- (NSDictionary *)textStyle +{ + UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:12.0f]; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + style.paragraphSpacing = 0.5 * font.lineHeight; + style.hyphenationFactor = 1.0; + + return @{ NSFontAttributeName: font, + NSParagraphStyleAttributeName: style }; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGSize videoNodeSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize) + : CGSizeMake(kImageSize, kImageSize); + + [_videoNode.style setPreferredSize:videoNodeSize]; + + _textNode.style.flexShrink = 1.0; + + ASStackLayoutSpec *stackSpec = [[ASStackLayoutSpec alloc] init]; + stackSpec.direction = ASStackLayoutDirectionHorizontal; + stackSpec.spacing = kInnerPadding; + [stackSpec setChildren:!_swappedTextAndImage ? @[_videoNode, _textNode] : @[_textNode, _videoNode]]; + + ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding); + insetSpec.child = stackSpec; + + return insetSpec; +} + +// With box model, you don't need to override this method, unless you want to add custom logic. +- (void)layout +{ + [super layout]; + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize imageSize = CGSizeMake(kImageSize, kImageSize); + CGSize textSize = [_textNode measure:CGSizeMake(constrainedSize.width - kImageSize - 2 * kOuterPadding - kInnerPadding, + constrainedSize.height)]; + + // ensure there's room for the text + CGFloat requiredHeight = MAX(textSize.height, imageSize.height); + return CGSizeMake(constrainedSize.width, requiredHeight + 2 * kOuterPadding); +} + +- (void)layout +{ + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); + + _imageNode.frame = CGRectMake(kOuterPadding, kOuterPadding, kImageSize, kImageSize); + + CGSize textSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(kOuterPadding + kImageSize + kInnerPadding, kOuterPadding, textSize.width, textSize.height); +} +#endif + +- (void)toggleImageEnlargement +{ + _isImageEnlarged = !_isImageEnlarged; + [self setNeedsLayout]; +} + +- (void)toggleNodesSwap +{ + _swappedTextAndImage = !_swappedTextAndImage; + + [UIView animateWithDuration:0.15 animations:^{ + self.alpha = 0; + } completion:^(BOOL finished) { + [self setNeedsLayout]; + [self.view layoutIfNeeded]; + + [UIView animateWithDuration:0.15 animations:^{ + self.alpha = 1; + }]; + }]; +} + +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [UIColor lightGrayColor]; + } else if (self.selected) { + self.backgroundColor = [UIColor blueColor]; + } else { + self.backgroundColor = [UIColor whiteColor]; + } +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self updateBackgroundColor]; +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/ViewController.h b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/ViewController.h new file mode 100644 index 0000000000..c8a0626291 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/ViewController.m b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/ViewController.m new file mode 100644 index 0000000000..5e1553c571 --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/ViewController.m @@ -0,0 +1,197 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import +#import + +#import "BlurbNode.h" +#import "NicCageNode.h" +#import + +static const NSInteger kCageSize = 20; // intial number of Cage cells in ASTableView +static const NSInteger kCageBatchSize = 10; // number of Cage cells to add to ASTableView +static const NSInteger kMaxCageSize = 100; // max number of Cage cells allowed in ASTableView + +@interface ViewController () +{ + ASTableView *_tableView; + + // array of boxed CGSizes corresponding to placekitten.com kittens + NSMutableArray *_kittenDataSource; + + BOOL _dataSourceLocked; + NSIndexPath *_blurbNodeIndexPath; +} + +@property (nonatomic, strong) NSMutableArray *kittenDataSource; +@property (atomic, assign) BOOL dataSourceLocked; + +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator + _tableView.asyncDataSource = self; + _tableView.asyncDelegate = self; + + // populate our "data source" with some random kittens + _kittenDataSource = [self createLitterWithSize:kCageSize]; + + _blurbNodeIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + self.title = @"Nic Cage"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit + target:self + action:@selector(toggleEditingMode)]; + + return self; +} + +- (NSMutableArray *)createLitterWithSize:(NSInteger)litterSize +{ + NSMutableArray *cages = [NSMutableArray arrayWithCapacity:litterSize]; + for (NSInteger i = 0; i < litterSize; i++) { + + u_int32_t deltaX = arc4random_uniform(10) - 5; + u_int32_t deltaY = arc4random_uniform(10) - 5; + CGSize size = CGSizeMake(350 + 2 * deltaX, 350 + 4 * deltaY); + + [cages addObject:[NSValue valueWithCGSize:size]]; + } + return cages; +} + +- (void)setKittenDataSource:(NSMutableArray *)kittenDataSource { + ASDisplayNodeAssert(!self.dataSourceLocked, @"Could not update data source when it is locked !"); + + _kittenDataSource = kittenDataSource; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_tableView]; +} + +- (void)viewWillLayoutSubviews +{ + _tableView.frame = self.view.bounds; +} + +- (void)toggleEditingMode +{ + [_tableView setEditing:!_tableView.editing animated:YES]; +} + +#pragma mark - +#pragma mark ASTableView. + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [_tableView deselectRowAtIndexPath:indexPath animated:YES]; + // Assume only kitten nodes are selectable (see -tableView:shouldHighlightRowAtIndexPath:). + NicCageNode *node = (NicCageNode *)[_tableView nodeForRowAtIndexPath:indexPath]; + [node toggleImageEnlargement]; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // special-case the first row + if ([_blurbNodeIndexPath compare:indexPath] == NSOrderedSame) { + BlurbNode *node = [[BlurbNode alloc] init]; + return node; + } + + NSValue *size = _kittenDataSource[indexPath.row - 1]; + NicCageNode *node = [[NicCageNode alloc] initWithKittenOfSize:size.CGSizeValue]; + return node; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + // blurb node + kLitterSize kitties + return 1 + _kittenDataSource.count; +} + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable selection for kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableViewLockDataSource:(ASTableView *)tableView +{ + self.dataSourceLocked = YES; +} + +- (void)tableViewUnlockDataSource:(ASTableView *)tableView +{ + self.dataSourceLocked = NO; +} + +- (BOOL)shouldBatchFetchForTableView:(UITableView *)tableView +{ + return _kittenDataSource.count < kMaxCageSize; +} + +- (void)tableView:(UITableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + sleep(1); + dispatch_async(dispatch_get_main_queue(), ^{ + + // populate a new array of random-sized kittens + NSArray *moarKittens = [self createLitterWithSize:kCageBatchSize]; + + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + + // find number of kittens in the data source and create their indexPaths + NSInteger existingRows = _kittenDataSource.count + 1; + + for (NSInteger i = 0; i < moarKittens.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:existingRows + i inSection:0]]; + } + + // add new kittens to the data source & notify table of new indexpaths + [_kittenDataSource addObjectsFromArray:moarKittens]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + + [context completeBatchFetching:YES]; + }); + }); +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable editing for Kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Assume only kitten nodes are editable (see -tableView:canEditRowAtIndexPath:). + [_kittenDataSource removeObjectAtIndex:indexPath.row - 1]; + [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/main.m b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/main.m new file mode 100644 index 0000000000..511cd1a7ac --- /dev/null +++ b/submodules/AsyncDisplayKit/examples_extra/VideoTableView/Sample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/plans/LayoutDebugger/Overview.md b/submodules/AsyncDisplayKit/plans/LayoutDebugger/Overview.md new file mode 100644 index 0000000000..593cbb1a79 --- /dev/null +++ b/submodules/AsyncDisplayKit/plans/LayoutDebugger/Overview.md @@ -0,0 +1,43 @@ +# Layout Debugger + +## Motivation + +The layout system is arguably one of the hardest parts to deal with in the framework. There are many factors, such as constrained size range, preferred size and flex properties, that make a layout spec to behave in a certain way. As a result, it's often hard to debug a layout, as well as to help debugging because we have to grab the whole context of the layout in order to pinpoint the problem at hand. Currently we have a couple of tools to help address this issue, namely ASCII art string and the layout inspector project from @hannahmbanana. + +While the layout inspector project is definitely a step toward the right direction and was proven to be helpful, I think we can do a lot more. One drawback of that project is that we didn't have a clear integration contract with the core components of the framework. We ended up introducing lots of complexities to ASDisplayNode that were hard to reason and mantain. Another problem is it was difficult to bootstrap the tool into existing code/layout. + +This project aims to provide a functional layout debugging solution that requires minimal change to the core framework, is easy to setup and works out-of-the-box on existing applications. These will be achieved by building an extension framework that is hosted in a separate repository, integrates with Texture core and leverages Chrome DevTools. + +## Execution plan + +At first, we'll build on top of [PonyDebugger](https://github.com/square/PonyDebugger). I played with it for a few hours and [the result](https://www.dropbox.com/s/8bcpdgogoewmox9/view%20debugger.mov?dl=0) looks promising. The bottom line is that we'll implement a new `PDDomainController` that is inspired by `PDDOMDomainController` but is catered toward `ASLayoutElement`. The controller should expose style properties, allow editting those properties and, as a result, support hot reloading. It should also expose the constrained size passed to each element during the previous layout pass. + +Eventually, we'll possibly move away from PonyDebugger and implement our own framework due to a few reasons: + +1. PonyDebugger is not actively maintained. That might be because it's considered a "done" project, although the amount of open issues may suggest otherwise. +2. It's not easy to setup the environment, especially because of the [ponyd gateway server](https://github.com/square/PonyDebugger/tree/master/ponyd). Ponyd is essentially a middleman that sits between the client code and Chrome DevTools. It is implemented in Python and hosts its own version of DevTools. Its bootstrap script is more or less broken. It's not trivial (at least for me) to setup a working environment. Instead, my limitted research showed that we can do better by letting the client app be a mDNS broadcaster and allowing Chrome DevTools to connect to it. The workflow will be very straight-forward for developers, similar to [Stetho](https://facebook.github.io/stetho/)'s. In addition, the whole project will be simpler because we don't need to maintain ponyd. +3. It contains other features besides layout debugging, such as network monitoring and remote logging. While they are absolutely useful, they are not in the scope of this project and add complexities to it. + +## Integration with Texture (core) + +As mentioned above, this framework will be a seperate project that integrates with Texture. Most of the changes in Texture's components, like `ASLayoutElement`, `ASDisplayNode` and `ASLayoutSpec`s, will be implemented as extensions inside the debugger framework. We'll try as much as we can to minimalize changes in Texture (core) that are needed to support this project. + +## Technical issues related to Texture (core) + +There are a few technical difficulties that we need to address: + +- Layout spec flattening: Currently `ASDisplayNode` flattens its layout tree right after it receives an `ASLayout`. As a result, `ASLayoutSpec` objects are discarded and are not available for inspecting/debugging. My current solution is introducing a new `shouldSkipFlattening` flag that tells `ASDisplayNode` to keep its layout tree as is. This flag defaults to `NO`. In addition, we need to update `-layoutSublayouts` to skip any non-node objects in the tree. We should avoid introducing runtime overheads to production code and projects that don't use the debugger. +- Style properties overrding: It's common for client code to set flex properties to subnodes inside `-layoutSpecThatFits:`. Doing this will override any values set to these properties by the debugger right before a layout pass which is needed for these changes to be taken into account. My current idea is adding a special `style` object that is loaded once from the exising object and can be changed by the debugger. This special object will be preferred over the built-in one when it's time to calculate a new layout. +- Manual layout is not supported: Layouts that are done manually (via `-calculateSizeThatFits:` and `-layout`) can't be updated by the debugger. Nodes inside these layouts are still available for inspecting though. + +## Long-term ideas + +Once we have a functional debugger with a solid foundation, we can start exploring below ideas: + +- Remote debugging: Since the client app is a mDNS broadcaster, I *think* it's possible to support remote debugging as well as pair programming: "I have a layout issue" "Let me connect to your runtime and inspect it". Crazy I know! Inspired by this [Chrome extension](https://github.com/auchenberg/devtools-remote). +- Layout spec injecting: We may try to abstract `-layoutSpecThatFits:` in such a way that the entire layout specification of a node is not only defined within the class but can be loaded (or manipulated) elsewhere, be it from the debugger or even a backend server. + +## Naming + +I'm planning to call this project "Texture Debugger". It'll be a suite of debugging tools tailored mainly for Texture framework. + diff --git a/submodules/AsyncDisplayKit/run_tests_update_status.sh b/submodules/AsyncDisplayKit/run_tests_update_status.sh new file mode 100755 index 0000000000..3f457c07ce --- /dev/null +++ b/submodules/AsyncDisplayKit/run_tests_update_status.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -eo pipefail + +UPDATE_STATUS_PATH=$1 +BUILDKITE_PULL_REQUEST=$2 +BUILDKITE_BUILD_URL=$3 + +function updateStatus() { + if [ "${BUILDKITE_PULL_REQUEST}" != "false" ] ; then + ${UPDATE_STATUS_PATH} "TextureGroup" "Texture" ${BUILDKITE_PULL_REQUEST} "$1" ${BUILDKITE_BUILD_URL} "$2" "CI/Pinterest" "$3" + fi +} + +if [[ -z "${UPDATE_STATUS_PATH}" || -z "${BUILDKITE_PULL_REQUEST}" || -z "${BUILDKITE_BUILD_URL}" ]] ; then + echo "Update status path (${UPDATE_STATUS_PATH}), pull request (${BUILDKITE_BUILD_URL}) or build url (${BUILDKITE_PULL_REQUEST}) unset." + trap - EXIT + exit 255 +fi + +trapped="false" +function trap_handler() { + if [[ "$trapped" = "false" ]]; then + updateStatus "failure" "Tests failed…" `pwd`/log.txt + echo "Tests failed, updated status to failure" + rm `pwd`/log.txt + fi + trapped="true" +} +trap trap_handler INT TERM EXIT + +updateStatus "pending" "Starting build…" + +echo "--- Running Danger" +bundle exec danger --verbose 2>&1|tee `pwd`/log.txt + +./build.sh all 2>&1|tee `pwd`/log.txt + +rm `pwd`/log.txt + +updateStatus "success" "Tests passed" + +echo "All tests succeeded, updated status to success" +trap - EXIT +exit 0 \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-568h@2x.png b/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-568h@2x.png new file mode 100644 index 0000000000..1547a98454 Binary files /dev/null and b/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-667h@2x.png b/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-667h@2x.png new file mode 100644 index 0000000000..988ea56bab Binary files /dev/null and b/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-736h@3x.png b/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-736h@3x.png new file mode 100644 index 0000000000..d19eb325a2 Binary files /dev/null and b/submodules/AsyncDisplayKit/smoke-tests/Framework/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/README.md b/submodules/AsyncDisplayKit/smoke-tests/Framework/README.md new file mode 100644 index 0000000000..cce2cb22fb --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/README.md @@ -0,0 +1,11 @@ +# "Framework" + +This is a very simple pseudo-"integration test" project that links against +AsyncDisplayKit as a dynamic framework, for Swift/Carthage users. + +If it fails to compile, Travis CI builds will fail. To escape from such dire straits: + +* If you've added a new class intended for public use, make sure you added its + header to the "Public" group of the "Headers" build phase in the + AsyncDisplayKit-iOS framework target. Note that this smoke test will only fail + if you remembered to add your new file to the umbrella helper. diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..62bb5e98b1 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,394 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7319D22E19004363C2 /* AppDelegate.swift */; }; + 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7519D22E19004363C2 /* ViewController.swift */; }; + 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */; }; + 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053D919EE266A00E385DE /* Default-667h@2x.png */; }; + 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 34566CA21BC1202A00715E6B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 058D09AC195D04C000B7D73C; + remoteInfo = AsyncDisplayKit; + }; + 34566CA41BC1202A00715E6B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 058D09BC195D04C000B7D73C; + remoteInfo = AsyncDisplayKitTests; + }; + 34566CA61BC1202A00715E6B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 057D02BF1AC0A66700C7AC3C; + remoteInfo = AsyncDisplayKitTestHost; + }; + 34566CAC1BC1204100715E6B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = B35061D91B010EDF0018CF92; + remoteInfo = "AsyncDisplayKit-iOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 34566CAE1BC1204100715E6B /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 050E7C6E19D22E19004363C2 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 050E7C7219D22E19004363C2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 050E7C7319D22E19004363C2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 050E7C7519D22E19004363C2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = AsyncDisplayKit.xcodeproj; path = ../../AsyncDisplayKit.xcodeproj; sourceTree = ""; }; + 34566CAF1BC1208200715E6B /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 34566CB11BC1208700715E6B /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 6C5053D919EE266A00E385DE /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 050E7C6B19D22E19004363C2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 050E7C6519D22E19004363C2 = { + isa = PBXGroup; + children = ( + 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */, + 050E7C7019D22E19004363C2 /* Sample */, + 050E7C6F19D22E19004363C2 /* Products */, + 092C2001FE124604891D6E90 /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 050E7C6F19D22E19004363C2 /* Products */ = { + isa = PBXGroup; + children = ( + 050E7C6E19D22E19004363C2 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 050E7C7019D22E19004363C2 /* Sample */ = { + isa = PBXGroup; + children = ( + 050E7C7319D22E19004363C2 /* AppDelegate.swift */, + 050E7C7519D22E19004363C2 /* ViewController.swift */, + 050E7C7119D22E19004363C2 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 050E7C7119D22E19004363C2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 050E7C7219D22E19004363C2 /* Info.plist */, + 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */, + 6C5053D919EE266A00E385DE /* Default-667h@2x.png */, + 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 092C2001FE124604891D6E90 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 34566CB11BC1208700715E6B /* AssetsLibrary.framework */, + 34566CAF1BC1208200715E6B /* Photos.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 34566C9C1BC1202A00715E6B /* Products */ = { + isa = PBXGroup; + children = ( + 34566CA31BC1202A00715E6B /* libAsyncDisplayKit.a */, + 34566CA51BC1202A00715E6B /* AsyncDisplayKitTests.xctest */, + 34566CA71BC1202A00715E6B /* AsyncDisplayKitTestHost.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 050E7C6D19D22E19004363C2 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 050E7C8D19D22E1A004363C2 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 050E7C6A19D22E19004363C2 /* Sources */, + 050E7C6B19D22E19004363C2 /* Frameworks */, + 050E7C6C19D22E19004363C2 /* Resources */, + 34566CAE1BC1204100715E6B /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 34566CAD1BC1204100715E6B /* PBXTargetDependency */, + ); + name = Sample; + productName = Sample; + productReference = 050E7C6E19D22E19004363C2 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 050E7C6619D22E19004363C2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 050E7C6D19D22E19004363C2 = { + CreatedOnToolsVersion = 6.0.1; + LastSwiftMigration = 0830; + }; + }; + }; + buildConfigurationList = 050E7C6919D22E19004363C2 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 050E7C6519D22E19004363C2; + productRefGroup = 050E7C6F19D22E19004363C2 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 34566C9C1BC1202A00715E6B /* Products */; + ProjectRef = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 050E7C6D19D22E19004363C2 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 34566CA31BC1202A00715E6B /* libAsyncDisplayKit.a */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = AsyncDisplayKit.framework; + remoteRef = 34566CA21BC1202A00715E6B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 34566CA51BC1202A00715E6B /* AsyncDisplayKitTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = AsyncDisplayKitTests.xctest; + remoteRef = 34566CA41BC1202A00715E6B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 34566CA71BC1202A00715E6B /* AsyncDisplayKitTestHost.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = AsyncDisplayKitTestHost.app; + remoteRef = 34566CA61BC1202A00715E6B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 050E7C6C19D22E19004363C2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */, + 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */, + 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 050E7C6A19D22E19004363C2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */, + 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 34566CAD1BC1204100715E6B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "AsyncDisplayKit-iOS"; + targetProxy = 34566CAC1BC1204100715E6B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 050E7C8B19D22E1A004363C2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 050E7C8C19D22E1A004363C2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 050E7C8E19D22E1A004363C2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 050E7C8F19D22E1A004363C2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 050E7C6919D22E19004363C2 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050E7C8B19D22E1A004363C2 /* Debug */, + 050E7C8C19D22E1A004363C2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 050E7C8D19D22E1A004363C2 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050E7C8E19D22E1A004363C2 /* Debug */, + 050E7C8F19D22E1A004363C2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 050E7C6619D22E19004363C2 /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..3cb0b0bd7b --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/AppDelegate.swift b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/AppDelegate.swift new file mode 100644 index 0000000000..a2c10adbba --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = UIColor.white + window.rootViewController = ViewController(nibName: nil, bundle: nil) + window.makeKeyAndVisible() + self.window = window + return true + } + +} diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/Info.plist b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/ViewController.swift b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/ViewController.swift new file mode 100644 index 0000000000..9d96ac7477 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Framework/Sample/ViewController.swift @@ -0,0 +1,61 @@ +// +// ViewController.swift +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit +import AsyncDisplayKit + +class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { + + var tableNode: ASTableNode + + + // MARK: UIViewController. + + override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + self.tableNode = ASTableNode() + + super.init(nibName: nil, bundle: nil) + + self.tableNode.dataSource = self + self.tableNode.delegate = self + } + + required init(coder aDecoder: NSCoder) { + fatalError("storyboards are incompatible with truth and beauty") + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.addSubview(self.tableNode.view) + } + + override func viewWillLayoutSubviews() { + self.tableNode.frame = self.view.bounds + } + + + // MARK: ASTableView data source and delegate. + + func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { + let patter = NSString(format: "[%ld.%ld] says hello!", indexPath.section, indexPath.row) + let node = ASTextCellNode() + node.text = patter as String + + return node + } + + func numberOfSections(in tableNode: ASTableNode) -> Int { + return 1 + } + + func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { + return 20 + } + +} diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..eeea76c2db --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Base.lproj/LaunchScreen.storyboard b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..ebf48f6039 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Info.plist b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Info.plist new file mode 100644 index 0000000000..eabb3ae346 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/main.m b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/main.m new file mode 100644 index 0000000000..feb53cc3f7 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life With Frameworks/main.m @@ -0,0 +1,16 @@ +// +// main.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..dd795b7545 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj @@ -0,0 +1,555 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 058968F51ABCE06E0059CE2A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 058968F41ABCE06E0059CE2A /* main.m */; }; + 058968F81ABCE06E0059CE2A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 058968F71ABCE06E0059CE2A /* AppDelegate.m */; }; + 058968FB1ABCE06E0059CE2A /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 058968FA1ABCE06E0059CE2A /* ViewController.m */; }; + 0589691B1ABCE0E80059CE2A /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 058969181ABCE0E80059CE2A /* Default-568h@2x.png */; }; + 0589691C1ABCE0E80059CE2A /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 058969191ABCE0E80059CE2A /* Default-667h@2x.png */; }; + 0589691D1ABCE0E80059CE2A /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0589691A1ABCE0E80059CE2A /* Default-736h@3x.png */; }; + 0589692A1ABCE17C0059CE2A /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058969291ABCE17C0059CE2A /* AssetsLibrary.framework */; }; + 0589692C1ABCE1820059CE2A /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0589692B1ABCE1820059CE2A /* Photos.framework */; }; + 92DD2FEC1BF4D8BB0074C9DD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92DD2FEB1BF4D8BB0074C9DD /* MapKit.framework */; }; + F729B8BB1D2E176700C9EDBC /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F729B8BA1D2E176700C9EDBC /* main.m */; }; + F729B8C61D2E176700C9EDBC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F729B8C51D2E176700C9EDBC /* Assets.xcassets */; }; + F729B8C91D2E176700C9EDBC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F729B8C71D2E176700C9EDBC /* LaunchScreen.storyboard */; }; + F729B8D11D2E17A300C9EDBC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 058968F71ABCE06E0059CE2A /* AppDelegate.m */; }; + F729B8D21D2E17A300C9EDBC /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 058968FA1ABCE06E0059CE2A /* ViewController.m */; }; + F7CE6CB61D2CE00800BE4C15 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7CE6CAD1D2CDFFB00BE4C15 /* AsyncDisplayKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F729B8D51D2E17C800C9EDBC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = B35061D91B010EDF0018CF92; + remoteInfo = "AsyncDisplayKit-iOS"; + }; + F7CE6CAC1D2CDFFB00BE4C15 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 058D09AC195D04C000B7D73C; + remoteInfo = AsyncDisplayKit; + }; + F7CE6CAE1D2CDFFB00BE4C15 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 058D09BC195D04C000B7D73C; + remoteInfo = AsyncDisplayKitTests; + }; + F7CE6CB01D2CDFFB00BE4C15 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 057D02BF1AC0A66700C7AC3C; + remoteInfo = AsyncDisplayKitTestHost; + }; + F7CE6CB41D2CE00300BE4C15 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = B35061D91B010EDF0018CF92; + remoteInfo = AsyncDisplayKit; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + F729B8D71D2E17C800C9EDBC /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 058968EF1ABCE06E0059CE2A /* Life Without CocoaPods.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Life Without CocoaPods.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 058968F31ABCE06E0059CE2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 058968F41ABCE06E0059CE2A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 058968F61ABCE06E0059CE2A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 058968F71ABCE06E0059CE2A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 058968F91ABCE06E0059CE2A /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 058968FA1ABCE06E0059CE2A /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 058969181ABCE0E80059CE2A /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 058969191ABCE0E80059CE2A /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = ""; }; + 0589691A1ABCE0E80059CE2A /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = ""; }; + 058969291ABCE17C0059CE2A /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 0589692B1ABCE1820059CE2A /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 92DD2FEB1BF4D8BB0074C9DD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + F729B8B71D2E176700C9EDBC /* Life With Frameworks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Life With Frameworks.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + F729B8BA1D2E176700C9EDBC /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + F729B8C51D2E176700C9EDBC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F729B8C81D2E176700C9EDBC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F729B8CA1D2E176700C9EDBC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = AsyncDisplayKit.xcodeproj; path = ../../AsyncDisplayKit.xcodeproj; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 058968EC1ABCE06E0059CE2A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F7CE6CB61D2CE00800BE4C15 /* AsyncDisplayKit.framework in Frameworks */, + 92DD2FEC1BF4D8BB0074C9DD /* MapKit.framework in Frameworks */, + 0589692C1ABCE1820059CE2A /* Photos.framework in Frameworks */, + 0589692A1ABCE17C0059CE2A /* AssetsLibrary.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F729B8B41D2E176700C9EDBC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058968E61ABCE06E0059CE2A = { + isa = PBXGroup; + children = ( + 92DD2FEB1BF4D8BB0074C9DD /* MapKit.framework */, + 0589692B1ABCE1820059CE2A /* Photos.framework */, + 058969291ABCE17C0059CE2A /* AssetsLibrary.framework */, + F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */, + 058968F11ABCE06E0059CE2A /* Life Without CocoaPods */, + F729B8B81D2E176700C9EDBC /* Life With Frameworks */, + 058968F01ABCE06E0059CE2A /* Products */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 058968F01ABCE06E0059CE2A /* Products */ = { + isa = PBXGroup; + children = ( + 058968EF1ABCE06E0059CE2A /* Life Without CocoaPods.app */, + F729B8B71D2E176700C9EDBC /* Life With Frameworks.app */, + ); + name = Products; + sourceTree = ""; + }; + 058968F11ABCE06E0059CE2A /* Life Without CocoaPods */ = { + isa = PBXGroup; + children = ( + 058968F61ABCE06E0059CE2A /* AppDelegate.h */, + 058968F71ABCE06E0059CE2A /* AppDelegate.m */, + 058968F91ABCE06E0059CE2A /* ViewController.h */, + 058968FA1ABCE06E0059CE2A /* ViewController.m */, + 058968F21ABCE06E0059CE2A /* Supporting Files */, + ); + path = "Life Without CocoaPods"; + sourceTree = ""; + }; + 058968F21ABCE06E0059CE2A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 058969181ABCE0E80059CE2A /* Default-568h@2x.png */, + 058969191ABCE0E80059CE2A /* Default-667h@2x.png */, + 0589691A1ABCE0E80059CE2A /* Default-736h@3x.png */, + 058968F31ABCE06E0059CE2A /* Info.plist */, + 058968F41ABCE06E0059CE2A /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + F729B8B81D2E176700C9EDBC /* Life With Frameworks */ = { + isa = PBXGroup; + children = ( + F729B8C51D2E176700C9EDBC /* Assets.xcassets */, + F729B8C71D2E176700C9EDBC /* LaunchScreen.storyboard */, + F729B8CA1D2E176700C9EDBC /* Info.plist */, + F729B8B91D2E176700C9EDBC /* Supporting Files */, + ); + path = "Life With Frameworks"; + sourceTree = ""; + }; + F729B8B91D2E176700C9EDBC /* Supporting Files */ = { + isa = PBXGroup; + children = ( + F729B8BA1D2E176700C9EDBC /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + F7CE6CA61D2CDFFB00BE4C15 /* Products */ = { + isa = PBXGroup; + children = ( + F7CE6CAD1D2CDFFB00BE4C15 /* AsyncDisplayKit.framework */, + F7CE6CAF1D2CDFFB00BE4C15 /* AsyncDisplayKitTests.xctest */, + F7CE6CB11D2CDFFB00BE4C15 /* AsyncDisplayKitTestHost.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 058968EE1ABCE06E0059CE2A /* Life Without CocoaPods */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058969121ABCE06E0059CE2A /* Build configuration list for PBXNativeTarget "Life Without CocoaPods" */; + buildPhases = ( + 058968EB1ABCE06E0059CE2A /* Sources */, + 058968EC1ABCE06E0059CE2A /* Frameworks */, + 058968ED1ABCE06E0059CE2A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7CE6CB51D2CE00300BE4C15 /* PBXTargetDependency */, + ); + name = "Life Without CocoaPods"; + productName = "Life Without CocoaPods"; + productReference = 058968EF1ABCE06E0059CE2A /* Life Without CocoaPods.app */; + productType = "com.apple.product-type.application"; + }; + F729B8B61D2E176700C9EDBC /* Life With Frameworks */ = { + isa = PBXNativeTarget; + buildConfigurationList = F729B8D01D2E176700C9EDBC /* Build configuration list for PBXNativeTarget "Life With Frameworks" */; + buildPhases = ( + F729B8B31D2E176700C9EDBC /* Sources */, + F729B8B41D2E176700C9EDBC /* Frameworks */, + F729B8B51D2E176700C9EDBC /* Resources */, + F729B8D71D2E17C800C9EDBC /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + F729B8D61D2E17C800C9EDBC /* PBXTargetDependency */, + ); + name = "Life With Frameworks"; + productName = "Life With Frameworks"; + productReference = F729B8B71D2E176700C9EDBC /* Life With Frameworks.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 058968E71ABCE06E0059CE2A /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 058968EE1ABCE06E0059CE2A = { + CreatedOnToolsVersion = 6.2; + }; + F729B8B61D2E176700C9EDBC = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 058968EA1ABCE06E0059CE2A /* Build configuration list for PBXProject "Life Without CocoaPods" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 058968E61ABCE06E0059CE2A; + productRefGroup = 058968F01ABCE06E0059CE2A /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = F7CE6CA61D2CDFFB00BE4C15 /* Products */; + ProjectRef = F7CE6CA51D2CDFFB00BE4C15 /* AsyncDisplayKit.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 058968EE1ABCE06E0059CE2A /* Life Without CocoaPods */, + F729B8B61D2E176700C9EDBC /* Life With Frameworks */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + F7CE6CAD1D2CDFFB00BE4C15 /* AsyncDisplayKit.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = AsyncDisplayKit.framework; + remoteRef = F7CE6CAC1D2CDFFB00BE4C15 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + F7CE6CAF1D2CDFFB00BE4C15 /* AsyncDisplayKitTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = AsyncDisplayKitTests.xctest; + remoteRef = F7CE6CAE1D2CDFFB00BE4C15 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + F7CE6CB11D2CDFFB00BE4C15 /* AsyncDisplayKitTestHost.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = AsyncDisplayKitTestHost.app; + remoteRef = F7CE6CB01D2CDFFB00BE4C15 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 058968ED1ABCE06E0059CE2A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0589691B1ABCE0E80059CE2A /* Default-568h@2x.png in Resources */, + 0589691C1ABCE0E80059CE2A /* Default-667h@2x.png in Resources */, + 0589691D1ABCE0E80059CE2A /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F729B8B51D2E176700C9EDBC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F729B8C91D2E176700C9EDBC /* LaunchScreen.storyboard in Resources */, + F729B8C61D2E176700C9EDBC /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 058968EB1ABCE06E0059CE2A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058968FB1ABCE06E0059CE2A /* ViewController.m in Sources */, + 058968F81ABCE06E0059CE2A /* AppDelegate.m in Sources */, + 058968F51ABCE06E0059CE2A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F729B8B31D2E176700C9EDBC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F729B8D21D2E17A300C9EDBC /* ViewController.m in Sources */, + F729B8D11D2E17A300C9EDBC /* AppDelegate.m in Sources */, + F729B8BB1D2E176700C9EDBC /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F729B8D61D2E17C800C9EDBC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "AsyncDisplayKit-iOS"; + targetProxy = F729B8D51D2E17C800C9EDBC /* PBXContainerItemProxy */; + }; + F7CE6CB51D2CE00300BE4C15 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = AsyncDisplayKit; + targetProxy = F7CE6CB41D2CE00300BE4C15 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + F729B8C71D2E176700C9EDBC /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F729B8C81D2E176700C9EDBC /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 058969101ABCE06E0059CE2A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 058969111ABCE06E0059CE2A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_GENERATE_TEST_COVERAGE_FILES = YES; + GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 058969131ABCE06E0059CE2A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "Life Without CocoaPods/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 058969141ABCE06E0059CE2A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "Life Without CocoaPods/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F729B8CB1D2E176700C9EDBC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Life With Frameworks/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.Life-With-Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F729B8CC1D2E176700C9EDBC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Life With Frameworks/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.Life-With-Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 058968EA1ABCE06E0059CE2A /* Build configuration list for PBXProject "Life Without CocoaPods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058969101ABCE06E0059CE2A /* Debug */, + 058969111ABCE06E0059CE2A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058969121ABCE06E0059CE2A /* Build configuration list for PBXNativeTarget "Life Without CocoaPods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058969131ABCE06E0059CE2A /* Debug */, + 058969141ABCE06E0059CE2A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F729B8D01D2E176700C9EDBC /* Build configuration list for PBXNativeTarget "Life With Frameworks" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F729B8CB1D2E176700C9EDBC /* Debug */, + F729B8CC1D2E176700C9EDBC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 058968E71ABCE06E0059CE2A /* Project object */; +} diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a9e8c819ee --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/xcshareddata/xcschemes/Life Without CocoaPods.xcscheme b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/xcshareddata/xcschemes/Life Without CocoaPods.xcscheme new file mode 100644 index 0000000000..2abacce743 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/xcshareddata/xcschemes/Life Without CocoaPods.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcworkspace/contents.xcworkspacedata b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..b2fcbec3ec --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h new file mode 100644 index 0000000000..75aff7fc14 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m new file mode 100644 index 0000000000..d6890ff2e3 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m @@ -0,0 +1,24 @@ +// +// AppDelegate.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-568h@2x.png b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-568h@2x.png differ diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-667h@2x.png b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-667h@2x.png differ diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-736h@3x.png b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Default-736h@3x.png differ diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Info.plist b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Info.plist new file mode 100644 index 0000000000..fb4115c84c --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h new file mode 100644 index 0000000000..261e9ff480 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h @@ -0,0 +1,15 @@ +// +// ViewController.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ViewController : UIViewController + + +@end + diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m new file mode 100644 index 0000000000..106ee524f4 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m @@ -0,0 +1,28 @@ +// +// ViewController.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ViewController.h" + +#import + +@interface ViewController () +@property (nonatomic, strong) ASTextNode *textNode; +@end + +@implementation ViewController + +- (void)viewDidLoad +{ + self.textNode = [[ASTextNode alloc] init]; + self.textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Testing, testing." attributes:@{ NSForegroundColorAttributeName: [UIColor redColor] }]; + [self.textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, self.view.bounds.size)]; + self.textNode.frame = (CGRect){ .origin = CGPointZero, .size = self.textNode.calculatedSize }; + [self.view addSubnode:self.textNode]; +} + +@end diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m new file mode 100644 index 0000000000..ec43dab250 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m @@ -0,0 +1,16 @@ +// +// main.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/README.md b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/README.md new file mode 100644 index 0000000000..c20f63a5df --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/Life Without CocoaPods/README.md @@ -0,0 +1,14 @@ +# "Life Without CocoaPods" + +This is a very simple pseudo-"integration test" project that links against +AsyncDisplayKit manually, rather than using CocoaPods. If it fails to compile, +Travis CI builds will fail. To escape from such dire straits: + +* If you've added a new class intended for public use, make sure you added its + header to the "Public" group of the "Headers" build phase in the + AsyncDisplayKit target. Note that this smoke test will only fail if you + remembered to add your new file to the umbrella helper. + +* If you added a new framework dependency (like AssetsLibrary or Photos), add + it to this project's Link Binary With Libraries build phase and update the + project README (both README.md and docs/index.md). diff --git a/submodules/AsyncDisplayKit/smoke-tests/README.md b/submodules/AsyncDisplayKit/smoke-tests/README.md new file mode 100644 index 0000000000..2c0e607456 --- /dev/null +++ b/submodules/AsyncDisplayKit/smoke-tests/README.md @@ -0,0 +1,3 @@ +# Integration tests + +See READMEs in subfolders.