From a920e353c63168c00ce57a2202adbbde2beee9d4 Mon Sep 17 00:00:00 2001 From: rcancro Date: Fri, 12 Feb 2016 11:11:06 -0800 Subject: [PATCH 01/16] adjust font size to make text fit within constrained size # Conflicts: # AsyncDisplayKit/ASTextNode.mm --- AsyncDisplayKit/ASTextNode+Beta.h | 6 +- AsyncDisplayKit/ASTextNode.mm | 16 +- AsyncDisplayKit/TextKit/ASTextKitAttributes.h | 18 +- .../TextKit/ASTextKitFontSizeAdjuster.h | 46 ++++- .../TextKit/ASTextKitFontSizeAdjuster.m | 98 ---------- .../TextKit/ASTextKitFontSizeAdjuster.mm | 185 ++++++++++++++++++ AsyncDisplayKit/TextKit/ASTextKitRenderer.h | 2 + AsyncDisplayKit/TextKit/ASTextKitRenderer.mm | 42 +++- 8 files changed, 284 insertions(+), 129 deletions(-) delete mode 100644 AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m create mode 100644 AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm diff --git a/AsyncDisplayKit/ASTextNode+Beta.h b/AsyncDisplayKit/ASTextNode+Beta.h index 8c9bd0286c..38059aa7c3 100644 --- a/AsyncDisplayKit/ASTextNode+Beta.h +++ b/AsyncDisplayKit/ASTextNode+Beta.h @@ -10,9 +10,9 @@ @interface ASTextNode () /** - @abstract The minimum scale that the textnode can apply to fit long words. - @default 0 (No scaling) + @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 + @default nil (no scaling) */ -@property (nonatomic, assign) CGFloat minimumScaleFactor; +@property (nonatomic, copy) NSArray *pointSizeScaleFactors; @end \ No newline at end of file diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index a2b9f3121a..a6c17e5bf0 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -18,6 +18,7 @@ #import "ASTextKitCoreTextAdditions.h" #import "ASTextKitHelpers.h" +#import "ASTextKitFontSizeAdjuster.h" #import "ASTextKitRenderer.h" #import "ASTextKitRenderer+Positioning.h" #import "ASTextKitShadower.h" @@ -78,6 +79,7 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation CGSize _constrainedSize; ASTextKitRenderer *_renderer; + CGFloat _currentScaleFactor; UILongPressGestureRecognizer *_longPressGestureRecognizer; } @@ -242,7 +244,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; .lineBreakMode = _truncationMode, .maximumNumberOfLines = _maximumNumberOfLines, .exclusionPaths = _exclusionPaths, - .minimumScaleFactor = _minimumScaleFactor, + .pointSizeScaleFactors = _pointSizeScaleFactors, + .currentScaleFactor = _currentScaleFactor, }; } @@ -256,6 +259,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // actually dealloc. __block ASTextKitRenderer *renderer = _renderer; ASPerformBlockOnBackgroundThread(^{ + // before we remove the renderer, take its scale factor so we set it when a new renderer is created + _currentScaleFactor = renderer.currentScaleFactor; renderer = nil; }); _renderer = nil; @@ -1059,16 +1064,15 @@ static NSAttributedString *DefaultTruncationAttributedString() return visibleRange.length < _attributedString.length; } -- (void)setMinimumScaleFactor:(CGFloat)minimumScaleFactor +- (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors { - if (_minimumScaleFactor != minimumScaleFactor) { - _minimumScaleFactor = minimumScaleFactor; + if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) { + _pointSizeScaleFactors = pointSizeScaleFactors; [self _invalidateRenderer]; ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ [self setNeedsDisplay]; }); - } -} + }} - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines { diff --git a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h index d7bf44fef2..4d2160ea33 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h +++ b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h @@ -58,6 +58,7 @@ struct ASTextKitAttributes { 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; /** @@ -82,9 +83,13 @@ struct ASTextKitAttributes { */ CGFloat shadowRadius; /** - The minimum scale that the textnode can apply to fit long words in constrained size. + An array of scale factors in descending order to apply to the text to try to make it fit into a constrained size. */ - CGFloat minimumScaleFactor; + NSArray *pointSizeScaleFactors; + /** + The currently applied scale factor. Only valid if pointSizeScaleFactors are provided. Defaults to 0 (no scaling) + */ + CGFloat currentScaleFactor; /** A pointer to a function that that returns a custom layout manager subclass. If nil, defaults to NSLayoutManager. */ @@ -112,8 +117,10 @@ struct ASTextKitAttributes { [shadowColor copy], shadowOpacity, shadowRadius, - minimumScaleFactor, - layoutManagerFactory + pointSizeScaleFactors, + currentScaleFactor, + layoutManagerFactory, + layoutManagerDelegate, }; }; @@ -124,7 +131,8 @@ struct ASTextKitAttributes { && maximumNumberOfLines == other.maximumNumberOfLines && shadowOpacity == other.shadowOpacity && shadowRadius == other.shadowRadius - && minimumScaleFactor == other.minimumScaleFactor + && [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors] + && currentScaleFactor == currentScaleFactor && layoutManagerFactory == other.layoutManagerFactory && CGSizeEqualToSize(shadowOffset, other.shadowOffset) && _objectsEqual(exclusionPaths, other.exclusionPaths) diff --git a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h index 84ba5fa367..cd90726dd0 100644 --- a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h +++ b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h @@ -1,20 +1,46 @@ -// -// ASTextKitFontSizeAdjuster.h -// AsyncDisplayKit -// -// Created by Luke on 1/20/16. -// Copyright © 2016 Facebook. All rights reserved. -// +/* 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. + */ #import +#import "ASTextKitAttributes.h" +#import "ASTextKitContext.h" @interface ASTextKitFontSizeAdjuster : NSObject @property (nonatomic, assign) 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 - minimumScaleFactor:(CGFloat)minimumScaleFactor - constrainedSize:(CGSize)constrainedSize; + 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; -- (void) adjustFontSize; @end + + diff --git a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m deleted file mode 100644 index 731c19c208..0000000000 --- a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m +++ /dev/null @@ -1,98 +0,0 @@ -// -// ASTextKitFontSizeAdjuster.m -// AsyncDisplayKit -// -// Created by Luke on 1/20/16. -// Copyright © 2016 Facebook. All rights reserved. -// - -#import "ASTextKitContext.h" -#import "ASTextKitFontSizeAdjuster.h" - -@implementation ASTextKitFontSizeAdjuster -{ - __weak ASTextKitContext *_context; - CGFloat _minimumScaleFactor; -} - -- (instancetype)initWithContext:(ASTextKitContext *)context - minimumScaleFactor:(CGFloat)minimumScaleFactor - constrainedSize:(CGSize)constrainedSize -{ - if (self = [super init]) { - _context = context; - _minimumScaleFactor = minimumScaleFactor; - _constrainedSize = constrainedSize; - } - return self; -} - -- (CGSize)sizeForAttributedString:(NSAttributedString *)attrString -{ - return [attrString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) - options:NSStringDrawingUsesLineFragmentOrigin - context:nil].size; -} - - -- (void) adjustFontSizeForAttributeString:(NSMutableAttributedString *)attrString withScaleFactor:(CGFloat)scaleFactor -{ - { - [attrString beginEditing]; - - [attrString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attrString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { - - UIFont* font = value; - font = [font fontWithSize:font.pointSize * scaleFactor]; - - [attrString removeAttribute:NSFontAttributeName range:range]; - [attrString addAttribute:NSFontAttributeName value:font range:range]; - }]; - - [attrString endEditing]; - } -} - - -- (void)adjustFontSize -{ - if (_minimumScaleFactor <= 0 || _minimumScaleFactor >= 1) { - return; - } - [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { - NSString *str = textStorage.string; - NSArray *words = [str componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - NSString *longestWordNeedingResize = @""; - for (NSString *word in words) { - if ([word length] > [longestWordNeedingResize length]) { - longestWordNeedingResize = word; - } - } - - if ([longestWordNeedingResize length] == 0) { - return; - } - - NSRange range = [str rangeOfString:longestWordNeedingResize]; - NSMutableAttributedString *attrString = [textStorage attributedSubstringFromRange:range].mutableCopy; - CGSize defaultSize = [self sizeForAttributedString:attrString]; - - if (defaultSize.width > _constrainedSize.width) { - [attrString removeAttribute:NSParagraphStyleAttributeName range:NSMakeRange(0, [attrString length])]; - NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init]; - context.minimumScaleFactor = _minimumScaleFactor; - [attrString boundingRectWithSize:CGSizeMake(_constrainedSize.width, defaultSize.height) - options:NSStringDrawingUsesLineFragmentOrigin - context:context]; - - [self adjustFontSizeForAttributeString:attrString withScaleFactor:context.actualScaleFactor]; - - if ([self sizeForAttributedString:attrString].width <= _constrainedSize.width) { - [self adjustFontSizeForAttributeString:textStorage withScaleFactor:context.actualScaleFactor]; - NSLog(@"ASTextKitFontSizeAdjuster : adjusted \"%@\"to fontsize actualScaleFactor:%f", longestWordNeedingResize, context.actualScaleFactor); - } - } - }]; -} - -@end diff --git a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm new file mode 100644 index 0000000000..4f92c607aa --- /dev/null +++ b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm @@ -0,0 +1,185 @@ +/* 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. + */ + +#import "ASTextKitContext.h" +#import "ASTextKitFontSizeAdjuster.h" +#import "ASLayoutManager.h" + +#import + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +@implementation ASTextKitFontSizeAdjuster +{ + __weak ASTextKitContext *_context; + ASTextKitAttributes _attributes; + std::mutex _textKitMutex; +} + +- (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 +{ + [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:roundf(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); + paragraphStyle.lineHeightMultiple = (paragraphStyle.lineHeightMultiple * scaleFactor); + paragraphStyle.paragraphSpacing = (paragraphStyle.paragraphSpacing * scaleFactor); + + [attrString removeAttribute:NSParagraphStyleAttributeName range:range]; + [attrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + } + + }]; + + [attrString endEditing]; +} + +- (NSUInteger)lineCountForString:(NSAttributedString *)attributedString +{ + NSUInteger lineCount = 0; + + static std::mutex __static_mutex; + std::lock_guard l(__static_mutex); + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; + NSLayoutManager *layoutManager = _attributes.layoutManagerFactory ? _attributes.layoutManagerFactory() : [[ASLayoutManager alloc] init]; + layoutManager.usesFontLeading = NO; + [textStorage addLayoutManager:layoutManager]; + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:_constrainedSize]; + + textContainer.lineFragmentPadding = 0; + textContainer.lineBreakMode = _attributes.lineBreakMode; + + // use 0 regardless of what is in the attributes so that we get an accurate line count + textContainer.maximumNumberOfLines = 0; + textContainer.exclusionPaths = _attributes.exclusionPaths; + [layoutManager addTextContainer:textContainer]; + + NSRange lineRange = { 0, 0 }; + while (NSMaxRange(lineRange) < [layoutManager numberOfGlyphs]/* && lineCount <= _attributes.maximumNumberOfLines*/) { + [layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange]; + lineCount++; + } + + return lineCount; +} + +- (CGFloat)scaleFactor +{ + if ([_attributes.pointSizeScaleFactors count] == 0 || isinf(_constrainedSize.width)) { + return 1.0; + } + + __block CGFloat adjustedScale = 1.0; + + NSArray *scaleFactors = _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; + } + } + + NSUInteger scaleIndex = 0; + + // find the longest word and make sure it fits in the constrained width + if ([longestWordNeedingResize length] > 0) { + + NSRange longestWordRange = [str rangeOfString:longestWordNeedingResize]; + NSMutableAttributedString *attrString = [textStorage attributedSubstringFromRange:longestWordRange].mutableCopy; + CGSize longestWordSize = [attrString boundingRectWithSize:CGSizeMake(FLT_MAX, FLT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; + + // check if the longest word is larger than our constrained width + if (longestWordSize.width > _constrainedSize.width) { + + // we have a word that is too long. Loop through our scale factors until we fit + for (NSNumber *scaleFactor in scaleFactors) { + // even if we still don't fit, save this scaleFactor so more of the word will fit + adjustedScale = [scaleFactor floatValue]; + + // adjust here so we start at the proper place in our scale array if we have too many lines + scaleIndex++; + + if (ceilf(longestWordSize.width * [scaleFactor floatValue]) <= _constrainedSize.width) { + // we fit! we are done + break; + } + } + } + } + + if (_attributes.maximumNumberOfLines > 0) { + // get the number of lines in our possibly scaled string + NSUInteger numberOfLines = [self lineCountForString:textStorage]; + if (numberOfLines > _attributes.maximumNumberOfLines) { + + for (NSUInteger index = scaleIndex; index < scaleFactors.count; index++) { + NSMutableAttributedString *entireAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage]; + [[self class] adjustFontSizeForAttributeString:entireAttributedString withScaleFactor:[scaleFactors[index] floatValue]]; + + + // save away this scale factor. Even if we don't fit completely we should still scale down + adjustedScale = [scaleFactors[index] floatValue]; + + if ([self lineCountForString:entireAttributedString] <= _attributes.maximumNumberOfLines) { + // we fit! we are done + break; + } + } + + } + } + + }]; + return adjustedScale; +} + +@end diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.h b/AsyncDisplayKit/TextKit/ASTextKitRenderer.h index 75fc5e03a9..1889131f52 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.h +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.h @@ -55,6 +55,8 @@ @property (nonatomic, assign, readwrite) CGSize constrainedSize; +@property (nonatomic, assign, readonly) CGFloat currentScaleFactor; + #pragma mark - Drawing /* Draw the renderer's text content into the bounds provided. diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index 05996c4de3..825ce2e26e 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -49,6 +49,9 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() _constrainedSize = constrainedSize; _attributes = attributes; _sizeIsCalculated = NO; + if ([attributes.pointSizeScaleFactors count] > 0) { + _currentScaleFactor = attributes.currentScaleFactor; + } } return self; } @@ -84,8 +87,8 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() // We must inset the constrained size by the size of the shadower. CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] - minimumScaleFactor:attributes.minimumScaleFactor - constrainedSize:shadowConstrainedSize]; + constrainedSize:shadowConstrainedSize + textKitAttributes:attributes]; } return _fontSizeAdjuster; } @@ -137,8 +140,8 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (void)_calculateSize { [self truncater]; - if (_attributes.minimumScaleFactor < 1 && _attributes.minimumScaleFactor > 0) { - [[self fontSizeAdjuster] adjustFontSize]; + if ([_attributes.pointSizeScaleFactors count] > 0) { + _currentScaleFactor = [[self fontSizeAdjuster] scaleFactor]; } // Force glyph generation and layout, which may not have happened yet (and isn't triggered by @@ -156,8 +159,12 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() // 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, {.size = constrainedRect.size}); - - _calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; + CGSize boundingSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; + _calculatedSize = CGSizeMake(boundingSize.width, boundingSize.height); + + if (_currentScaleFactor > 0.0 && _currentScaleFactor < 1.0) { + _calculatedSize.height = ceilf(_calculatedSize.height * _currentScaleFactor); + } } #pragma mark - Drawing @@ -176,11 +183,32 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() LOG(@"%@, shadowInsetBounds = %@",self, NSStringFromCGRect(shadowInsetBounds)); [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + + NSTextStorage *scaledTextStorage = nil; + BOOL isScaled = (self.currentScaleFactor > 0 && self.currentScaleFactor < 1.0); + + 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:_currentScaleFactor]; + scaledTextStorage = [[NSTextStorage alloc] initWithAttributedString:scaledString]; + + [textStorage removeLayoutManager:layoutManager]; + [scaledTextStorage addLayoutManager:layoutManager]; + } + LOG(@"usedRect: %@", NSStringFromCGRect([layoutManager usedRectForTextContainer:textContainer])); - NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + NSRange glyphRange = [layoutManager glyphRangeForBoundingRect:CGRectMake(0,0,textContainer.size.width, textContainer.size.height) 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(); From 0e7fae1825d1df3404de64f1a8b868b5794548a8 Mon Sep 17 00:00:00 2001 From: rcancro Date: Fri, 12 Feb 2016 11:22:13 -0800 Subject: [PATCH 02/16] reverted some debug code --- AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm index 4f92c607aa..f1be0df651 100644 --- a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.mm @@ -95,10 +95,8 @@ textContainer.exclusionPaths = _attributes.exclusionPaths; [layoutManager addTextContainer:textContainer]; - NSRange lineRange = { 0, 0 }; - while (NSMaxRange(lineRange) < [layoutManager numberOfGlyphs]/* && lineCount <= _attributes.maximumNumberOfLines*/) { + for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [layoutManager numberOfGlyphs] && lineCount <= _attributes.maximumNumberOfLines; lineCount++) { [layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange]; - lineCount++; } return lineCount; From d4ca8f4fd816077430bc08da0f3803132d4d9cbb Mon Sep 17 00:00:00 2001 From: rcancro Date: Tue, 16 Feb 2016 08:56:09 -0800 Subject: [PATCH 03/16] add ASTextKitFontAdjuster.mm to the build. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 285d1b7ad2..e57d0a3ff2 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -286,12 +286,13 @@ 9C8221961BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */; }; 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */; }; 9C8221981BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */; }; + 9C8898BB1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */; }; + 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */; }; + 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */ = {isa = PBXBuildFile; fileRef = A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */; }; 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9CDC18CD1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */; }; - A32FEDD51C501B6A004F642A /* ASTextKitFontSizeAdjuster.h in Headers */ = {isa = PBXBuildFile; fileRef = A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */; }; - A32FEDD61C501B6A004F642A /* ASTextKitFontSizeAdjuster.m in Sources */ = {isa = PBXBuildFile; fileRef = A32FEDD41C501B6A004F642A /* ASTextKitFontSizeAdjuster.m */; }; - A32FEDD71C5042C1004F642A /* ASTextKitFontSizeAdjuster.m in Sources */ = {isa = PBXBuildFile; fileRef = A32FEDD41C501B6A004F642A /* ASTextKitFontSizeAdjuster.m */; }; + A32FEDD51C501B6A004F642A /* ASTextKitFontSizeAdjuster.h in Headers */ = {isa = PBXBuildFile; fileRef = A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */; settings = {ATTRIBUTES = (Public, ); }; }; A373200F1C571B730011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */; }; @@ -715,10 +716,10 @@ 9C6BB3B01B8CC9C200F13F52 /* ASStaticLayoutable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStaticLayoutable.h; path = AsyncDisplayKit/Layout/ASStaticLayoutable.h; sourceTree = ""; }; 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackBaselinePositionedLayout.h; sourceTree = ""; }; 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackBaselinePositionedLayout.mm; sourceTree = ""; }; + 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitFontSizeAdjuster.mm; path = TextKit/ASTextKitFontSizeAdjuster.mm; sourceTree = ""; }; 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutablePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutablePrivate.h; sourceTree = ""; }; 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionViewTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitFontSizeAdjuster.h; path = TextKit/ASTextKitFontSizeAdjuster.h; sourceTree = ""; }; - A32FEDD41C501B6A004F642A /* ASTextKitFontSizeAdjuster.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASTextKitFontSizeAdjuster.m; path = TextKit/ASTextKitFontSizeAdjuster.m; sourceTree = ""; }; A373200E1C571B050011FC94 /* ASTextNode+Beta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASTextNode+Beta.h"; sourceTree = ""; }; AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASStaticLayoutSpecSnapshotTests.m; sourceTree = ""; }; AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASChangeSetDataController.h; sourceTree = ""; }; @@ -1206,7 +1207,7 @@ 257754A11BEE44CD00737CA5 /* ASTextKitTailTruncater.h */, 257754A21BEE44CD00737CA5 /* ASTextKitTailTruncater.mm */, A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */, - A32FEDD41C501B6A004F642A /* ASTextKitFontSizeAdjuster.m */, + 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */, 257754A31BEE44CD00737CA5 /* ASTextKitTruncating.h */, 257754A41BEE44CD00737CA5 /* ASEqualityHashHelpers.h */, 2577548F1BED289A00737CA5 /* ASEqualityHashHelpers.mm */, @@ -1470,6 +1471,7 @@ B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */, 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, + 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */, 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */, 509E68631B3AEDB4009B9150 /* ASCollectionViewLayoutController.h in Headers */, @@ -1810,7 +1812,6 @@ 257754B01BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.mm in Sources */, 0516FA411A1563D200B4EBED /* ASMultiplexImageNode.mm in Sources */, DECBD6E91BE56E1900CF4905 /* ASButtonNode.mm in Sources */, - A32FEDD61C501B6A004F642A /* ASTextKitFontSizeAdjuster.m in Sources */, 058D0A1B195D050800B7D73C /* ASMutableAttributedStringBuilder.m in Sources */, 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */, AEB7B01B1C5962EA00662EF4 /* ASDefaultPlayButton.m in Sources */, @@ -1824,6 +1825,7 @@ ACF6ED2E1B17843500DA7C62 /* ASRatioLayoutSpec.mm in Sources */, AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */, 205F0E121B371BD7007741D0 /* ASScrollDirection.m in Sources */, + 9C8898BB1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, D785F6631A74327E00291744 /* ASScrollNode.m in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */, @@ -1910,6 +1912,7 @@ 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, 254C6B861BF94F8A003EC431 /* ASTextKitContext.mm in Sources */, DBDB83971C6E879900D0098C /* ASPagerFlowLayout.m in Sources */, + 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.m in Sources */, B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, @@ -1945,7 +1948,6 @@ 9C5FA3541B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */, 9C5FA3601B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */, DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, - A32FEDD71C5042C1004F642A /* ASTextKitFontSizeAdjuster.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, From 8d603bf5e666d0b6cb165319e9120c97be0ff2a2 Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Wed, 17 Feb 2016 16:49:15 +0700 Subject: [PATCH 04/16] Check loadedImageIdentifer before sending it to delegate --- AsyncDisplayKit/ASMultiplexImageNode.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index e407e2a47d..5eb0abcfcb 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -219,7 +219,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent [self _setDownloadIdentifier:nil]; - if (_cacheSupportsClearing) { + if (_cacheSupportsClearing && self.loadedImageIdentifier != nil) { [_cache clearFetchedImageFromCacheWithURL:[_dataSource multiplexImageNode:self URLForImageIdentifier:self.loadedImageIdentifier]]; } From 070982a50ba5b1f6ab91fc633a122afee2a58efa Mon Sep 17 00:00:00 2001 From: rcancro Date: Wed, 17 Feb 2016 13:08:16 -0800 Subject: [PATCH 05/16] fixed tests??? (it did locally anyway) --- AsyncDisplayKit/ASTextNode.mm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index a6c17e5bf0..ca45d23330 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -258,9 +258,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // expensive, and can take some time, so we dispatch onto a bg queue to // actually dealloc. __block ASTextKitRenderer *renderer = _renderer; + // before we remove the renderer, take its scale factor so we set it when a new renderer is created + _currentScaleFactor = renderer.currentScaleFactor; + ASPerformBlockOnBackgroundThread(^{ - // before we remove the renderer, take its scale factor so we set it when a new renderer is created - _currentScaleFactor = renderer.currentScaleFactor; renderer = nil; }); _renderer = nil; From 4a37b0882e308b2af68cb77f2a93387264ebc990 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Wed, 17 Feb 2016 22:21:46 -0800 Subject: [PATCH 06/16] [ASCollectionView] iOS 7-only issue where willDisplayCell: is not called, fixed for only that OS version. This includes some method moves to make sure related methods are in closer proximitiy. --- AsyncDisplayKit/ASCollectionView.mm | 98 +++++++++++--------- AsyncDisplayKit/Private/ASInternalHelpers.h | 2 + AsyncDisplayKit/Private/ASInternalHelpers.mm | 10 ++ 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index b9f58ee5ff..088f8bac02 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -481,14 +481,15 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; #pragma mark - #pragma mark Intercepted selectors. -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - _ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:kCellReuseIdentifier forIndexPath:indexPath]; - - ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; - cell.node = node; - [_rangeController configureContentView:cell.contentView forCellNode:node]; - return cell; + _superIsPendingDataLoad = NO; + return [_dataController numberOfSections]; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return [_dataController numberOfRowsInSection:section]; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath @@ -505,17 +506,59 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return view; } -- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView + + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - _superIsPendingDataLoad = NO; - return [_dataController numberOfSections]; + _ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:kCellReuseIdentifier forIndexPath:indexPath]; + + ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; + cell.node = node; + [_rangeController configureContentView:cell.contentView forCellNode:node]; + + if (ASRunningOnOS7()) { + // Even though UICV was introduced in iOS 6, and UITableView has always had the equivalent method, + // -willDisplayCell: was not introduced until iOS 8 for UICV. didEndDisplayingCell, however, is available. + [self collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; + } + + return cell; } -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { - return [_dataController numberOfRowsInSection:section]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; + + if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) { + [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; + } + + ASCellNode *cellNode = [self nodeForItemAtIndexPath:indexPath]; + if (cellNode.neverShowPlaceholders) { + [cellNode recursivelyEnsureDisplaySynchronously:YES]; + } } +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNode:forItemAtIndexPath:)]) { + ASCellNode *node = ((_ASCollectionViewCell *)cell).node; + ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil."); + [_asyncDelegate collectionView:self didEndDisplayingNode:node forItemAtIndexPath:indexPath]; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNodeForItemAtIndexPath:)]) { + [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath]; + } +#pragma clang diagnostic pop +} + +#pragma mark - +#pragma mark Scroll Direction. + - (ASScrollDirection)scrollDirection { CGPoint scrollVelocity; @@ -577,37 +620,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return scrollableDirection; } -- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath -{ - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; - - if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) { - [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; - } - - ASCellNode *cellNode = [self nodeForItemAtIndexPath:indexPath]; - if (cellNode.neverShowPlaceholders) { - [cellNode recursivelyEnsureDisplaySynchronously:YES]; - } -} - -- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath -{ - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; - - if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNode:forItemAtIndexPath:)]) { - ASCellNode *node = ((_ASCollectionViewCell *)cell).node; - ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil."); - [_asyncDelegate collectionView:self didEndDisplayingNode:node forItemAtIndexPath:indexPath]; - } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNodeForItemAtIndexPath:)]) { - [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath]; - } -#pragma clang diagnostic pop -} - - (void)layoutSubviews { if (_zeroContentInsets) { diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 1f173bd951..11709e8373 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -28,6 +28,8 @@ CGFloat ASCeilPixelValue(CGFloat f); CGFloat ASRoundPixelValue(CGFloat f); +BOOL ASRunningOnOS7(); + ASDISPLAYNODE_EXTERN_C_END /** diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.mm b/AsyncDisplayKit/Private/ASInternalHelpers.mm index 557d52119c..51e8d2685b 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.mm +++ b/AsyncDisplayKit/Private/ASInternalHelpers.mm @@ -93,6 +93,16 @@ CGFloat ASRoundPixelValue(CGFloat f) return roundf(f * ASScreenScale()) / ASScreenScale(); } +BOOL ASRunningOnOS7() +{ + static BOOL isOS7 = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + isOS7 = ([[UIDevice currentDevice].systemVersion floatValue] < 8.0); + }); + return isOS7; +} + @implementation NSIndexPath (ASInverseComparison) - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath From 0cf972d7ac049b063cb9464d67865e123e914363 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Wed, 17 Feb 2016 22:28:12 -0800 Subject: [PATCH 07/16] [ASVideoNode] Remove beta warning in preparation for 1.9.7 launch. --- AsyncDisplayKit/ASVideoNode.h | 6 +++--- AsyncDisplayKit/ASVideoNode.mm | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index 515dc20f4b..ebc9957c0c 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -10,9 +10,9 @@ @protocol ASVideoNodeDelegate; -// If you need ASVideoNode, please use AsyncDisplayKit master until this comment is removed. -// As of 1.9.6, ASVideoNode accidentally triggers creating the AVPlayerLayer even before playing -// the video. Using a lot of them intended to show static frame placeholders will be slow. +// 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 : ASControlNode @property (atomic, strong, readwrite) AVAsset *asset; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index e2efb5a80e..9df53ab874 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -46,11 +46,7 @@ if (!(self = [super init])) { return nil; } - -#if DEBUG - NSLog(@"*** Warning: ASVideoNode is a new component - the 1.9.6 version may cause performance hiccups."); -#endif - + _previewQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); self.playButton = [[ASDefaultPlayButton alloc] init]; From 19cc368d159992408a2a20fb7326924480292be9 Mon Sep 17 00:00:00 2001 From: Rahul Malik Date: Wed, 17 Feb 2016 13:19:00 -0800 Subject: [PATCH 08/16] In addition to allocating nodes in the background, perform that operation concurrently in ASDataController --- AsyncDisplayKit/ASCollectionView.mm | 4 ++- AsyncDisplayKit/ASTableView.mm | 6 +++-- AsyncDisplayKit/Details/ASDataController.mm | 27 ++++++++++++++++----- AsyncDisplayKitTests/ASDisplayNodeTests.m | 1 + 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index b9f58ee5ff..3d80ea8107 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -643,7 +643,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; scrollView.contentOffset.y - ((targetContentOffset != NULL) ? targetContentOffset->y : 0) ); - [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + if (targetContentOffset != NULL) { + [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + } if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { [_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 477c6ff4a3..99009d8eb9 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -637,8 +637,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; scrollView.contentOffset.x - ((targetContentOffset != NULL) ? targetContentOffset->x : 0), scrollView.contentOffset.y - ((targetContentOffset != NULL) ? targetContentOffset->y : 0) ); - - [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + + if (targetContentOffset != NULL) { + [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + } if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { [_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index a792eedc6d..32d62adac2 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -162,24 +162,37 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } NSUInteger nodeCount = nodes.count; - NSMutableArray *allocatedNodes = [NSMutableArray arrayWithCapacity:nodeCount]; + NSMutableArray *allocatedNodes = [NSMutableArray arrayWithCapacity:nodeCount]; dispatch_group_t layoutGroup = dispatch_group_create(); ASSizeRange *nodeBoundSizes = (ASSizeRange *)malloc(sizeof(ASSizeRange) * nodeCount); + for (NSUInteger j = 0; j < nodes.count && j < indexPaths.count; j += kASDataControllerSizingCountPerProcessor) { NSInteger batchCount = MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j); + __block NSArray *subarray; + // Allocate nodes concurrently. dispatch_block_t allocationBlock = ^{ - for (NSUInteger k = j; k < j + batchCount; k++) { + __strong ASCellNode **allocatedNodeBuffer = (__strong ASCellNode **)calloc(batchCount, sizeof(ASCellNode *)); + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_apply(batchCount, queue, ^(size_t i) { + unsigned long k = j + i; ASCellNodeBlock cellBlock = nodes[k]; ASCellNode *node = cellBlock(); ASDisplayNodeAssertNotNil(node, @"Node block created nil node"); - [allocatedNodes addObject:node]; + allocatedNodeBuffer[i] = node; if (!node.isNodeLoaded) { nodeBoundSizes[k] = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPaths[k]]; } + }); + subarray = [[NSArray alloc] initWithObjects:allocatedNodeBuffer count:batchCount]; + + // Nil out buffer indexes to allow arc to free the stored cells. + for (int i = 0; i < batchCount; i++) { + allocatedNodeBuffer[i] = nil; } + free(allocatedNodeBuffer); }; - + if (ASDisplayNodeThreadIsMain()) { dispatch_semaphore_t sema = dispatch_semaphore_create(0); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -187,14 +200,16 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; dispatch_semaphore_signal(sema); }); dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - [self layoutLoadedNodes:[allocatedNodes subarrayWithRange:NSMakeRange(j, batchCount)] ofKind:kind atIndexPaths:[indexPaths subarrayWithRange:NSMakeRange(j, batchCount)]]; + [self layoutLoadedNodes:subarray ofKind:kind atIndexPaths:[indexPaths subarrayWithRange:NSMakeRange(j, batchCount)]]; } else { allocationBlock(); [_mainSerialQueue performBlockOnMainThread:^{ - [self layoutLoadedNodes:[allocatedNodes subarrayWithRange:NSMakeRange(j, batchCount)] ofKind:kind atIndexPaths:[indexPaths subarrayWithRange:NSMakeRange(j, batchCount)]]; + [self layoutLoadedNodes:subarray ofKind:kind atIndexPaths:[indexPaths subarrayWithRange:NSMakeRange(j, batchCount)]]; }]; } + [allocatedNodes addObjectsFromArray:subarray]; + dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (NSUInteger k = j; k < j + batchCount; k++) { ASCellNode *node = allocatedNodes[k]; diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 9f34114231..455591cd1b 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -166,6 +166,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ } - (BOOL)resignFirstResponder { + [super resignFirstResponder]; if (self.isFirstResponder) { self.isFirstResponder = NO; return YES; From 76708c47cf921603610bc937dfe59ea59e5659f3 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 18 Feb 2016 09:54:48 -0800 Subject: [PATCH 09/16] [ASCollectionView] Always honor sectionInset in both dimensions --- AsyncDisplayKit/ASCollectionView.mm | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 2a807a530c..b69e685ed7 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -758,19 +758,15 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; if (_asyncDelegateImplementsInsetSection) { sectionInset = [(id)_asyncDelegate collectionView:self layout:self.collectionViewLayout insetForSectionAtIndex:indexPath.section]; } - - if (ASScrollDirectionContainsHorizontalDirection([self scrollableDirections])) { - constrainedSize.min.width = MAX(0, constrainedSize.min.width - sectionInset.left - sectionInset.right); - //ignore insets for FLT_MAX so FLT_MAX can be compared against - if (constrainedSize.max.width - FLT_EPSILON < FLT_MAX) { - constrainedSize.max.width = MAX(0, constrainedSize.max.width - sectionInset.left - sectionInset.right); - } - } else { - constrainedSize.min.height = MAX(0, constrainedSize.min.height - sectionInset.top - sectionInset.bottom); - //ignore insets for FLT_MAX so FLT_MAX can be compared against - if (constrainedSize.max.height - FLT_EPSILON < FLT_MAX) { - constrainedSize.max.height = MAX(0, constrainedSize.max.height - sectionInset.top - sectionInset.bottom); - } + + constrainedSize.min.height = MAX(0, constrainedSize.min.height - sectionInset.top - sectionInset.bottom); + constrainedSize.min.width = MAX(0, constrainedSize.min.width - sectionInset.left - sectionInset.right); + //ignore insets for FLT_MAX so FLT_MAX can be compared against + if (constrainedSize.max.width - FLT_EPSILON < FLT_MAX) { + constrainedSize.max.width = MAX(0, constrainedSize.max.width - sectionInset.left - sectionInset.right); + } + if (constrainedSize.max.height - FLT_EPSILON < FLT_MAX) { + constrainedSize.max.height = MAX(0, constrainedSize.max.height - sectionInset.top - sectionInset.bottom); } return constrainedSize; From eeb81a54de62aaea917b9a17e419f1338c593c41 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Thu, 18 Feb 2016 13:59:20 -0800 Subject: [PATCH 10/16] [ASDisplayNode+Beta] Expose helper functions, ASPerformBlockOnMainThread & ASPerformBlockOnBackgroundThread. --- AsyncDisplayKit/ASDisplayNode+Beta.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index bad81b740a..aa7df08583 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -8,6 +8,11 @@ #import "ASContextTransitioning.h" +ASDISPLAYNODE_EXTERN_C_BEGIN +void ASPerformBlockOnMainThread(void (^block)()); +void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT +ASDISPLAYNODE_EXTERN_C_END + @interface ASDisplayNode (Beta) + (BOOL)usesImplicitHierarchyManagement; From 40791dd8591fb63a300bffba1e9d27d6cb17a580 Mon Sep 17 00:00:00 2001 From: Luke Zhao Date: Wed, 17 Feb 2016 14:52:33 -0800 Subject: [PATCH 11/16] optimize reload data, reload sections, & move nodes performance & logic --- AsyncDisplayKit.xcodeproj/project.pbxproj | 2 +- AsyncDisplayKit/ASCollectionView.mm | 105 ++++- AsyncDisplayKit/ASTableView.mm | 83 +++- .../Details/ASChangeSetDataController.m | 4 +- .../Details/ASCollectionDataController.mm | 81 ++-- .../Details/ASDataController+Subclasses.h | 39 +- AsyncDisplayKit/Details/ASDataController.h | 43 +- AsyncDisplayKit/Details/ASDataController.mm | 386 +++++++----------- AsyncDisplayKit/Details/ASRangeController.h | 53 +++ AsyncDisplayKit/Details/ASRangeController.mm | 28 +- AsyncDisplayKitTests/ASTableViewTests.m | 2 +- 11 files changed, 479 insertions(+), 347 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 285d1b7ad2..a332403bb3 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -646,7 +646,7 @@ 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGRect+ASConvenience.h"; sourceTree = ""; }; 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGRect+ASConvenience.m"; sourceTree = ""; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderTests.m; sourceTree = ""; }; - 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionDataController.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionDataController.h; sourceTree = ""; }; 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionDataController.mm; sourceTree = ""; }; 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewFlowLayoutInspector.h; sourceTree = ""; }; 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index b9f58ee5ff..d57bf44ddf 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -249,7 +249,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _superIsPendingDataLoad = YES; [super reloadData]; }); - [_dataController reloadDataWithAnimationOptions:kASCollectionViewAnimationNone completion:completion]; + [_dataController reloadDataWithCompletion:completion]; } - (void)reloadData @@ -261,7 +261,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASDisplayNodeAssertMainThread(); _superIsPendingDataLoad = YES; - [_dataController reloadDataImmediatelyWithAnimationOptions:kASCollectionViewAnimationNone]; + [_dataController reloadDataImmediately]; [super reloadData]; } @@ -446,7 +446,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); - [_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone]; + [_dataController moveSection:section toSection:newSection]; } - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths @@ -470,7 +470,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); - [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } - (NSString *)__reuseIdentifierForKind:(NSString *)kind @@ -941,6 +941,46 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super reloadItemsAtIndexPaths:indexPaths]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; + [UIView performWithoutAnimation:^{ + [super reloadItemsAtIndexPaths:indexPaths]; + }]; + } +} + +- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[fromIndexPath] batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[fromIndexPath] batched:NO]; + [UIView performWithoutAnimation:^{ + [super moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; + }]; + } +} + - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -961,6 +1001,26 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super reloadSections:indexSet]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; + [UIView performWithoutAnimation:^{ + [super reloadSections:indexSet]; + }]; + } +} + - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -981,6 +1041,43 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:[NSIndexSet indexSetWithIndex:fromIndex] batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super moveSection:fromIndex toSection:toIndex]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:[NSIndexSet indexSetWithIndex:fromIndex] batched:NO]; + [UIView performWithoutAnimation:^{ + [super moveSection:fromIndex toSection:toIndex]; + }]; + } +} + +- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_batchUpdateBlocks addObject:^{ + [super reloadData]; + }]; + } else { + [UIView performWithoutAnimation:^{ + [super reloadData]; + }]; + } +} + #pragma mark - ASCellNodeDelegate - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 477c6ff4a3..d91d0241f0 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -303,10 +303,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)reloadDataWithCompletion:(void (^)())completion { - ASPerformBlockOnMainThread(^{ - [super reloadData]; - }); - [_dataController reloadDataWithAnimationOptions:UITableViewRowAnimationNone completion:completion]; + [_dataController reloadDataWithCompletion:completion]; } - (void)reloadData @@ -317,8 +314,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)reloadDataImmediately { ASDisplayNodeAssertMainThread(); - [_dataController reloadDataImmediatelyWithAnimationOptions:UITableViewRowAnimationNone]; - [super reloadData]; + [_dataController reloadDataImmediately]; } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -433,7 +429,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); - [_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone]; + [_dataController moveSection:section toSection:newSection]; } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation @@ -457,7 +453,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); - [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } #pragma mark - @@ -834,6 +830,36 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView reloadRows:%ld rows", indexPaths.count); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + [super reloadRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; + }); + + if (_automaticallyAdjustsContentOffset) { + [self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:YES]; + } +} + +- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath +{ + ASDisplayNodeAssertMainThread(); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + [self moveRowAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; +} + - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -850,6 +876,36 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); } +- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView reloadSections:%@", indexSet); + + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + [super reloadSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; + }); +} + +- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex +{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView moveSection:%@", indexSet); + + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + [super moveSection:fromIndex toSection:toIndex]; +} + + - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -865,6 +921,17 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); } +- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView reloadData"); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + [super reloadData]; +} + #pragma mark - ASDataControllerDelegate - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath { diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index 666ea86ce7..bdc0c4993c 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -131,7 +131,7 @@ [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; } else { - [super moveSection:section toSection:newSection withAnimationOptions:animationOptions]; + [super moveSection:section toSection:newSection]; } } @@ -174,7 +174,7 @@ [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; } else { - [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:animationOptions]; + [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } } diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm index 2408a57e1f..979acd30e3 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.mm +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -49,37 +49,27 @@ [self _populateSupplementaryNodesOfKind:kind withMutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; - - // Measure loaded nodes before leaving the main thread - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } - (void)willReloadData { [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { - // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind]; - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - - NSArray *editingNodes = [self editingNodesOfKind:kind]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; - [self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil]; - - // Insert each section + // Insert sections NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { [sections addObject:[NSMutableArray array]]; } - [self insertSections:sections ofKind:kind atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] completion:nil]; - - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + self.editingNode[kind] = sections; + + [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:kind withCompletion:nil]; }]; - [_pendingNodes removeObjectForKey:kind]; - [_pendingIndexPaths removeObjectForKey:kind]; }]; + + [_pendingNodes removeAllObjects]; + [_pendingIndexPaths removeAllObjects]; } - (void)prepareForInsertSections:(NSIndexSet *)sections @@ -91,9 +81,6 @@ [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; - - // Measure loaded nodes before leaving the main thread - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } @@ -104,23 +91,22 @@ for (NSUInteger i = 0; i < sections.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } - - [self insertSections:sectionArray ofKind:kind atIndexSet:sections completion:nil]; - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + + [self insertSections:sectionArray ofKind:kind atIndexSet:sections]; + [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:kind withCompletion:nil]; }]; - [_pendingNodes removeObjectForKey:kind]; - [_pendingIndexPaths removeObjectForKey:kind]; }]; + + [_pendingNodes removeAllObjects]; + [_pendingIndexPaths removeAllObjects]; } - (void)willDeleteSections:(NSIndexSet *)sections { for (NSString *kind in [self supplementaryKinds]) { - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); - - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - [self deleteSectionsOfKind:kind atIndexSet:sections completion:nil]; + [self deleteSectionsOfKind:kind atIndexSet:sections]; + [self commitChangesToNodesOfKind:kind withCompletion:nil]; } } @@ -132,40 +118,31 @@ [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; - - // Measure loaded nodes before leaving the main thread - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } - (void)willReloadSections:(NSIndexSet *)sections { [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - // reinsert the elements - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + // clear sections + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + self.editingNode[kind][idx] = [[NSMutableArray alloc] init]; + }]; + // reinsert the elements + [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:kind withCompletion:nil]; }]; - [_pendingNodes removeObjectForKey:kind]; - [_pendingIndexPaths removeObjectForKey:kind]; }]; + + [_pendingNodes removeAllObjects]; + [_pendingIndexPaths removeAllObjects]; } - (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection { for (NSString *kind in [self supplementaryKinds]) { - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths([self editingNodesOfKind:kind], indexPaths); - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - - // update the section of indexpaths - NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; - }]; - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + [self moveSection:section ofKind:kind toSection:newSection]; + [self commitChangesToNodesOfKind:kind withCompletion:nil]; } } diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h index 32d787910e..54f1fc259a 100644 --- a/AsyncDisplayKit/Details/ASDataController+Subclasses.h +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -14,16 +14,7 @@ @interface ASDataController (Subclasses) #pragma mark - Internal editing & completed store querying - -/** - * Provides a collection of index paths for nodes of the given kind that are currently in the editing store - */ -- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind; - -/** - * Read-only access to the underlying editing nodes of the given kind - */ -- (NSMutableArray *)editingNodesOfKind:(NSString *)kind; +@property (nonatomic, strong, readonly) NSMutableDictionary *editingNode; /** * Read only access to the underlying completed nodes of the given kind @@ -35,7 +26,7 @@ /** * Measure and layout the given nodes in optimized batches, constraining each to a given size in `constrainedSizeForNodeOfKind:atIndexPath:`. */ -- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (void)layoutAndInsertFromNodeBlocks:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; /* * Perform measurement and layout of loaded nodes on the main thread, skipping unloaded nodes. @@ -53,24 +44,34 @@ #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. + * Inserts the given nodes of the specified kind into the backing store. */ -- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; /** - * Deletes the given nodes of the specified kind in the backing store, calling completion on the main thread when the deletion finishes. + * Deletes the given nodes of the specified kind in the backing store. */ -- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (NSArray *)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; /** - * Inserts the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + * Inserts the given sections of the specified kind in the backing store. */ -- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock; +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet; /** - * Deletes the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + * Deletes the given sections of the specified kind in the backing store. */ -- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock; +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet; + +/** + * Moves the given section of the specified kind in the backing store. + */ +- (void)moveSection:(NSInteger)section ofKind:(NSString *)kind toSection:(NSInteger)newSection; + +/** + * Commit the change for insert/delete node or sections to the backing store, calling completion on the main thread when finished. + */ +- (void)commitChangesToNodesOfKind:(NSString *)kind withCompletion:(void (^)())completionBlock; #pragma mark - Data Manipulation Hooks diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 6648275d9b..1766b7dfc0 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -91,16 +91,41 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + Called for reload of elements. + */ +- (void)dataController:(ASDataController *)dataController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + Called for movement of elements. + */ +- (void)dataController:(ASDataController *)dataController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; + /** Called for insertion of sections. */ -- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *> *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for deletion of sections. */ - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + Called for reload of sections. + */ +- (void)dataController:(ASDataController *)dataController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + Called for movement of sections. + */ +- (void)dataController:(ASDataController *)dataController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex; + +/** + Called for reload data. + */ +- (void)dataControllerDidReloadData:(ASDataController *)dataController; + @end /** @@ -137,14 +162,6 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled; -/** @name Initial loading - * - * @discussion This method allows choosing an animation style for the first load of content. It is typically used just once, - * for example in viewWillAppear:, to specify an animation option for the information already present in the asyncDataSource. - */ - -- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - /** @name Data Updating */ - (void)beginUpdates; @@ -159,7 +176,7 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -175,11 +192,11 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (void)relayoutAllNodes; -- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; -- (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^ _Nullable)())completion; +- (void)reloadDataWithCompletion:(void (^)())completion; -- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)reloadDataImmediately; /** @name Data Querying */ diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index a792eedc6d..e9c93e907b 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -30,7 +30,6 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { - 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 propogated to _completedNodes. @@ -42,9 +41,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; BOOL _asyncDataFetchingEnabled; BOOL _delegateDidInsertNodes; + BOOL _delegateDidReloadNodes; BOOL _delegateDidDeleteNodes; + BOOL _delegateDidMoveNode; BOOL _delegateDidInsertSections; BOOL _delegateDidDeleteSections; + BOOL _delegateDidReloadSections; + BOOL _delegateDidMoveSection; + BOOL _delegateDidReloadData; } @property (atomic, assign) NSUInteger batchUpdateCounter; @@ -92,8 +96,13 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // 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:)]; + _delegateDidReloadNodes = [_delegate respondsToSelector:@selector(dataController:didReloadNodes:atIndexPaths:withAnimationOptions:)]; + _delegateDidMoveNode = [_delegate respondsToSelector:@selector(dataController:didMoveNodeAtIndexPath:toIndexPath:)]; + _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSectionsAtIndexSet:withAnimationOptions:)]; + _delegateDidReloadSections = [_delegate respondsToSelector:@selector(dataController:didReloadSectionsAtIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; + _delegateDidMoveSection = [_delegate respondsToSelector:@selector(dataController:didMoveSection:toSection:)]; + _delegateDidReloadData = [_delegate respondsToSelector:@selector(dataControllerDidReloadData:)]; } + (NSUInteger)parallelProcessorCount @@ -110,17 +119,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Cell Layout -- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (void)layoutAndInsertFromNodeBlocks:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { - NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; - - // Processing in batches - for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { - NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); - NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; - NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; - [self _layoutNodes:batchedNodes ofKind:kind atIndexPaths:batchedIndexPaths completion:completionBlock]; - } + [self _layoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + }]; } - (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { @@ -144,21 +150,13 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); } -/** - * Measures and defines the layout for each node in optimized batches on an editing queue, inserting the results into the backing store. - */ -- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - [self batchLayoutNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - // Insert finished nodes into data storage - [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; -} - - (void)_layoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (!nodes.count) { - return; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + return; } NSUInteger nodeCount = nodes.count; @@ -223,178 +221,84 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - External Data Querying + Editing -- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { if (indexPaths.count == 0) return; + LOG(@"insertNodes:%@ ofKind:%@", nodes, kind); NSMutableArray *editingNodes = _editingNodes[kind]; ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); _editingNodes[kind] = editingNodes; - - // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. - NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes); - - [_mainSerialQueue performBlockOnMainThread:^{ - _completedNodes[kind] = completedNodes; - if (completionBlock) { - completionBlock(nodes, indexPaths); - } - }]; } -- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (NSArray *)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { if (indexPaths.count == 0) { - return; + return @[]; } - LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@, full index paths in _editingNodes = %@", indexPaths, kind, ASIndexPathsForMultidimensionalArray(_editingNodes[kind])); + LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@,", indexPaths, kind); NSMutableArray *editingNodes = _editingNodes[kind]; + NSArray *deletedNodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[kind], indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); _editingNodes[kind] = editingNodes; - - [_mainSerialQueue performBlockOnMainThread:^{ - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); - if (completionBlock) { - completionBlock(nodes, indexPaths); - } - }]; + return deletedNodes; } -- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock -{ +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet{ if (indexSet.count == 0) return; + LOG(@"insertSections:%@ ofKind:%@", sections, kind); 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 = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); - - [_mainSerialQueue performBlockOnMainThread:^{ - [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; - if (completionBlock) { - completionBlock(sections, indexSet); - } - }]; } -- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet { if (indexSet.count == 0) return; + + LOG(@"deleteSectionsOfKind:%@", kind); [_editingNodes[kind] removeObjectsAtIndexes:indexSet]; +} + +- (void)moveSection:(NSInteger)section ofKind:(NSString *)kind toSection:(NSInteger)newSection +{ + NSArray *movedSection = _editingNodes[kind][section]; + [_editingNodes[kind] removeObjectAtIndex:section]; + [_editingNodes[kind] insertObject:movedSection atIndex:newSection]; +} + +- (void)commitChangesToNodesOfKind:(NSString *)kind withCompletion:(void (^)())completionBlock +{ + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes[kind]); + [_mainSerialQueue performBlockOnMainThread:^{ - [_completedNodes[kind] removeObjectsAtIndexes:indexSet]; + _completedNodes[kind] = completedNodes; if (completionBlock) { - completionBlock(indexSet); + completionBlock(); } }]; } -#pragma mark - Internal Data Querying + Editing +#pragma mark - Reload (External API) -/** - * 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 +- (void)reloadDataWithCompletion:(void (^)())completion { - [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; + [self _reloadDataSynchronously:NO completion:completion]; } -/** - * 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 +- (void)reloadDataImmediately { - [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; + [self _reloadDataSynchronously:YES completion:nil]; } -/** - * Inserts sections, represented as arrays, into the backing store at the given indicies 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 -{ - [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; - }]; -} - -/** - * Removes sections at the given indicies 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 -{ - [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) { - if (_delegateDidDeleteSections) - [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - }]; -} - -#pragma mark - Initial Load & Full Reload (External API) - -- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - [self accessDataSourceWithBlock:^{ - NSMutableArray *indexPaths = [NSMutableArray array]; - NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; - - // insert sections - [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; - - for (NSUInteger i = 0; i < sectionNum; i++) { - NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; - - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; - for (NSUInteger j = 0; j < rowNum; j++) { - [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; - } - } - - // insert elements - [self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; - }]; -} - -- (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 +- (void)_reloadDataSynchronously:(BOOL)synchronously completion:(void (^)())completion { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -402,39 +306,35 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self accessDataSourceSynchronously:synchronously withBlock:^{ NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; - NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromEntireDataSourceWithMutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; // Allow subclasses to perform setup before going into the edit transaction [self prepareForReloadData]; void (^transactionBlock)() = ^{ LOG(@"Edit Transaction - reloadData"); - - // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind]); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - - NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; - [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - + [self willReloadData]; - - // Insert each section + + // Insert sections NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { [sections addObject:[[NSMutableArray alloc] init]]; } - - [self _insertSections:sections atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] withAnimationOptions:animationOptions]; + _editingNodes[ASDataControllerRowNodeKind] = sections; - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; - - if (completion) { - dispatch_async(dispatch_get_main_queue(), completion); - } + [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidReloadData) { + [_delegate dataControllerDidReloadData:self]; + } + if (completion) { + completion(); + } + }]; + }]; }; if (synchronously) { @@ -533,15 +433,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; if (_batchUpdateCounter == 0) { LOG(@"endUpdatesWithCompletion - beginning"); - [_editingTransactionQueue addOperationWithBlock:^{ - [_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 = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); - - LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); - [_delegate dataControllerBeginUpdates:self]; - }]; + [_mainSerialQueue performBlockOnMainThread:^{ + [_delegate dataControllerBeginUpdates:self]; }]; // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. @@ -552,15 +445,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; block(); }]; [_pendingEditCommandBlocks removeAllObjects]; - - [_editingTransactionQueue addOperationWithBlock:^{ - [_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]; - }]; + + [_mainSerialQueue performBlockOnMainThread:^{ + [_delegate dataController:self endUpdatesAnimated:animated completion:completion]; }]; } } @@ -592,9 +479,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; [self prepareForInsertSections:sections]; @@ -607,8 +494,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [sectionArray addObject:[NSMutableArray array]]; } - [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + [self insertSections:sectionArray ofKind:ASDataControllerRowNodeKind atIndexSet:sections]; + + [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; @@ -626,10 +519,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // remove elements LOG(@"Edit Transaction - deleteSections: %@", sections); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + + [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:sections]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidDeleteSections) + [_delegate dataController:self didDeleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }]; }]; }]; } @@ -643,29 +538,32 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; [self prepareForReloadSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ [self willReloadSections:sections]; - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + // clear sections + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + _editingNodes[ASDataControllerRowNodeKind][idx] = [[NSMutableArray alloc] init]; + }]; - // reinsert the elements - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidReloadSections) + [_delegate dataController:self didReloadSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; } -- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -675,24 +573,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ [self willMoveSection:section toSection:newSection]; - - // remove elements LOG(@"Edit Transaction - moveSection"); - - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - - // update the section of indexpaths - NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - for (NSIndexPath *indexPath in indexPaths) { - [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; - } - - // Don't re-calculate size for moving - [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + [self moveSection:section ofKind:ASDataControllerRowNodeKind toSection:newSection]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidMoveSection) { + [_delegate dataController:self didMoveSection:section toSection:newSection]; + } + }]; }]; }]; } @@ -760,7 +648,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertRows: %@", indexPaths); - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self layoutAndInsertFromNodeBlocks:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; @@ -780,7 +673,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - deleteRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; + NSArray *deletedNodes = [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:sortedIndexPaths]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodes:deletedNodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; }]; }]; } @@ -795,20 +692,25 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. [self accessDataSourceWithBlock:^{ - NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + NSMutableArray *nodeBlocks = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; // FIXME: This doesn't currently do anything // FIXME: Shouldn't deletes be sorted in descending order? [indexPaths sortedArrayUsingSelector:@selector(compare:)]; for (NSIndexPath *indexPath in indexPaths) { - [nodes addObject:[_dataSource dataController:self nodeBlockAtIndexPath:indexPath]]; + [nodeBlocks addObject:[_dataSource dataController:self nodeBlockAtIndexPath:indexPath]]; } [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; + [self layoutAndInsertFromNodeBlocks:nodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidReloadNodes) + [_delegate dataController:self didReloadNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; @@ -854,7 +756,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } -- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -864,26 +766,23 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], [NSArray arrayWithObject:indexPath]); - NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:@[indexPath]]; // Don't re-calculate size for moving - NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath]; - [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:@[newIndexPath]]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidMoveNode) { + [_delegate dataController:self didMoveNodeAtIndexPath:indexPath toIndexPath:newIndexPath]; + } + }]; }]; }]; } #pragma mark - Data Querying (Subclass API) -- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind -{ - return _editingNodes[kind] != nil ? ASIndexPathsForMultidimensionalArray(_editingNodes[kind]) : nil; -} - -- (NSMutableArray *)editingNodesOfKind:(NSString *)kind -{ - return _editingNodes[kind] != nil ? _editingNodes[kind] : [NSMutableArray array]; +- (NSMutableDictionary *)editingNode{ + return _editingNodes; } - (NSMutableArray *)completedNodesOfKind:(NSString *)kind @@ -950,11 +849,10 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return ASFindElementsInMultidimensionalArrayAtIndexPaths((NSMutableArray *)[self completedNodes], [indexPaths sortedArrayUsingSelector:@selector(compare:)]); } -/// Returns nodes that can be queried externally. _externalCompletedNodes is used if available, _completedNodes otherwise. - (NSArray *)completedNodes { ASDisplayNodeAssertMainThread(); - return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes[ASDataControllerRowNodeKind]; + return _completedNodes[ASDataControllerRowNodeKind]; } #pragma mark - Dealloc diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index d5288f40e2..c3e6f31551 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -163,6 +163,30 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Called for nodes reload. + * + * @param rangeController Sender. + * + * @param nodes Inserted nodes. + * + * @param indexPaths Index path of reloaded nodes. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. + */ +- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + * Called for movement of node. + * + * @param rangeController Sender. + * + * @param fromIndexPath Index path of moved node before the movement. + * + * @param toIndexPath Index path of moved node after the movement. + */ +- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; + /** * Called for section insertion. * @@ -174,6 +198,17 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Called for section reload. + * + * @param rangeController Sender. + * + * @param indexSet Index set of reloaded sections. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. + */ +- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + /** * Called for section deletion. * @@ -185,6 +220,24 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Called for movement of section. + * + * @param rangeController Sender. + * + * @param fromIndex Index of moved section before the movement. + * + * @param toIndex Index of moved section after the movement. + */ +- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex; + +/** + * Called for reload data. + * + * @param rangeController Sender. + */ +- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController; + @end NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 1b8d7d8f89..c13ef1cdf6 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -361,15 +361,30 @@ }); } -- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)dataController:(ASDataController *)dataController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions{ + ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); + ASPerformBlockOnMainThread(^{ + _rangeIsValid = NO; + [_delegate rangeController:self didReloadNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }); +} + +- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections"); ASPerformBlockOnMainThread(^{ _rangeIsValid = NO; [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); } +- (void)dataController:(ASDataController *)dataController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASPerformBlockOnMainThread(^{ + _rangeIsValid = NO; + [_delegate rangeController:self didReloadSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + }); +} + - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASPerformBlockOnMainThread(^{ @@ -378,4 +393,11 @@ }); } -@end \ No newline at end of file +- (void)dataControllerDidReloadData:(ASDataController *)dataController{ + ASPerformBlockOnMainThread(^{ + _rangeIsValid = NO; + [_delegate rangeControllerDidReloadData:self]; + }); +} + +@end diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index 2949a16e2f..9de6b2d11f 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -15,7 +15,7 @@ #define NumberOfSections 10 #define NumberOfRowsPerSection 20 -#define NumberOfReloadIterations 50 +#define NumberOfReloadIterations 500 @interface ASTestDataController : ASChangeSetDataController @property (atomic) int numberOfAllNodesRelayouts; From cefc985171c982e9fed29adad90e8545c01c74f8 Mon Sep 17 00:00:00 2001 From: Luke Zhao Date: Thu, 18 Feb 2016 16:28:03 -0800 Subject: [PATCH 12/16] reset tests --- AsyncDisplayKitTests/ASTableViewTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index 9de6b2d11f..2949a16e2f 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -15,7 +15,7 @@ #define NumberOfSections 10 #define NumberOfRowsPerSection 20 -#define NumberOfReloadIterations 500 +#define NumberOfReloadIterations 50 @interface ASTestDataController : ASChangeSetDataController @property (atomic) int numberOfAllNodesRelayouts; From 2adc30440f0da642460e11c231297b8c5dc001e7 Mon Sep 17 00:00:00 2001 From: rcancro Date: Thu, 18 Feb 2016 16:41:58 -0800 Subject: [PATCH 13/16] Exposing currentScaleFactor --- AsyncDisplayKit/ASTextNode+Beta.h | 5 +++++ AsyncDisplayKit/ASTextNode.mm | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode+Beta.h b/AsyncDisplayKit/ASTextNode+Beta.h index 38059aa7c3..b4d3b88982 100644 --- a/AsyncDisplayKit/ASTextNode+Beta.h +++ b/AsyncDisplayKit/ASTextNode+Beta.h @@ -15,4 +15,9 @@ */ @property (nonatomic, copy) NSArray *pointSizeScaleFactors; +/** + @abstract The currently applied scale factor, or 0 if the text node is not being scaled. + */ +@property (nonatomic, assign, readonly) CGFloat currentScaleFactor; + @end \ No newline at end of file diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index ca45d23330..202a5bb2ed 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -79,7 +79,6 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation CGSize _constrainedSize; ASTextKitRenderer *_renderer; - CGFloat _currentScaleFactor; UILongPressGestureRecognizer *_longPressGestureRecognizer; } @@ -245,7 +244,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; .maximumNumberOfLines = _maximumNumberOfLines, .exclusionPaths = _exclusionPaths, .pointSizeScaleFactors = _pointSizeScaleFactors, - .currentScaleFactor = _currentScaleFactor, + .currentScaleFactor = self.currentScaleFactor, }; } From 32aa333c21bea6eaf8c0ac55c7f12d8b1d598a58 Mon Sep 17 00:00:00 2001 From: appleguy Date: Thu, 18 Feb 2016 19:50:06 -0800 Subject: [PATCH 14/16] Revert "[ASCollectionView / ASTableView] Optimize reloadData and reloadSection: methods." --- AsyncDisplayKit.xcodeproj/project.pbxproj | 2 +- AsyncDisplayKit/ASCollectionView.mm | 105 +---- AsyncDisplayKit/ASTableView.mm | 83 +--- .../Details/ASChangeSetDataController.m | 4 +- .../Details/ASCollectionDataController.mm | 79 ++-- .../Details/ASDataController+Subclasses.h | 39 +- AsyncDisplayKit/Details/ASDataController.h | 43 +- AsyncDisplayKit/Details/ASDataController.mm | 400 +++++++++++------- AsyncDisplayKit/Details/ASRangeController.h | 53 --- AsyncDisplayKit/Details/ASRangeController.mm | 28 +- 10 files changed, 352 insertions(+), 484 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ac70348372..e57d0a3ff2 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -647,7 +647,7 @@ 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGRect+ASConvenience.h"; sourceTree = ""; }; 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGRect+ASConvenience.m"; sourceTree = ""; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderTests.m; sourceTree = ""; }; - 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionDataController.h; sourceTree = ""; }; + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionDataController.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionDataController.mm; sourceTree = ""; }; 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewFlowLayoutInspector.h; sourceTree = ""; }; 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 07484f3622..b69e685ed7 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -249,7 +249,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _superIsPendingDataLoad = YES; [super reloadData]; }); - [_dataController reloadDataWithCompletion:completion]; + [_dataController reloadDataWithAnimationOptions:kASCollectionViewAnimationNone completion:completion]; } - (void)reloadData @@ -261,7 +261,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASDisplayNodeAssertMainThread(); _superIsPendingDataLoad = YES; - [_dataController reloadDataImmediately]; + [_dataController reloadDataImmediatelyWithAnimationOptions:kASCollectionViewAnimationNone]; [super reloadData]; } @@ -446,7 +446,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); - [_dataController moveSection:section toSection:newSection]; + [_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths @@ -470,7 +470,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); - [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone]; } - (NSString *)__reuseIdentifierForKind:(NSString *)kind @@ -951,46 +951,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } -- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource || _superIsPendingDataLoad) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; - [_batchUpdateBlocks addObject:^{ - [super reloadItemsAtIndexPaths:indexPaths]; - }]; - } else { - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; - [UIView performWithoutAnimation:^{ - [super reloadItemsAtIndexPaths:indexPaths]; - }]; - } -} - -- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath -{ - ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource || _superIsPendingDataLoad) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[fromIndexPath] batched:YES]; - [_batchUpdateBlocks addObject:^{ - [super moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; - }]; - } else { - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[fromIndexPath] batched:NO]; - [UIView performWithoutAnimation:^{ - [super moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; - }]; - } -} - - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -1011,26 +971,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } -- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource || _superIsPendingDataLoad) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; - [_batchUpdateBlocks addObject:^{ - [super reloadSections:indexSet]; - }]; - } else { - [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; - [UIView performWithoutAnimation:^{ - [super reloadSections:indexSet]; - }]; - } -} - - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -1051,43 +991,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } -- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex -{ - ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource || _superIsPendingDataLoad) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:[NSIndexSet indexSetWithIndex:fromIndex] batched:YES]; - [_batchUpdateBlocks addObject:^{ - [super moveSection:fromIndex toSection:toIndex]; - }]; - } else { - [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:[NSIndexSet indexSetWithIndex:fromIndex] batched:NO]; - [UIView performWithoutAnimation:^{ - [super moveSection:fromIndex toSection:toIndex]; - }]; - } -} - -- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController{ - ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource || _superIsPendingDataLoad) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (_performingBatchUpdates) { - [_batchUpdateBlocks addObject:^{ - [super reloadData]; - }]; - } else { - [UIView performWithoutAnimation:^{ - [super reloadData]; - }]; - } -} - #pragma mark - ASCellNodeDelegate - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index bbcfb42bba..99009d8eb9 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -303,7 +303,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)reloadDataWithCompletion:(void (^)())completion { - [_dataController reloadDataWithCompletion:completion]; + ASPerformBlockOnMainThread(^{ + [super reloadData]; + }); + [_dataController reloadDataWithAnimationOptions:UITableViewRowAnimationNone completion:completion]; } - (void)reloadData @@ -314,7 +317,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)reloadDataImmediately { ASDisplayNodeAssertMainThread(); - [_dataController reloadDataImmediately]; + [_dataController reloadDataImmediatelyWithAnimationOptions:UITableViewRowAnimationNone]; + [super reloadData]; } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -429,7 +433,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); - [_dataController moveSection:section toSection:newSection]; + [_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone]; } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation @@ -453,7 +457,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); - [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone]; } #pragma mark - @@ -832,36 +836,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } -- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - ASDisplayNodeAssertMainThread(); - LOG(@"UITableView reloadRows:%ld rows", indexPaths.count); - - if (!self.asyncDataSource) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; - ASPerformBlockWithoutAnimation(preventAnimation, ^{ - [super reloadRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; - }); - - if (_automaticallyAdjustsContentOffset) { - [self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:YES]; - } -} - -- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath -{ - ASDisplayNodeAssertMainThread(); - - if (!self.asyncDataSource) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - [self moveRowAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; -} - - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -878,36 +852,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); } -- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - ASDisplayNodeAssertMainThread(); - LOG(@"UITableView reloadSections:%@", indexSet); - - - if (!self.asyncDataSource) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; - ASPerformBlockWithoutAnimation(preventAnimation, ^{ - [super reloadSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; - }); -} - -- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex -{ - ASDisplayNodeAssertMainThread(); - LOG(@"UITableView moveSection:%@", indexSet); - - - if (!self.asyncDataSource) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - [super moveSection:fromIndex toSection:toIndex]; -} - - - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -923,17 +867,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); } -- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController{ - ASDisplayNodeAssertMainThread(); - LOG(@"UITableView reloadData"); - - if (!self.asyncDataSource) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - [super reloadData]; -} - #pragma mark - ASDataControllerDelegate - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath { diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index bdc0c4993c..666ea86ce7 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -131,7 +131,7 @@ [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; } else { - [super moveSection:section toSection:newSection]; + [super moveSection:section toSection:newSection withAnimationOptions:animationOptions]; } } @@ -174,7 +174,7 @@ [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; } else { - [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; + [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:animationOptions]; } } diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm index 979acd30e3..2408a57e1f 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.mm +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -49,27 +49,37 @@ [self _populateSupplementaryNodesOfKind:kind withMutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } - (void)willReloadData { [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { - // Insert sections + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind]; + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + + NSArray *editingNodes = [self editingNodesOfKind:kind]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; + [self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil]; + + // Insert each section NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { [sections addObject:[NSMutableArray array]]; } - self.editingNode[kind] = sections; - - [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:kind withCompletion:nil]; + [self insertSections:sections ofKind:kind atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] completion:nil]; + + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; }]; + [_pendingNodes removeObjectForKey:kind]; + [_pendingIndexPaths removeObjectForKey:kind]; }]; - - [_pendingNodes removeAllObjects]; - [_pendingIndexPaths removeAllObjects]; } - (void)prepareForInsertSections:(NSIndexSet *)sections @@ -81,6 +91,9 @@ [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } @@ -91,22 +104,23 @@ for (NSUInteger i = 0; i < sections.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } - - [self insertSections:sectionArray ofKind:kind atIndexSet:sections]; - [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:kind withCompletion:nil]; + + [self insertSections:sectionArray ofKind:kind atIndexSet:sections completion:nil]; + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; }]; + [_pendingNodes removeObjectForKey:kind]; + [_pendingIndexPaths removeObjectForKey:kind]; }]; - - [_pendingNodes removeAllObjects]; - [_pendingIndexPaths removeAllObjects]; } - (void)willDeleteSections:(NSIndexSet *)sections { for (NSString *kind in [self supplementaryKinds]) { - [self deleteSectionsOfKind:kind atIndexSet:sections]; - [self commitChangesToNodesOfKind:kind withCompletion:nil]; + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); + + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + [self deleteSectionsOfKind:kind atIndexSet:sections completion:nil]; } } @@ -118,31 +132,40 @@ [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } - (void)willReloadSections:(NSIndexSet *)sections { [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { - // clear sections - [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - self.editingNode[kind][idx] = [[NSMutableArray alloc] init]; - }]; + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; // reinsert the elements - [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:kind withCompletion:nil]; + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; }]; + [_pendingNodes removeObjectForKey:kind]; + [_pendingIndexPaths removeObjectForKey:kind]; }]; - - [_pendingNodes removeAllObjects]; - [_pendingIndexPaths removeAllObjects]; } - (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection { for (NSString *kind in [self supplementaryKinds]) { - [self moveSection:section ofKind:kind toSection:newSection]; - [self commitChangesToNodesOfKind:kind withCompletion:nil]; + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths([self editingNodesOfKind:kind], indexPaths); + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + + // update the section of indexpaths + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; + }]; + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h index 54f1fc259a..32d787910e 100644 --- a/AsyncDisplayKit/Details/ASDataController+Subclasses.h +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -14,7 +14,16 @@ @interface ASDataController (Subclasses) #pragma mark - Internal editing & completed store querying -@property (nonatomic, strong, readonly) NSMutableDictionary *editingNode; + +/** + * Provides a collection of index paths for nodes of the given kind that are currently in the editing store + */ +- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind; + +/** + * 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 @@ -26,7 +35,7 @@ /** * Measure and layout the given nodes in optimized batches, constraining each to a given size in `constrainedSizeForNodeOfKind:atIndexPath:`. */ -- (void)layoutAndInsertFromNodeBlocks:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; /* * Perform measurement and layout of loaded nodes on the main thread, skipping unloaded nodes. @@ -44,34 +53,24 @@ #pragma mark - Node & Section Insertion/Deletion API /** - * Inserts the given nodes of the specified kind into the backing store. + * 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; +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; /** - * Deletes the given nodes of the specified kind in the backing store. + * Deletes the given nodes of the specified kind in the backing store, calling completion on the main thread when the deletion finishes. */ -- (NSArray *)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; /** - * Inserts the given sections of the specified kind in the backing store. + * 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; +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock; /** - * Deletes the given sections of the specified kind in the backing store. + * Deletes the given sections of the specified kind in the backing store, calling completion on the main thread when finished. */ -- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet; - -/** - * Moves the given section of the specified kind in the backing store. - */ -- (void)moveSection:(NSInteger)section ofKind:(NSString *)kind toSection:(NSInteger)newSection; - -/** - * Commit the change for insert/delete node or sections to the backing store, calling completion on the main thread when finished. - */ -- (void)commitChangesToNodesOfKind:(NSString *)kind withCompletion:(void (^)())completionBlock; +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock; #pragma mark - Data Manipulation Hooks diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 1766b7dfc0..6648275d9b 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -91,41 +91,16 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -/** - Called for reload of elements. - */ -- (void)dataController:(ASDataController *)dataController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - -/** - Called for movement of elements. - */ -- (void)dataController:(ASDataController *)dataController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; - /** Called for insertion of sections. */ -- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *> *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for deletion of sections. */ - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -/** - Called for reload of sections. - */ -- (void)dataController:(ASDataController *)dataController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - -/** - Called for movement of sections. - */ -- (void)dataController:(ASDataController *)dataController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex; - -/** - Called for reload data. - */ -- (void)dataControllerDidReloadData:(ASDataController *)dataController; - @end /** @@ -162,6 +137,14 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled; +/** @name Initial loading + * + * @discussion This method allows choosing an animation style for the first load of content. It is typically used just once, + * for example in viewWillAppear:, to specify an animation option for the information already present in the asyncDataSource. + */ + +- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + /** @name Data Updating */ - (void)beginUpdates; @@ -176,7 +159,7 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -192,11 +175,11 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (void)relayoutAllNodes; -- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -- (void)reloadDataWithCompletion:(void (^)())completion; +- (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^ _Nullable)())completion; -- (void)reloadDataImmediately; +- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** @name Data Querying */ diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 7a7d28e446..32d62adac2 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -30,6 +30,7 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { + 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 propogated to _completedNodes. @@ -41,14 +42,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; BOOL _asyncDataFetchingEnabled; BOOL _delegateDidInsertNodes; - BOOL _delegateDidReloadNodes; BOOL _delegateDidDeleteNodes; - BOOL _delegateDidMoveNode; BOOL _delegateDidInsertSections; BOOL _delegateDidDeleteSections; - BOOL _delegateDidReloadSections; - BOOL _delegateDidMoveSection; - BOOL _delegateDidReloadData; } @property (atomic, assign) NSUInteger batchUpdateCounter; @@ -96,13 +92,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // 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:)]; - _delegateDidReloadNodes = [_delegate respondsToSelector:@selector(dataController:didReloadNodes:atIndexPaths:withAnimationOptions:)]; - _delegateDidMoveNode = [_delegate respondsToSelector:@selector(dataController:didMoveNodeAtIndexPath:toIndexPath:)]; - _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSectionsAtIndexSet:withAnimationOptions:)]; - _delegateDidReloadSections = [_delegate respondsToSelector:@selector(dataController:didReloadSectionsAtIndexSet:withAnimationOptions:)]; + _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; - _delegateDidMoveSection = [_delegate respondsToSelector:@selector(dataController:didMoveSection:toSection:)]; - _delegateDidReloadData = [_delegate respondsToSelector:@selector(dataControllerDidReloadData:)]; } + (NSUInteger)parallelProcessorCount @@ -119,14 +110,17 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Cell Layout -- (void)layoutAndInsertFromNodeBlocks:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { - [self _layoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths]; - if (completionBlock) { - completionBlock(nodes, indexPaths); - } - }]; + NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; + + // Processing in batches + for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { + NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); + NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; + NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; + [self _layoutNodes:batchedNodes ofKind:kind atIndexPaths:batchedIndexPaths completion:completionBlock]; + } } - (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { @@ -150,13 +144,21 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); } +/** + * Measures and defines the layout for each node in optimized batches on an editing queue, inserting the results into the backing store. + */ +- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self batchLayoutNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + // Insert finished nodes into data storage + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + - (void)_layoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (!nodes.count) { - if (completionBlock) { - completionBlock(nodes, indexPaths); - } - return; + return; } NSUInteger nodeCount = nodes.count; @@ -236,84 +238,178 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - External Data Querying + Editing -- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (indexPaths.count == 0) return; - LOG(@"insertNodes:%@ ofKind:%@", nodes, kind); NSMutableArray *editingNodes = _editingNodes[kind]; ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); _editingNodes[kind] = editingNodes; + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes); + + [_mainSerialQueue performBlockOnMainThread:^{ + _completedNodes[kind] = completedNodes; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + }]; } -- (NSArray *)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (indexPaths.count == 0) { - return @[]; + return; } - LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@,", indexPaths, kind); + LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@, full index paths in _editingNodes = %@", indexPaths, kind, ASIndexPathsForMultidimensionalArray(_editingNodes[kind])); NSMutableArray *editingNodes = _editingNodes[kind]; - NSArray *deletedNodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[kind], indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); _editingNodes[kind] = editingNodes; - return deletedNodes; + + [_mainSerialQueue performBlockOnMainThread:^{ + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + }]; } -- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet{ +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock +{ if (indexSet.count == 0) return; - LOG(@"insertSections:%@ ofKind:%@", sections, kind); if (_editingNodes[kind] == nil) { _editingNodes[kind] = [NSMutableArray array]; } [_editingNodes[kind] insertObjects:sections atIndexes:indexSet]; -} - -- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet -{ - if (indexSet.count == 0) - return; - - LOG(@"deleteSectionsOfKind:%@", kind); - [_editingNodes[kind] removeObjectsAtIndexes:indexSet]; -} - -- (void)moveSection:(NSInteger)section ofKind:(NSString *)kind toSection:(NSInteger)newSection -{ - NSArray *movedSection = _editingNodes[kind][section]; - [_editingNodes[kind] removeObjectAtIndex:section]; - [_editingNodes[kind] insertObject:movedSection atIndex:newSection]; -} - -- (void)commitChangesToNodesOfKind:(NSString *)kind withCompletion:(void (^)())completionBlock -{ - NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes[kind]); - + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); + [_mainSerialQueue performBlockOnMainThread:^{ - _completedNodes[kind] = completedNodes; + [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; if (completionBlock) { - completionBlock(); + completionBlock(sections, indexSet); } }]; } -#pragma mark - Reload (External API) - -- (void)reloadDataWithCompletion:(void (^)())completion +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock { - [self _reloadDataSynchronously:NO completion:completion]; + if (indexSet.count == 0) + return; + [_editingNodes[kind] removeObjectsAtIndexes:indexSet]; + [_mainSerialQueue performBlockOnMainThread:^{ + [_completedNodes[kind] removeObjectsAtIndexes:indexSet]; + if (completionBlock) { + completionBlock(indexSet); + } + }]; } -- (void)reloadDataImmediately +#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 { - [self _reloadDataSynchronously:YES completion:nil]; + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; } -- (void)_reloadDataSynchronously:(BOOL)synchronously completion:(void (^)())completion +/** + * 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 +{ + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Inserts sections, represented as arrays, into the backing store at the given indicies 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 +{ + [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes sections at the given indicies 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 +{ + [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) { + if (_delegateDidDeleteSections) + [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + }]; +} + +#pragma mark - Initial Load & Full Reload (External API) + +- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [self accessDataSourceWithBlock:^{ + NSMutableArray *indexPaths = [NSMutableArray array]; + NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; + + // insert sections + [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; + + for (NSUInteger i = 0; i < sectionNum; i++) { + NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; + + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; + for (NSUInteger j = 0; j < rowNum; j++) { + [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; + } + } + + // insert elements + [self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; +} + +- (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 { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -321,35 +417,39 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self accessDataSourceSynchronously:synchronously withBlock:^{ NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; - NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; + NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromEntireDataSourceWithMutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; + [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; // Allow subclasses to perform setup before going into the edit transaction [self prepareForReloadData]; void (^transactionBlock)() = ^{ LOG(@"Edit Transaction - reloadData"); - + + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind]); + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + + NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; + [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [self willReloadData]; - - // Insert sections + + // Insert each section NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { [sections addObject:[[NSMutableArray alloc] init]]; } - _editingNodes[ASDataControllerRowNodeKind] = sections; + + [self _insertSections:sections atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] withAnimationOptions:animationOptions]; - [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidReloadData) { - [_delegate dataControllerDidReloadData:self]; - } - if (completion) { - completion(); - } - }]; - }]; + [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + + if (completion) { + dispatch_async(dispatch_get_main_queue(), completion); + } }; if (synchronously) { @@ -448,8 +548,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; if (_batchUpdateCounter == 0) { LOG(@"endUpdatesWithCompletion - beginning"); - [_mainSerialQueue performBlockOnMainThread:^{ - [_delegate dataControllerBeginUpdates:self]; + [_editingTransactionQueue addOperationWithBlock:^{ + [_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 = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); + + LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); + [_delegate dataControllerBeginUpdates:self]; + }]; }]; // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. @@ -460,9 +567,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; block(); }]; [_pendingEditCommandBlocks removeAllObjects]; - - [_mainSerialQueue performBlockOnMainThread:^{ - [_delegate dataController:self endUpdatesAnimated:animated completion:completion]; + + [_editingTransactionQueue addOperationWithBlock:^{ + [_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]; + }]; }]; } } @@ -494,9 +607,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; + NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; [self prepareForInsertSections:sections]; @@ -509,14 +622,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [sectionArray addObject:[NSMutableArray array]]; } - [self insertSections:sectionArray ofKind:ASDataControllerRowNodeKind atIndexSet:sections]; - - [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; - }]; - }]; + [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; + [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; }]; }]; @@ -534,12 +641,10 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // remove elements LOG(@"Edit Transaction - deleteSections: %@", sections); - - [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:sections]; - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidDeleteSections) - [_delegate dataController:self didDeleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; - }]; + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); + + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; }]; }]; } @@ -553,32 +658,29 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; + NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; [self prepareForReloadSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ [self willReloadSections:sections]; - // clear sections - [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - _editingNodes[ASDataControllerRowNodeKind][idx] = [[NSMutableArray alloc] init]; - }]; + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); + + LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); + + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidReloadSections) - [_delegate dataController:self didReloadSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; - }]; - }]; + // reinsert the elements + [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; }]; }]; } -- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -588,14 +690,24 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ [self willMoveSection:section toSection:newSection]; + + // remove elements LOG(@"Edit Transaction - moveSection"); - [self moveSection:section ofKind:ASDataControllerRowNodeKind toSection:newSection]; - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidMoveSection) { - [_delegate dataController:self didMoveSection:section toSection:newSection]; - } - }]; + + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + + // update the section of indexpaths + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + for (NSIndexPath *indexPath in indexPaths) { + [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; + } + + // Don't re-calculate size for moving + [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; }]; } @@ -663,12 +775,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertRows: %@", indexPaths); - [self layoutAndInsertFromNodeBlocks:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; - }]; + [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }]; }]; }]; @@ -688,11 +795,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - deleteRows: %@", indexPaths); - NSArray *deletedNodes = [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:sortedIndexPaths]; - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodes:deletedNodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; + [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; }]; }]; } @@ -707,25 +810,20 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. [self accessDataSourceWithBlock:^{ - NSMutableArray *nodeBlocks = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; // FIXME: This doesn't currently do anything // FIXME: Shouldn't deletes be sorted in descending order? [indexPaths sortedArrayUsingSelector:@selector(compare:)]; for (NSIndexPath *indexPath in indexPaths) { - [nodeBlocks addObject:[_dataSource dataController:self nodeBlockAtIndexPath:indexPath]]; + [nodes addObject:[_dataSource dataController:self nodeBlockAtIndexPath:indexPath]]; } [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadRows: %@", indexPaths); - [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; - [self layoutAndInsertFromNodeBlocks:nodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidReloadNodes) - [_delegate dataController:self didReloadNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; - }]; + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }]; }]; }]; @@ -771,7 +869,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } -- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -781,23 +879,26 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], [NSArray arrayWithObject:indexPath]); - [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:@[indexPath]]; + NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; // Don't re-calculate size for moving - [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:@[newIndexPath]]; - [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ - if (_delegateDidMoveNode) { - [_delegate dataController:self didMoveNodeAtIndexPath:indexPath toIndexPath:newIndexPath]; - } - }]; + NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath]; + [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; }]; }]; } #pragma mark - Data Querying (Subclass API) -- (NSMutableDictionary *)editingNode{ - return _editingNodes; +- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] != nil ? ASIndexPathsForMultidimensionalArray(_editingNodes[kind]) : nil; +} + +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] != nil ? _editingNodes[kind] : [NSMutableArray array]; } - (NSMutableArray *)completedNodesOfKind:(NSString *)kind @@ -864,10 +965,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return ASFindElementsInMultidimensionalArrayAtIndexPaths((NSMutableArray *)[self completedNodes], [indexPaths sortedArrayUsingSelector:@selector(compare:)]); } +/// Returns nodes that can be queried externally. _externalCompletedNodes is used if available, _completedNodes otherwise. - (NSArray *)completedNodes { ASDisplayNodeAssertMainThread(); - return _completedNodes[ASDataControllerRowNodeKind]; + return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes[ASDataControllerRowNodeKind]; } #pragma mark - Dealloc diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index c3e6f31551..d5288f40e2 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -163,30 +163,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -/** - * Called for nodes reload. - * - * @param rangeController Sender. - * - * @param nodes Inserted nodes. - * - * @param indexPaths Index path of reloaded nodes. - * - * @param animationOptions Animation options. See ASDataControllerAnimationOptions. - */ -- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - -/** - * Called for movement of node. - * - * @param rangeController Sender. - * - * @param fromIndexPath Index path of moved node before the movement. - * - * @param toIndexPath Index path of moved node after the movement. - */ -- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; - /** * Called for section insertion. * @@ -198,17 +174,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -/** - * Called for section reload. - * - * @param rangeController Sender. - * - * @param indexSet Index set of reloaded sections. - * - * @param animationOptions Animation options. See ASDataControllerAnimationOptions. - */ -- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - /** * Called for section deletion. * @@ -220,24 +185,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -/** - * Called for movement of section. - * - * @param rangeController Sender. - * - * @param fromIndex Index of moved section before the movement. - * - * @param toIndex Index of moved section after the movement. - */ -- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex; - -/** - * Called for reload data. - * - * @param rangeController Sender. - */ -- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController; - @end NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index c13ef1cdf6..1b8d7d8f89 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -361,30 +361,15 @@ }); } -- (void)dataController:(ASDataController *)dataController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions{ - ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); - ASPerformBlockOnMainThread(^{ - _rangeIsValid = NO; - [_delegate rangeController:self didReloadNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }); -} - -- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections"); ASPerformBlockOnMainThread(^{ _rangeIsValid = NO; [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); } -- (void)dataController:(ASDataController *)dataController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - ASPerformBlockOnMainThread(^{ - _rangeIsValid = NO; - [_delegate rangeController:self didReloadSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - }); -} - - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASPerformBlockOnMainThread(^{ @@ -393,11 +378,4 @@ }); } -- (void)dataControllerDidReloadData:(ASDataController *)dataController{ - ASPerformBlockOnMainThread(^{ - _rangeIsValid = NO; - [_delegate rangeControllerDidReloadData:self]; - }); -} - -@end +@end \ No newline at end of file From a62de38670e2f8d806fa6290d98bc1f12875348f Mon Sep 17 00:00:00 2001 From: rcancro Date: Thu, 18 Feb 2016 20:01:23 -0800 Subject: [PATCH 15/16] currentScaleFactor management --- AsyncDisplayKit/ASTextNode.mm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 202a5bb2ed..3c0d7797b0 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -257,8 +257,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // expensive, and can take some time, so we dispatch onto a bg queue to // actually dealloc. __block ASTextKitRenderer *renderer = _renderer; - // before we remove the renderer, take its scale factor so we set it when a new renderer is created - _currentScaleFactor = renderer.currentScaleFactor; ASPerformBlockOnBackgroundThread(^{ renderer = nil; @@ -340,7 +338,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; [self setNeedsDisplay]; }); - return [[self _renderer] size]; + CGSize size = [[self _renderer] size]; + // the renderer computes the current scale factor during sizing, so let's grab it here + _currentScaleFactor = _renderer.currentScaleFactor; + return size; } #pragma mark - Modifying User Text @@ -381,6 +382,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } }); + // reset the scale factor if we get a new string. + _currentScaleFactor = 0; if (attributedString.length > 0) { CGFloat screenScale = ASScreenScale(); self.ascender = round([[attributedString attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL] ascender] * screenScale)/screenScale; From 58a3ba5e8f2918cda68521048c6c3f4f6077f4dd Mon Sep 17 00:00:00 2001 From: appleguy Date: Thu, 18 Feb 2016 23:06:14 -0800 Subject: [PATCH 16/16] Revert "Revert "[ASCollectionView / ASTableView] Optimize reloadData and reloadSection: methods."" --- AsyncDisplayKit.xcodeproj/project.pbxproj | 2 +- AsyncDisplayKit/ASCollectionView.mm | 105 ++++- AsyncDisplayKit/ASTableView.mm | 83 +++- .../Details/ASChangeSetDataController.m | 4 +- .../Details/ASCollectionDataController.mm | 81 ++-- .../Details/ASDataController+Subclasses.h | 39 +- AsyncDisplayKit/Details/ASDataController.h | 43 +- AsyncDisplayKit/Details/ASDataController.mm | 386 +++++++----------- AsyncDisplayKit/Details/ASRangeController.h | 53 +++ AsyncDisplayKit/Details/ASRangeController.mm | 28 +- 10 files changed, 478 insertions(+), 346 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index e57d0a3ff2..ac70348372 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -647,7 +647,7 @@ 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGRect+ASConvenience.h"; sourceTree = ""; }; 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGRect+ASConvenience.m"; sourceTree = ""; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderTests.m; sourceTree = ""; }; - 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionDataController.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASCollectionDataController.h; sourceTree = ""; }; 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionDataController.mm; sourceTree = ""; }; 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewFlowLayoutInspector.h; sourceTree = ""; }; 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index b69e685ed7..07484f3622 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -249,7 +249,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _superIsPendingDataLoad = YES; [super reloadData]; }); - [_dataController reloadDataWithAnimationOptions:kASCollectionViewAnimationNone completion:completion]; + [_dataController reloadDataWithCompletion:completion]; } - (void)reloadData @@ -261,7 +261,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASDisplayNodeAssertMainThread(); _superIsPendingDataLoad = YES; - [_dataController reloadDataImmediatelyWithAnimationOptions:kASCollectionViewAnimationNone]; + [_dataController reloadDataImmediately]; [super reloadData]; } @@ -446,7 +446,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); - [_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone]; + [_dataController moveSection:section toSection:newSection]; } - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths @@ -470,7 +470,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); - [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } - (NSString *)__reuseIdentifierForKind:(NSString *)kind @@ -951,6 +951,46 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super reloadItemsAtIndexPaths:indexPaths]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; + [UIView performWithoutAnimation:^{ + [super reloadItemsAtIndexPaths:indexPaths]; + }]; + } +} + +- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[fromIndexPath] batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[fromIndexPath] batched:NO]; + [UIView performWithoutAnimation:^{ + [super moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; + }]; + } +} + - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -971,6 +1011,26 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super reloadSections:indexSet]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; + [UIView performWithoutAnimation:^{ + [super reloadSections:indexSet]; + }]; + } +} + - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -991,6 +1051,43 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex +{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:[NSIndexSet indexSetWithIndex:fromIndex] batched:YES]; + [_batchUpdateBlocks addObject:^{ + [super moveSection:fromIndex toSection:toIndex]; + }]; + } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:[NSIndexSet indexSetWithIndex:fromIndex] batched:NO]; + [UIView performWithoutAnimation:^{ + [super moveSection:fromIndex toSection:toIndex]; + }]; + } +} + +- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController{ + ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource || _superIsPendingDataLoad) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_performingBatchUpdates) { + [_batchUpdateBlocks addObject:^{ + [super reloadData]; + }]; + } else { + [UIView performWithoutAnimation:^{ + [super reloadData]; + }]; + } +} + #pragma mark - ASCellNodeDelegate - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 99009d8eb9..bbcfb42bba 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -303,10 +303,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)reloadDataWithCompletion:(void (^)())completion { - ASPerformBlockOnMainThread(^{ - [super reloadData]; - }); - [_dataController reloadDataWithAnimationOptions:UITableViewRowAnimationNone completion:completion]; + [_dataController reloadDataWithCompletion:completion]; } - (void)reloadData @@ -317,8 +314,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)reloadDataImmediately { ASDisplayNodeAssertMainThread(); - [_dataController reloadDataImmediatelyWithAnimationOptions:UITableViewRowAnimationNone]; - [super reloadData]; + [_dataController reloadDataImmediately]; } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -433,7 +429,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); - [_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone]; + [_dataController moveSection:section toSection:newSection]; } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation @@ -457,7 +453,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); - [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } #pragma mark - @@ -836,6 +832,36 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } +- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView reloadRows:%ld rows", indexPaths.count); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + [super reloadRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; + }); + + if (_automaticallyAdjustsContentOffset) { + [self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:YES]; + } +} + +- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath +{ + ASDisplayNodeAssertMainThread(); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + [self moveRowAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; +} + - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -852,6 +878,36 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); } +- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView reloadSections:%@", indexSet); + + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; + ASPerformBlockWithoutAnimation(preventAnimation, ^{ + [super reloadSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; + }); +} + +- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex +{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView moveSection:%@", indexSet); + + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + [super moveSection:fromIndex toSection:toIndex]; +} + + - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -867,6 +923,17 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); } +- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController{ + ASDisplayNodeAssertMainThread(); + LOG(@"UITableView reloadData"); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + [super reloadData]; +} + #pragma mark - ASDataControllerDelegate - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath { diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index 666ea86ce7..bdc0c4993c 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -131,7 +131,7 @@ [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; } else { - [super moveSection:section toSection:newSection withAnimationOptions:animationOptions]; + [super moveSection:section toSection:newSection]; } } @@ -174,7 +174,7 @@ [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; } else { - [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:animationOptions]; + [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } } diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm index 2408a57e1f..979acd30e3 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.mm +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -49,37 +49,27 @@ [self _populateSupplementaryNodesOfKind:kind withMutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; - - // Measure loaded nodes before leaving the main thread - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } - (void)willReloadData { [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { - // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind]; - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - - NSArray *editingNodes = [self editingNodesOfKind:kind]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; - [self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil]; - - // Insert each section + // Insert sections NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { [sections addObject:[NSMutableArray array]]; } - [self insertSections:sections ofKind:kind atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] completion:nil]; - - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + self.editingNode[kind] = sections; + + [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:kind withCompletion:nil]; }]; - [_pendingNodes removeObjectForKey:kind]; - [_pendingIndexPaths removeObjectForKey:kind]; }]; + + [_pendingNodes removeAllObjects]; + [_pendingIndexPaths removeAllObjects]; } - (void)prepareForInsertSections:(NSIndexSet *)sections @@ -91,9 +81,6 @@ [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; - - // Measure loaded nodes before leaving the main thread - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } @@ -104,23 +91,22 @@ for (NSUInteger i = 0; i < sections.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } - - [self insertSections:sectionArray ofKind:kind atIndexSet:sections completion:nil]; - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + + [self insertSections:sectionArray ofKind:kind atIndexSet:sections]; + [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:kind withCompletion:nil]; }]; - [_pendingNodes removeObjectForKey:kind]; - [_pendingIndexPaths removeObjectForKey:kind]; }]; + + [_pendingNodes removeAllObjects]; + [_pendingIndexPaths removeAllObjects]; } - (void)willDeleteSections:(NSIndexSet *)sections { for (NSString *kind in [self supplementaryKinds]) { - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); - - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - [self deleteSectionsOfKind:kind atIndexSet:sections completion:nil]; + [self deleteSectionsOfKind:kind atIndexSet:sections]; + [self commitChangesToNodesOfKind:kind withCompletion:nil]; } } @@ -132,40 +118,31 @@ [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; _pendingNodes[kind] = nodes; _pendingIndexPaths[kind] = indexPaths; - - // Measure loaded nodes before leaving the main thread - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; } } - (void)willReloadSections:(NSIndexSet *)sections { [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - // reinsert the elements - [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + // clear sections + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + self.editingNode[kind][idx] = [[NSMutableArray alloc] init]; + }]; + // reinsert the elements + [self layoutAndInsertFromNodeBlocks:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:kind withCompletion:nil]; }]; - [_pendingNodes removeObjectForKey:kind]; - [_pendingIndexPaths removeObjectForKey:kind]; }]; + + [_pendingNodes removeAllObjects]; + [_pendingIndexPaths removeAllObjects]; } - (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection { for (NSString *kind in [self supplementaryKinds]) { - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths([self editingNodesOfKind:kind], indexPaths); - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - - // update the section of indexpaths - NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; - }]; - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + [self moveSection:section ofKind:kind toSection:newSection]; + [self commitChangesToNodesOfKind:kind withCompletion:nil]; } } diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h index 32d787910e..54f1fc259a 100644 --- a/AsyncDisplayKit/Details/ASDataController+Subclasses.h +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -14,16 +14,7 @@ @interface ASDataController (Subclasses) #pragma mark - Internal editing & completed store querying - -/** - * Provides a collection of index paths for nodes of the given kind that are currently in the editing store - */ -- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind; - -/** - * Read-only access to the underlying editing nodes of the given kind - */ -- (NSMutableArray *)editingNodesOfKind:(NSString *)kind; +@property (nonatomic, strong, readonly) NSMutableDictionary *editingNode; /** * Read only access to the underlying completed nodes of the given kind @@ -35,7 +26,7 @@ /** * Measure and layout the given nodes in optimized batches, constraining each to a given size in `constrainedSizeForNodeOfKind:atIndexPath:`. */ -- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (void)layoutAndInsertFromNodeBlocks:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; /* * Perform measurement and layout of loaded nodes on the main thread, skipping unloaded nodes. @@ -53,24 +44,34 @@ #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. + * Inserts the given nodes of the specified kind into the backing store. */ -- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; /** - * Deletes the given nodes of the specified kind in the backing store, calling completion on the main thread when the deletion finishes. + * Deletes the given nodes of the specified kind in the backing store. */ -- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; +- (NSArray *)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; /** - * Inserts the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + * Inserts the given sections of the specified kind in the backing store. */ -- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock; +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet; /** - * Deletes the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + * Deletes the given sections of the specified kind in the backing store. */ -- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock; +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet; + +/** + * Moves the given section of the specified kind in the backing store. + */ +- (void)moveSection:(NSInteger)section ofKind:(NSString *)kind toSection:(NSInteger)newSection; + +/** + * Commit the change for insert/delete node or sections to the backing store, calling completion on the main thread when finished. + */ +- (void)commitChangesToNodesOfKind:(NSString *)kind withCompletion:(void (^)())completionBlock; #pragma mark - Data Manipulation Hooks diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 6648275d9b..1766b7dfc0 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -91,16 +91,41 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + Called for reload of elements. + */ +- (void)dataController:(ASDataController *)dataController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + Called for movement of elements. + */ +- (void)dataController:(ASDataController *)dataController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; + /** Called for insertion of sections. */ -- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *> *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for deletion of sections. */ - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + Called for reload of sections. + */ +- (void)dataController:(ASDataController *)dataController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + Called for movement of sections. + */ +- (void)dataController:(ASDataController *)dataController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex; + +/** + Called for reload data. + */ +- (void)dataControllerDidReloadData:(ASDataController *)dataController; + @end /** @@ -137,14 +162,6 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled; -/** @name Initial loading - * - * @discussion This method allows choosing an animation style for the first load of content. It is typically used just once, - * for example in viewWillAppear:, to specify an animation option for the information already present in the asyncDataSource. - */ - -- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - /** @name Data Updating */ - (void)beginUpdates; @@ -159,7 +176,7 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -175,11 +192,11 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (void)relayoutAllNodes; -- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; -- (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^ _Nullable)())completion; +- (void)reloadDataWithCompletion:(void (^)())completion; -- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)reloadDataImmediately; /** @name Data Querying */ diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 32d62adac2..7a7d28e446 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -30,7 +30,6 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { - 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 propogated to _completedNodes. @@ -42,9 +41,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; BOOL _asyncDataFetchingEnabled; BOOL _delegateDidInsertNodes; + BOOL _delegateDidReloadNodes; BOOL _delegateDidDeleteNodes; + BOOL _delegateDidMoveNode; BOOL _delegateDidInsertSections; BOOL _delegateDidDeleteSections; + BOOL _delegateDidReloadSections; + BOOL _delegateDidMoveSection; + BOOL _delegateDidReloadData; } @property (atomic, assign) NSUInteger batchUpdateCounter; @@ -92,8 +96,13 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // 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:)]; + _delegateDidReloadNodes = [_delegate respondsToSelector:@selector(dataController:didReloadNodes:atIndexPaths:withAnimationOptions:)]; + _delegateDidMoveNode = [_delegate respondsToSelector:@selector(dataController:didMoveNodeAtIndexPath:toIndexPath:)]; + _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSectionsAtIndexSet:withAnimationOptions:)]; + _delegateDidReloadSections = [_delegate respondsToSelector:@selector(dataController:didReloadSectionsAtIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; + _delegateDidMoveSection = [_delegate respondsToSelector:@selector(dataController:didMoveSection:toSection:)]; + _delegateDidReloadData = [_delegate respondsToSelector:@selector(dataControllerDidReloadData:)]; } + (NSUInteger)parallelProcessorCount @@ -110,17 +119,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Cell Layout -- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (void)layoutAndInsertFromNodeBlocks:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { - NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; - - // Processing in batches - for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { - NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); - NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; - NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; - [self _layoutNodes:batchedNodes ofKind:kind atIndexPaths:batchedIndexPaths completion:completionBlock]; - } + [self _layoutNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + }]; } - (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { @@ -144,21 +150,13 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); } -/** - * Measures and defines the layout for each node in optimized batches on an editing queue, inserting the results into the backing store. - */ -- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - [self batchLayoutNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - // Insert finished nodes into data storage - [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; -} - - (void)_layoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (!nodes.count) { - return; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } + return; } NSUInteger nodeCount = nodes.count; @@ -238,178 +236,84 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - External Data Querying + Editing -- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { if (indexPaths.count == 0) return; + LOG(@"insertNodes:%@ ofKind:%@", nodes, kind); NSMutableArray *editingNodes = _editingNodes[kind]; ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); _editingNodes[kind] = editingNodes; - - // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. - NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes); - - [_mainSerialQueue performBlockOnMainThread:^{ - _completedNodes[kind] = completedNodes; - if (completionBlock) { - completionBlock(nodes, indexPaths); - } - }]; } -- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +- (NSArray *)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { if (indexPaths.count == 0) { - return; + return @[]; } - LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@, full index paths in _editingNodes = %@", indexPaths, kind, ASIndexPathsForMultidimensionalArray(_editingNodes[kind])); + LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@,", indexPaths, kind); NSMutableArray *editingNodes = _editingNodes[kind]; + NSArray *deletedNodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[kind], indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); _editingNodes[kind] = editingNodes; - - [_mainSerialQueue performBlockOnMainThread:^{ - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); - if (completionBlock) { - completionBlock(nodes, indexPaths); - } - }]; + return deletedNodes; } -- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock -{ +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet{ if (indexSet.count == 0) return; + LOG(@"insertSections:%@ ofKind:%@", sections, kind); 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 = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); - - [_mainSerialQueue performBlockOnMainThread:^{ - [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; - if (completionBlock) { - completionBlock(sections, indexSet); - } - }]; } -- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet { if (indexSet.count == 0) return; + + LOG(@"deleteSectionsOfKind:%@", kind); [_editingNodes[kind] removeObjectsAtIndexes:indexSet]; +} + +- (void)moveSection:(NSInteger)section ofKind:(NSString *)kind toSection:(NSInteger)newSection +{ + NSArray *movedSection = _editingNodes[kind][section]; + [_editingNodes[kind] removeObjectAtIndex:section]; + [_editingNodes[kind] insertObject:movedSection atIndex:newSection]; +} + +- (void)commitChangesToNodesOfKind:(NSString *)kind withCompletion:(void (^)())completionBlock +{ + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes[kind]); + [_mainSerialQueue performBlockOnMainThread:^{ - [_completedNodes[kind] removeObjectsAtIndexes:indexSet]; + _completedNodes[kind] = completedNodes; if (completionBlock) { - completionBlock(indexSet); + completionBlock(); } }]; } -#pragma mark - Internal Data Querying + Editing +#pragma mark - Reload (External API) -/** - * 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 +- (void)reloadDataWithCompletion:(void (^)())completion { - [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; + [self _reloadDataSynchronously:NO completion:completion]; } -/** - * 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 +- (void)reloadDataImmediately { - [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; + [self _reloadDataSynchronously:YES completion:nil]; } -/** - * Inserts sections, represented as arrays, into the backing store at the given indicies 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 -{ - [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; - }]; -} - -/** - * Removes sections at the given indicies 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 -{ - [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) { - if (_delegateDidDeleteSections) - [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - }]; -} - -#pragma mark - Initial Load & Full Reload (External API) - -- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - [self accessDataSourceWithBlock:^{ - NSMutableArray *indexPaths = [NSMutableArray array]; - NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; - - // insert sections - [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; - - for (NSUInteger i = 0; i < sectionNum; i++) { - NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; - - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; - for (NSUInteger j = 0; j < rowNum; j++) { - [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; - } - } - - // insert elements - [self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; - }]; -} - -- (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 +- (void)_reloadDataSynchronously:(BOOL)synchronously completion:(void (^)())completion { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -417,39 +321,35 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self accessDataSourceSynchronously:synchronously withBlock:^{ NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; - NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromEntireDataSourceWithMutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; // Allow subclasses to perform setup before going into the edit transaction [self prepareForReloadData]; void (^transactionBlock)() = ^{ LOG(@"Edit Transaction - reloadData"); - - // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind]); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - - NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; - [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - + [self willReloadData]; - - // Insert each section + + // Insert sections NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { [sections addObject:[[NSMutableArray alloc] init]]; } - - [self _insertSections:sections atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] withAnimationOptions:animationOptions]; + _editingNodes[ASDataControllerRowNodeKind] = sections; - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; - - if (completion) { - dispatch_async(dispatch_get_main_queue(), completion); - } + [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidReloadData) { + [_delegate dataControllerDidReloadData:self]; + } + if (completion) { + completion(); + } + }]; + }]; }; if (synchronously) { @@ -548,15 +448,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; if (_batchUpdateCounter == 0) { LOG(@"endUpdatesWithCompletion - beginning"); - [_editingTransactionQueue addOperationWithBlock:^{ - [_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 = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); - - LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); - [_delegate dataControllerBeginUpdates:self]; - }]; + [_mainSerialQueue performBlockOnMainThread:^{ + [_delegate dataControllerBeginUpdates:self]; }]; // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. @@ -567,15 +460,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; block(); }]; [_pendingEditCommandBlocks removeAllObjects]; - - [_editingTransactionQueue addOperationWithBlock:^{ - [_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]; - }]; + + [_mainSerialQueue performBlockOnMainThread:^{ + [_delegate dataController:self endUpdatesAnimated:animated completion:completion]; }]; } } @@ -607,9 +494,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; [self prepareForInsertSections:sections]; @@ -622,8 +509,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [sectionArray addObject:[NSMutableArray array]]; } - [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + [self insertSections:sectionArray ofKind:ASDataControllerRowNodeKind atIndexSet:sections]; + + [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; @@ -641,10 +534,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // remove elements LOG(@"Edit Transaction - deleteSections: %@", sections); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + + [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:sections]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidDeleteSections) + [_delegate dataController:self didDeleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }]; }]; }]; } @@ -658,29 +553,32 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedNodeBlocks = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodeBlocks mutableIndexPaths:updatedIndexPaths]; [self prepareForReloadSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ [self willReloadSections:sections]; - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + // clear sections + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + _editingNodes[ASDataControllerRowNodeKind][idx] = [[NSMutableArray alloc] init]; + }]; - // reinsert the elements - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + [self layoutAndInsertFromNodeBlocks:updatedNodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidReloadSections) + [_delegate dataController:self didReloadSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; } -- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -690,24 +588,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ [self willMoveSection:section toSection:newSection]; - - // remove elements LOG(@"Edit Transaction - moveSection"); - - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - - // update the section of indexpaths - NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - for (NSIndexPath *indexPath in indexPaths) { - [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; - } - - // Don't re-calculate size for moving - [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + [self moveSection:section ofKind:ASDataControllerRowNodeKind toSection:newSection]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidMoveSection) { + [_delegate dataController:self didMoveSection:section toSection:newSection]; + } + }]; }]; }]; } @@ -775,7 +663,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertRows: %@", indexPaths); - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self layoutAndInsertFromNodeBlocks:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; @@ -795,7 +688,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - deleteRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; + NSArray *deletedNodes = [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:sortedIndexPaths]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodes:deletedNodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; }]; }]; } @@ -810,20 +707,25 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. [self accessDataSourceWithBlock:^{ - NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + NSMutableArray *nodeBlocks = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; // FIXME: This doesn't currently do anything // FIXME: Shouldn't deletes be sorted in descending order? [indexPaths sortedArrayUsingSelector:@selector(compare:)]; for (NSIndexPath *indexPath in indexPaths) { - [nodes addObject:[_dataSource dataController:self nodeBlockAtIndexPath:indexPath]]; + [nodeBlocks addObject:[_dataSource dataController:self nodeBlockAtIndexPath:indexPath]]; } [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; + [self layoutAndInsertFromNodeBlocks:nodeBlocks ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidReloadNodes) + [_delegate dataController:self didReloadNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; }]; }]; }]; @@ -869,7 +771,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } -- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); @@ -879,26 +781,23 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], [NSArray arrayWithObject:indexPath]); - NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:@[indexPath]]; // Don't re-calculate size for moving - NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath]; - [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:@[newIndexPath]]; + [self commitChangesToNodesOfKind:ASDataControllerRowNodeKind withCompletion:^{ + if (_delegateDidMoveNode) { + [_delegate dataController:self didMoveNodeAtIndexPath:indexPath toIndexPath:newIndexPath]; + } + }]; }]; }]; } #pragma mark - Data Querying (Subclass API) -- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind -{ - return _editingNodes[kind] != nil ? ASIndexPathsForMultidimensionalArray(_editingNodes[kind]) : nil; -} - -- (NSMutableArray *)editingNodesOfKind:(NSString *)kind -{ - return _editingNodes[kind] != nil ? _editingNodes[kind] : [NSMutableArray array]; +- (NSMutableDictionary *)editingNode{ + return _editingNodes; } - (NSMutableArray *)completedNodesOfKind:(NSString *)kind @@ -965,11 +864,10 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return ASFindElementsInMultidimensionalArrayAtIndexPaths((NSMutableArray *)[self completedNodes], [indexPaths sortedArrayUsingSelector:@selector(compare:)]); } -/// Returns nodes that can be queried externally. _externalCompletedNodes is used if available, _completedNodes otherwise. - (NSArray *)completedNodes { ASDisplayNodeAssertMainThread(); - return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes[ASDataControllerRowNodeKind]; + return _completedNodes[ASDataControllerRowNodeKind]; } #pragma mark - Dealloc diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index d5288f40e2..c3e6f31551 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -163,6 +163,30 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Called for nodes reload. + * + * @param rangeController Sender. + * + * @param nodes Inserted nodes. + * + * @param indexPaths Index path of reloaded nodes. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. + */ +- (void)rangeController:(ASRangeController *)rangeController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + * Called for movement of node. + * + * @param rangeController Sender. + * + * @param fromIndexPath Index path of moved node before the movement. + * + * @param toIndexPath Index path of moved node after the movement. + */ +- (void)rangeController:(ASRangeController *)rangeController didMoveNodeAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; + /** * Called for section insertion. * @@ -174,6 +198,17 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Called for section reload. + * + * @param rangeController Sender. + * + * @param indexSet Index set of reloaded sections. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. + */ +- (void)rangeController:(ASRangeController *)rangeController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + /** * Called for section deletion. * @@ -185,6 +220,24 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Called for movement of section. + * + * @param rangeController Sender. + * + * @param fromIndex Index of moved section before the movement. + * + * @param toIndex Index of moved section after the movement. + */ +- (void)rangeController:(ASRangeController *)rangeController didMoveSection:(NSInteger)fromIndex toSection:(NSInteger)toIndex; + +/** + * Called for reload data. + * + * @param rangeController Sender. + */ +- (void)rangeControllerDidReloadData:(ASRangeController *)rangeController; + @end NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 1b8d7d8f89..c13ef1cdf6 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -361,15 +361,30 @@ }); } -- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)dataController:(ASDataController *)dataController didReloadNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions{ + ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); + ASPerformBlockOnMainThread(^{ + _rangeIsValid = NO; + [_delegate rangeController:self didReloadNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }); +} + +- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections"); ASPerformBlockOnMainThread(^{ _rangeIsValid = NO; [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); } +- (void)dataController:(ASDataController *)dataController didReloadSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASPerformBlockOnMainThread(^{ + _rangeIsValid = NO; + [_delegate rangeController:self didReloadSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + }); +} + - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASPerformBlockOnMainThread(^{ @@ -378,4 +393,11 @@ }); } -@end \ No newline at end of file +- (void)dataControllerDidReloadData:(ASDataController *)dataController{ + ASPerformBlockOnMainThread(^{ + _rangeIsValid = NO; + [_delegate rangeControllerDidReloadData:self]; + }); +} + +@end