From bfc137d935fed03e76345a1aaebaf840595334ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ka=C5=A1par?= Date: Fri, 4 Aug 2017 07:09:13 -0700 Subject: [PATCH 01/26] [Showcase] Update showcase - add blog post link to ClassDojo icon #trivial (#493) * Update showcase - add blog post link to ClassDojo icon * Revert "Update showcase - add blog post link to ClassDojo icon" This reverts commit 6b180f1191626b86edbeacd01bec70ef4a1b97db. * Add link to ClassDojo icon --- docs/showcase.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/showcase.md b/docs/showcase.md index ab6c061239..b90d9957a3 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -125,6 +125,8 @@ permalink: /showcase.html
ClassDojo +
+ Powering Class Story With Texture From 27abc771e0a4f06871968133ea8d18264c18f87f Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 4 Aug 2017 16:22:39 +0100 Subject: [PATCH 02/26] [Layout Transition] Avoid calling didComplete method if pending layout transition is nil (#490) * Avoid calling didComplete method if pending layout transition is nil * Update CHANGELOG --- CHANGELOG.md | 1 + Source/ASDisplayNode+Layout.mm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f85d51016..6ec5646dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) +- Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 66a5687685..eb79f1f61b 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -850,8 +850,8 @@ ASPrimitiveTraitCollectionDeprecatedImplementation if (pendingLayoutTransition != nil) { [self _setCalculatedDisplayNodeLayout:pendingLayoutTransition.pendingLayout]; [self _completeLayoutTransition:pendingLayoutTransition]; + [self _pendingLayoutTransitionDidComplete]; } - [self _pendingLayoutTransitionDidComplete]; } /** From 3c6b836571609e9eb94d5730c93998c8b39a467e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 4 Aug 2017 16:24:05 +0100 Subject: [PATCH 03/26] Make sure _locked_constrainedSizeForLayoutPass is called with the lock held (#488) --- Source/ASDisplayNode+Layout.mm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index eb79f1f61b..5674997d00 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -520,7 +520,14 @@ ASPrimitiveTraitCollectionDeprecatedImplementation measurementCompletion:(void(^)())completion { ASDisplayNodeAssertMainThread(); - [self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass] + + ASSizeRange sizeRange; + { + ASDN::MutexLocker l(__instanceLock__); + sizeRange = [self _locked_constrainedSizeForLayoutPass]; + } + + [self transitionLayoutWithSizeRange:sizeRange animated:animated shouldMeasureAsync:shouldMeasureAsync measurementCompletion:completion]; From 10efa31abdd4f4a13f11f560f6c3ae914512b8b8 Mon Sep 17 00:00:00 2001 From: Christian Selig Date: Fri, 4 Aug 2017 14:06:30 -0300 Subject: [PATCH 04/26] iOS 11 UITableView automatic height estimation fix (#485) * Add iOS 11 checks. * Negate iOS 11 automatic table height estimatation * Negate iOS 11 automatic estimated table row heights * Add note about iOS 11 estimated height behavior --- CHANGELOG.md | 1 + Source/ASTableView.mm | 7 +++++++ Source/Base/ASAvailability.h | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec5646dba..e9f2f007cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) +- Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 7cf6abcc30..3f230ba40d 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -345,6 +345,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; _retainedLayer = self.layer; } + // iOS 11 automatically uses estimated heights, so disable those (see PR #485) + if (AS_AT_LEAST_IOS11) { + super.estimatedRowHeight = 0.0; + super.estimatedSectionHeaderHeight = 0.0; + super.estimatedSectionFooterHeight = 0.0; + } + return self; } diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index ff71816027..64c5a127ea 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -27,8 +27,13 @@ #define kCFCoreFoundationVersionNumber_iOS_10_0 1348.00 #endif +#ifndef kCFCoreFoundationVersionNumber_iOS_11_0 + #define kCFCoreFoundationVersionNumber_iOS_11_0 1438.10 +#endif + #define AS_AT_LEAST_IOS9 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_9_0) #define AS_AT_LEAST_IOS10 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_10_0) +#define AS_AT_LEAST_IOS11 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_11_0) // If Yoga is available, make it available anywhere we use ASAvailability. // This reduces Yoga-specific code in other files. From 69a915356dc48f566951361225004c156035d39c Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sun, 6 Aug 2017 13:20:33 +0100 Subject: [PATCH 05/26] [ASStackLayoutSpec] Fix interitem spacing not being reset on new lines and add snapshot tests #trivial (#491) * Add snapshot tests for interitem and interline spacings of stack spec * Improve comment * Make sure item spacings are properly considered and reset on new lines, snapshot tests included --- .../Layout/ASStackUnpositionedLayout.mm | 12 +- Tests/ASStackLayoutSpecSnapshotTests.mm | 114 +++++++++++++++++- ...eSpacingOverflow_alignContentCenter@2x.png | Bin 0 -> 2044 bytes ...LineSpacingOverflow_alignContentEnd@2x.png | Bin 0 -> 2051 bytes ...ingOverflow_alignContentSpaceAround@2x.png | Bin 0 -> 2044 bytes ...ngOverflow_alignContentSpaceBetween@2x.png | Bin 0 -> 2023 bytes ...neSpacingOverflow_alignContentStart@2x.png | Bin 0 -> 2023 bytes ...SpacingUnderflow_alignContentCenter@2x.png | Bin 0 -> 5395 bytes ...ineSpacingUnderflow_alignContentEnd@2x.png | Bin 0 -> 5372 bytes ...ngUnderflow_alignContentSpaceAround@2x.png | Bin 0 -> 5255 bytes ...gUnderflow_alignContentSpaceBetween@2x.png | Bin 0 -> 5366 bytes ...eSpacingUnderflow_alignContentStart@2x.png | Bin 0 -> 5417 bytes ...pacingUnderflow_alignContentStretch@2x.png | Bin 0 -> 5325 bytes .../testFlexWrapWithItemSpacings@2x.png | Bin 0 -> 7346 bytes ...ithItemSpacingsBeingResetOnNewLines@2x.png | Bin 0 -> 10115 bytes 15 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png create mode 100644 Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png diff --git a/Source/Private/Layout/ASStackUnpositionedLayout.mm b/Source/Private/Layout/ASStackUnpositionedLayout.mm index 8bed958aa4..54a17bb089 100644 --- a/Source/Private/Layout/ASStackUnpositionedLayout.mm +++ b/Source/Private/Layout/ASStackUnpositionedLayout.mm @@ -485,7 +485,8 @@ static CGFloat computeItemsStackDimensionSum(const std::vector collectChildrenIntoLines(const std:: std::vector lines; std::vector lineItems; CGFloat lineStackDimensionSum = 0; + CGFloat interitemSpacing = 0; for(auto it = items.begin(); it != items.end(); ++it) { const auto &item = *it; const CGFloat itemStackDimension = stackDimension(style.direction, item.layout.size); - const CGFloat itemAndSpacingStackDimension = (lineItems.empty() ? 0.0 : style.spacing) + item.child.style.spacingBefore + itemStackDimension + item.child.style.spacingAfter; - const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + itemAndSpacingStackDimension, style, sizeRange) < 0); + const CGFloat itemAndSpacingStackDimension = item.child.style.spacingBefore + itemStackDimension + item.child.style.spacingAfter; + const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + interitemSpacing + itemAndSpacingStackDimension, style, sizeRange) < 0); const BOOL breakCurrentLine = negativeViolationIfAddItem && !lineItems.empty(); if (breakCurrentLine) { lines.push_back({.items = std::vector (lineItems)}); lineItems.clear(); lineStackDimensionSum = 0; + interitemSpacing = 0; } lineItems.push_back(std::move(item)); - lineStackDimensionSum += itemAndSpacingStackDimension; + lineStackDimensionSum += interitemSpacing + itemAndSpacingStackDimension; + interitemSpacing = style.spacing; } // Handle last line diff --git a/Tests/ASStackLayoutSpecSnapshotTests.mm b/Tests/ASStackLayoutSpecSnapshotTests.mm index 042203fe1b..7f4c3251f6 100644 --- a/Tests/ASStackLayoutSpecSnapshotTests.mm +++ b/Tests/ASStackLayoutSpecSnapshotTests.mm @@ -110,6 +110,7 @@ static NSArray *defaultTextNodes() alignItems:style.alignItems flexWrap:style.flexWrap alignContent:style.alignContent + lineSpacing:style.lineSpacing children:children]; [self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; @@ -163,6 +164,7 @@ static NSArray *defaultTextNodes() } - (void)testStackLayoutSpecWithAlignContent:(ASStackLayoutAlignContent)alignContent + lineSpacing:(CGFloat)lineSpacing sizeRange:(ASSizeRange)sizeRange identifier:(NSString *)identifier { @@ -170,8 +172,9 @@ static NSArray *defaultTextNodes() .direction = ASStackLayoutDirectionHorizontal, .flexWrap = ASStackLayoutFlexWrapWrap, .alignContent = alignContent, + .lineSpacing = lineSpacing, }; - + CGSize subnodeSize = {50, 50}; NSArray *subnodes = @[ ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), @@ -181,10 +184,17 @@ static NSArray *defaultTextNodes() ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), ]; - + [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; } +- (void)testStackLayoutSpecWithAlignContent:(ASStackLayoutAlignContent)alignContent + sizeRange:(ASSizeRange)sizeRange + identifier:(NSString *)identifier +{ + [self testStackLayoutSpecWithAlignContent:alignContent lineSpacing:0.0 sizeRange:sizeRange identifier:identifier]; +} + #pragma mark - - (void)testDefaultStackLayoutElementFlexProperties @@ -1209,6 +1219,77 @@ static NSArray *defaultTextNodes() [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:children identifier:nil]; } +#pragma mark - Flex wrap and item spacings test + +- (void)testFlexWrapWithItemSpacings +{ + ASStackLayoutSpecStyle style = { + .spacing = 50, + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStart, + .lineSpacing = 5, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ]; + + for (ASDisplayNode *subnode in subnodes) { + subnode.style.spacingBefore = 5; + subnode.style.spacingAfter = 5; + } + + // 3 items, each item has a size of {50, 50} + // width is 230px, enough to fit all items without taking all spacings into account + // Test that all spacings are included and therefore the last item is pushed to a second line. + // See: https://github.com/TextureGroup/Texture/pull/472 + static ASSizeRange kSize = {{230, 300}, {230, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + +- (void)testFlexWrapWithItemSpacingsBeingResetOnNewLines +{ + ASStackLayoutSpecStyle style = { + .spacing = 5, + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStart, + .lineSpacing = 5, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + // 1st line + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + // 2nd line + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + // 3rd line + ASDisplayNodeWithBackgroundColor([UIColor brownColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor orangeColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor purpleColor], subnodeSize), + ]; + + for (ASDisplayNode *subnode in subnodes) { + subnode.style.spacingBefore = 5; + subnode.style.spacingAfter = 5; + } + + // 3 lines, each line has 3 items, each item has a size of {50, 50} + // width is 190px, enough to fit 3 items into a line + // Test that interitem spacing is reset on new lines. Otherwise, lines after the 1st line would have only 2 items. + // See: https://github.com/TextureGroup/Texture/pull/472 + static ASSizeRange kSize = {{190, 300}, {190, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + #pragma mark - Content alignment tests - (void)testAlignContentUnderflow @@ -1282,4 +1363,33 @@ static NSArray *defaultTextNodes() [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; } +#pragma mark - Line spacing tests + +- (void)testAlignContentAndLineSpacingUnderflow +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // 10px between lines + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 320}, {110, 320}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart lineSpacing:10 sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter lineSpacing:10 sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd lineSpacing:10 sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceAround"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch lineSpacing:10 sizeRange:kSize identifier:@"alignContentStretch"]; +} + +- (void)testAlignContentAndLineSpacingOverflow +{ + // 6 lines, each line has 1 item, each item has a size of {50, 50} + // 10px between lines + // width is 40px. It's 10px smaller than the width of each item (40px vs 50px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{40, 310}, {40, 310}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart lineSpacing:10 sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter lineSpacing:10 sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd lineSpacing:10 sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround lineSpacing:10 sizeRange:kSize identifier:@"alignContentSpaceAround"]; +} + @end diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..188cd6007e74b51c09f058c2719e3f17d98f531c GIT binary patch literal 2044 zcmeAS@N?(olHy`uVBq!ia0vp^0SpXGIUH<2R+gCxNPw|8$lZxy-8q?;Kn`btM`SSr zgP1A^GkON8d;ki{WV-l=0BI&51_DMiAfW-FL2TwPJsV6Sfb;=R7srqa#y58zvko)x zusHnxe|hC+*0l<2tQzI6pXfIEaqanM8~OUrd~#3Ep8j37{J;k$(G>^Yumm5 z%}EE+Ck~_?zH}3;-k@}e<$0jK35{plmTrcqf8|j;52#Opc{Za3iOksYJq_rr4UGnQ%%T!z0>^R}Fdb$)w1YP!;f906_C_uq zX^zKc0gZ{wiFeqv4005DZac8H@wOC7gEjrjdr;2#*Vj5AVTTP@$b8gKSSc@F1P-E#gPUGj8-R6O>F2ft zTec_R<`-G8j)GUmTV=qO#cjy>#Rt}5@%1rf&NDFouvcM!A#;4=-zGK@i7DTEwQzWqQei0|kvDD-PTjN@31jz`JH&)2oM*&-brg!vu`?SK2G&>P~NbV#x)J z-qq_DRQ))zZd3*>3?5iM{Gajv`^WPCfAFPl9_~NP3=IGO$2Qb22Il3ppuGGRn3H83 zMOZHG`@b~z7grbmS{Hw((>h%WT4@Kk?f?H#Ve*fX{?BaS#h}nI(P56UzyS}1bCMhh zDgw`W&M?Hc{Hr_jfN?!1OF~U81JjLp3BD4b+$vw+y5ju%r1zZFhvsciuemtssU3!>j0IzU$kfJ`;-5Jiw%@= zK%6~n$Nn*JJlM_%s##tHTkK)7># zfrt6PpZ}LPpA^?|R?g@B5_nLsY~pR3`tzTko4&(2_vi8+r5$GMaVJMI?-Rf))iBx>rP-g{5p66(`I2G&drU$c1kW+nj3lktZn=c%<=#K7hj(b z%H}gc+5GL@gSj$}B1bNqw0}8Ot@Y`{)eF>}k2guP%)Z>fUuSpz`Qz`^Z2T4X4_Du0 zl*m_TVA!S|A$$yIY{4(dr(C-at=pg-GoR^nHSZM%-YW;5wq4zvy?`xi0o%1co%f~z z%s@0=rG4v;DCq1IZ2KL6@lkG zECxay#cWIxoJTB~8GBeB&1n>U@cVqxKNhA$u0P%kAeE80RFY`$FjOw_zyDjkef>Y% zMh*tzLl_Yd^>M6!+1MViR;;gKV5*p{04ho^Z%_F3q5dH!8!(wX_@X<*ZV%hBe+(QC zau0w?;a^>PGWm=R4ENGnKs5kZcYDbj zexL~39H7X8^#yxaK{g4(ZIZjR(HuiE+>SkK=T){GId< Z<|~``f9YRXcmY&Pd%F6$taD0e0sw+nGM4}V literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..188cd6007e74b51c09f058c2719e3f17d98f531c GIT binary patch literal 2044 zcmeAS@N?(olHy`uVBq!ia0vp^0SpXGIUH<2R+gCxNPw|8$lZxy-8q?;Kn`btM`SSr zgP1A^GkON8d;ki{WV-l=0BI&51_DMiAfW-FL2TwPJsV6Sfb;=R7srqa#y58zvko)x zusHnxe|hC+*0l<2tQzI6pXfIEaqanM8~OUrd~#3Ep8j37{J;k$(G>^Yumm5 z%}EE+Ck~_?zH}3;-k@}e<$0jK35{plmTrcqf8|j;52#Opc{Za3iOksYJq_rr4UGnQ%%T!z0>^R}Fdb$)w1YP!;f906_C_uq zX^zKc0gZ{wiFeqv4005DZac8H@wOC7gEjrjdr;2#*Vj5AVTTP@$b8gKSSc@F1P-E#gPUGj8-R6O>F2ft zTec_R<`-G8j)GUmTV=qO#cjy>#Rt}5@%1rf&NDFouvcM!A#;4=-zGK@i7DTEwQzWqQei0|kvDD-PTjN@31jz`JH&)2oM*&-brg!vu`?SK2G&>P~NbV#x)J z-qq_DRQ))zZd3*>3?5iM{Gajv`^WPCfAFPl9_~NP3=IGO$2Qb22Il3ppuGGRn3H83 zMOZHG`@b~z7grbmS{Hw((>h%WT4@Kk?f?H#Ve*fX{?BaS#h}nI(P56UzyS}1bCMhh zDgw`W&M?Hc{Hr_jfN?!1OF~U81JjLp3BD4b+$vw+y5ju%r1zZFhvsciuemtssU3!>j0IzU$kfJ`;-5Jiw%@= zK%6~n$Nn*JJlM_%s##tHTkK2f2hvkKT^vIy7~fnw*m;D3 zhxx#l`(I)&HKj(Rq$SSh>f6w6Q-A*RbNlxX0`KkFv)-DuO?-s|Ynu8Dp)}^)1-xti z^%NcQyTNJw@JsLl%kvIwMiUy(wk_RkJfV>%CE=Av@jM`{z&xARJ3U>2S@OhzD-)mD z0BHv{GwEsP&VkjhR6bh?(#MlyrZaagSbebjtj{2Qk~eZ9tgON6m-U%E2kA4~aC1XR z8CboKZ00$LMVk{}z5%PBVRdN^#GbC=J&1 zFYm$jmVcM|L?rb3nM8j?*X1nW-O_k9o)HvsiRw0gyY^2=1_uxKG~fG;;2>CD7jwc8 z6p9a6PcM6a031@m_IghwKwC!q%)Xv6TXUu z@XxQQ%Sjal22EJJ|M=+1QYW{;Uy&hU;rGp~0tY-4&Pj44 zs0cjgVKETuC}v}l;5=f<%-F;7Xig)88&lyq2L|Ovi*$vCi4Jp&4VY^LfBl%ChGGE=JJ;-ss_CSdvS-=&55ATzHV=$^J_-a@eaj$7-ytFysQiQAI$ zl?Nh!wJ!EfG}tL0sNvKK?gzpg3=$9a1~cFYIeY>Ar}BT|jrk4=K8^L944~lWCc_Xc zv4zjhp{o4x|BL^gKR-WTU4W5+=wyo&5a(Af$WLI1YxsVOU7_K=uK=)QJn%Byp!Vng zhw2jWY$YY}n;9t0YXK@a>$#7IGbl8~pFLskr+>Xx!}6vps3!Jw^>bP0l+XkKMS%o; literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..20d303b586029a5ef5c5f72240c7f0268d49d1a6 GIT binary patch literal 2023 zcmeAS@N?(olHy`uVBq!ia0vp^0SpXGIUH<2R+gCxNPw|8$lZxy-8q?;Kn`btM`SSr zgP1A^GkON8d;ki{WV-l=0BI&51_DMiAfW-FL2Tv|i|>2f2hvkKT^vIy7~fnw*m;D3 zhxx#l`(I)&HKj(Rq$SSh>f6w6Q-A*RbNlxX0`KkFv)-DuO?-s|Ynu8Dp)}^)1-xti z^%NcQyTNJw@JsLl%kvIwMiUy(wk_RkJfV>%CE=Av@jM`{z&xARJ3U>2S@OhzD-)mD z0BHv{GwEsP&VkjhR6bh?(#MlyrZaagSbebjtj{2Qk~eZ9tgON6m-U%E2kA4~aC1XR z8CboKZ00$LMVk{}z5%PBVRdN^#GbC=J&1 zFYm$jmVcM|L?rb3nM8j?*X1nW-O_k9o)HvsiRw0gyY^2=1_uxKG~fG;;2>CD7jwc8 z6p9a6PcM6a031@m_IghwKwC!q%)Xv6TXUu z@XxQQ%Sjal22EJJ|M=+1QYW{;Uy&hU;rGp~0tY-4&Pj44 zs0cjgVKETuC}v}l;5=f<%-F;7Xig)88&lyq2L|Ovi*$vCi4Jp&4VY^LfBl%ChGGE=JJ;-ss_CSdvS-=&55ATzHV=$^J_-a@eaj$7-ytFysQiQAI$ zl?Nh!wJ!EfG}tL0sNvKK?gzpg3=$9a1~cFYIeY>Ar}BT|jrk4=K8^L944~lWCc_Xc zv4zjhp{o4x|BL^gKR-WTU4W5+=wyo&5a(Af$WLI1YxsVOU7_K=uK=)QJn%Byp!Vng zhw2jWY$YY}n;9t0YXK@a>$#7IGbl8~pFLskr+>Xx!}6vps3!Jw^>bP0l+XkKMS%o; literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..27b26426db5c4cd34f9189461069fd32031aa606 GIT binary patch literal 5395 zcmeHLX;f258ch%(!G(YjP?)WmjUi^c$vMm7OigphQ-fZDVDm^o+qoS8A_ywpo2U#05Sz28^& zo!IE@u|VlhN(cmE0m0MF543y)Lhdk10o;wB&@ccEGRDut1;OHK^?(m($TKhofzU97 zFC@Xw;63Qn+U4f63AA$H0xzT!cr%%4;d`Z4ONF`1L3@SZ=DcY?vXAe}>abUv`wLD= zpe!-M=xduQX!uc8B8^AmL@jw+rPCwMh@?~Zwq_fkkn$20Z!`vl##quUl#rYrgq$9d zbL?k#JttdjCtU+4Hm2M}9#t?0MUzKqax{)1g!>Rl=?I}q%SDVXW1%iaRW}3`tR_c$ zhLm@aqs`l%;&K?WKkVdr7}ERR7lrXw{5tqoSpHwx@_u}9k6ky-Q!>k{*#?2>fp{fA{+9hy<~;TwcfxA$B7~8 z!9ilq@Y!4o*|8{o0?0>3GX{8?3_aXr6Z+vEcpYg zNl(gSO*vUs{Shhyfe&MhOoi1~3?fYoWl`}ZH^&rk>mL;DXn>cLYYmokXtTKLuCbPI z9g|N{pLn(xp3wpeO-dK&1B<(riYL@g5li$vJe%}RF>h+CRy<4 zS7{^JXEO6LWl`OAO>Im1%oi7@>iL^I<0f)x=doj(hXyB^5N%2PUcu%!J`VvsP&Q>D z{L{IAw?#NU_c=E0V{3ST+?{jQJKOEmgYBD+Od&}+XlpSV)JB{1P=j$!AE zl@UsjZLK^yWL9m zy=WyqxX@#l-A@i04>7pt(*vs#@pDG;1&@_wM^K)rLwf%8o9uYTX zw}_nI5>Qvun*g@ItMO1k65iq`c=RWGfAC0zNlFXuov3xY$|AiQds9CM;rt66-jY&6 z(I0kmFGTllqM1duH7?_qW_J@eapKn#nl^gLqDI0a`xnGzp9-i?$7b##;YpmwYE#!H zD2IGx>>f0IUm${Kv?R!KpaJxOe~Qwko<+SFz23f5JfTe{z00f@e_vvj6!v&=S#-`x z=n4@RNK1G*%sAm?y9Y~H>_w18g5hbrae2@U7}n)*#x0ecE_D zbgrZ<1RmY>VV(9znHDH6c+p3hl$BQriMF3fWGH&Vn+J$`^RjrL=T@mE5GAoVug9HT8v#u&Wtje3F7P>Tf98Rj(|_BO39qkqw(RaW6-w zwF|IGJ8o;P)(L1R49`8VloKne>2!8w*XHYj1o4@zMlt=)M%fo+;;GX2X9y9;FTU+mvfJF}&vRw7 z#1w~}C`yYO!09u*uOnEF4S!M1l*w=%EWh7t z%4#k#1pB=?hbkZ`5`mCPE0Y)Q1BYD!d|>V8-zRsiQ$_erOMTK>lacSNE% zJ{Jt$K+9ba|tFL15(oAiu-Max6IFhAzcE)D^i zPKrwgn-a3cPsV`}YyPNjWCu0YMHakVo%~&iao7I(#W13er&9r<*0S1jD5N^Z86!)f zqjnNdVN@$C1tkDgBUvR%OQBfKu(fJ!JzVpqD#H?nAL1t# zM*6X%V$FQ>-xAx#4ou_Lzv1nxx%!cPj1kmVz;tDkKT<+g(Gs@6hmFq)6hwsKj}i~c zG?J6MLweTqp2C$uQ|gw<-&)AHtCL7*Hpf6D!Fy+zmaaQvtiC-z}Y=S4k!t*NK& zmlKWHXk>D6M2Ef!sJa7U{#D32MRsC=bwVDXm;;@_Ntyg>XGhMwnG*|(Hw2RHWFo7X zDIQk7>q=J}2zup}SqNx*Crrv!ov?njeD_+iUV+9`VK;G}%|T`-#_5fwTya93rX0!* zSem3Y3Q3wsPV1Z*_p$^SoW5BP^doRGakFM;9q>cASyyuj7@tqx2sj+>tO;rdK8ZBz zcA|kHDxLLAYk;k4h0RfOjZOoV>HcO!Qul3f@Kmt$T-<3WtqQtJiO2hTaU8wVd)BHB z5W;~nw@wZ96E03qhr!wm)ex(9ypnFPo30I@xRZHG)?lPQ@7sXdo}24 zOUzc3E=EN+M854C`7j_-<(Wqk|4^jh3C4di2Yem=EB60h_4w~&j$}?7?h>TtJfH}k PS0V`R-fk?HZAX3s){xbV literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..740100fce28daaf85e717e6e2021b5ba9c30f4a8 GIT binary patch literal 5372 zcmeHLdpMM78-L#!GsZZUQy8r_VVy(HgmfSw#;MFABpcJ)H;qX-42!l}Qz}V0brRWz z4j85~bU_qfyT1Kr|1{V0&i%}NJ@5V8zx#a6QGegf zidbzd004?!TRa0`$_D@h6@!L%pO0$j!vrJ+Z1w=?Y}_FHLk`*!MgjnhHIfJP3eX>e znYegQKRiqk@B@E9H~7nFkxKTmk4{t?n!z;B%hMfy5FFucKgM%Y+4!Poq5S=%Qj8Hn zds=3|;^|f;vYDd(@WgQ-!zVI!YYs+*AtS_KvJjc_H@7WQDbmqX!D7rG+HQ1%h*UQt zJ%$QT4Qy@z5f`ZfkAy%O86kS<@RBu_Vq+;&mP)}=Vf?4tL@1~0*|cM;U?vScNEyz5X>#>iC1Cg;dhomB~Z2Vh;dm21%9D9+b>}8 zgf%}csBo%2fAQ@>IXenx*;fcnNWc$x0 zPevRbD_9s$6;=k$mo&LZWuy1h^`}0#>73lQGwGz@FkXda}lPJUMoMqL8DQ4c8 zV_N0#I>x9-oV>;k6$qvRcK1+Mz-TSXXJ5M(t#1Xq%%dHzor&Ktm*kuiZ7*GnIM;`NZvos^=b^;wmZyx&CK zWIBAsPwOk0Hf@CzeqRZpkq;F1RBy+f=j%ouxGQ@J@zY~kYy(@0f&+GT7j?K#UHEls z#4)mWZ$9{89ab>f(mTFVu*tUR$bGR;)3{JX9i|Qu**98#)A~plU{fcxQ4pn1bWj;u zeSyhJB5a;SF_bDs2l&PbPN=5g9e|y-z-T_O39wtB!I(TGpBRZyf=un}f`cTR*r+P(C$<<+f~e69k}}EpL1&`K(WmVz9<5gn zS4V2N-9>-Ed!BOktD7axwl%GZk~l+Y+o+PIV-IpW=o#I5EBsF&-s{b7+mn){TO+D{ z@)sbz8+xb3^7&6GEV3KfP8b|DtT1CvigP=hi(oLvD0gN51OEjVn`$`*}y~Ux0`%ie{&?y$75Gj%AG(NL4jtbZS9^wKUcVQ%*fde%pX~ z4S|!F3eu6+nGW?>o^P|LQG=C>wm-J;+JcpvMQQ%@>Mr%Lc|wTl8{-7J;c;F%#m=RC z9PfsjzlI2r7YBgqM%MwsF>Io?I>Q*Scks_>!2fV?a65Il?v0-qN$U%R{Kr&rqA3a$86L*%Stjt)QI!;& z2Z`jD1+HxsIAxz~%NyLYeJhmCWc$N`OGOr$z2}zb*Of#XXSJWTQtk2_V?G9Of^UnN zCah9h3jrX@_%&%iDvbrgr*#g`J#(vQEV()It|NVG7)JF$?!SXGPMtUO;?>UPt4#TH zN3mN`R_Y^!Rxlj)h_<4@_O>1@)9J<3OeuzpHV^fygfm>`bz+2WMxN0e1Q3U-%vqP%}7(NE+|gm?(7$+GLd z(QL!lP;_M>a-V3A4VCva6P95le-W;Fa8ei+_G{_iM4;lX039a9%t$_J#k#*ac%|CvmSfqwX;_j{e5*7D{e^X z2Uqixm60MxCBaZj?~6S^eIW{#;jiH#C)L>3s?~bSE_rOZ$GPAV9W8U!5gD9l1rnkj zKQKzk5+9A0_*p(tGelcPq8}?ab#}KNp71<0Y2v+RZ?@%H{!c`8qsrt{cL*I3av9HG z>%LA2*9&a!A>&w`*-e*U07PqfSVoy?)^e%FMKQFvlMgl2B(dadXBBS1m9y_zixvQy zgM1N2sM;zqgl3mH-3GH5HD9Lwh#HIg@=?1O%esyI1H}6A%(G3)5v7$*71Ip2^2~d~ z_S7o*^v#z+{%#FehFchBjZ|ZYcHDElqh?0uuu(gbi7^bH8dd0Sv;qq>f7#HxjBQAh z7{dGxAv|71;!VJAa{NMuKiMVvO?yE^ie}40Xmw*y*_8pWmq}*nYh+vlDgNU989u1P zoc%zE<~(f^lJU$GW6#RCrQ$8sm~_ga#C`cre@pe2I}`jis4&t{aIkd+oK;syO{o}Iq(VaL2TCZ&9|Y@i=e zh@GVO19bCQ-CQ(uAxts~#I8Cc)mW%ca{f`jt_;q?`1cKYeOArBtZ~{}I03c@(>B|e z+8e-zkR;+&cS(~$^0}@1&>o$ievUPaNE;{ok3@8T_Wu$w)e?%LDqcevLxCB?+dH?S z@MRTnDKowkj~Bqz@7>=Nseig2TFx8TFN_3bEX-F6s#Ir&s@>0TWF>N-nGNWV$;v>z zu3r3l_64)4xmvg?B&?IK=Si(r&=Xwj{QZLMli;Ktd%u;{O;y&zcV!n_xS`H>_a@&E3d@@`78!%)=@SR%2*qJSmq=+Nbb7O8u=e@r z6Pvl(MaEXj^y$`N(a=*z5h$JN`0bm#t5cIg$_J}aQbu@VN2&Ao=^=5yB`Zs2LRz1% zZNQ-qx0zPk(#;kbFA4MS#%Gw0x7mkT(&2(~EAQpr((e5DnZ30|*%f9TiDz6apxRh+HGGfG&y^4~~{Bmxu-lgUX?D zXz7H@i-8$Y#DT1`5`zco8gx*DCYnTGB67nJ7$M2N#F?$#s{M1Tw%nqty1%cxzxTcG zc<=Y?>SMt{zUFuvJOn}J{(e58K*}Hp&BdAkZSok&9tadC)Yl8D7gPGc4G!^(jqABVv23Fi4?^p(HO5$;F?PO)lnW{U0%f0s zlwMuui8MZeMzo?uW23CmyiSy{7n)}loA1R%)^I)jxQK0h2$mRRO2p!bE<9&EO4J9T zZBe4!YwK-28B|Z2y{CZK_tcOh5)-uG0SBboG*AN?qaS=EvA?5VY~m-_Dbou zAi&>X3XFRHg!8tnTMF~3Lrd~waKG*)D6zf*#1Xi}`Z79@ec@-Pn#K!HSj+RBzfH_c~jOHB+&Z9VW0{_Y0xEtw7!Vm-S3KK^pRnkf0=6Y`Tlmt&A~fu!w7sUC_~&qx@GnL1)n8-c?lcw=tjwU z0o%FbRR~Vy>_GRXf2w$X*+l%cl`FQDdtVN|T&`lmvcQI`#}7+XAoTV}SrOSD&S&&# zJQWD+JoKd&U14gMTh1h=q(AKu9)m`beM^wgUGSt4= z(Di*q9)Y|M(?3cVy3Y`rXInJeBKsfUyr3vE2PN)*V2q#s4p)e!gTd8~J}^ zcdhnxvQ`{BGTdPN5zf0aV#5-&kto-xO$y==|IK@92}+4&_smIS89)!OJE+8vb#sG^kK)}7V1ve;WRFxek5jW6b`%?T{1GH_+$4ZxL| zUPWoqIS7v&`B!;}IH_eZuohJB-_OnlR1H+u2a6VaO=P`H*u3!26k*r(wksdDoDb{` ze61E}%B>hRnIYr*bNOGXYbCbP5n7mJBce8$SrsAwl4$+n^^i$DZ^D zuE6!FvT17X%>8}9nn$*opGLwON*u+RiTyHM&VHlrQRe-RlWeM=SOh^jU1VI-ci_8r zxBlC7P#Tgf4X}b(Z*@PfN=IQRYnQU9wdOipe6{Rh_WS0ml75X^#Wa5_k>`0>>_s$A zj;-6Stu>XZhS5!Vu8i72&ORZ^^f!z4>`ciRDN6|OnIy$C#@2$9k2!wIeC|Y z$tlRXhb@OpnWI>I*wH@maAA;OWqua=f!j7ChN7s2b9g7y?e=1jR@(dhqhhi#&`f0$ z6y0*{iw@2k^}}Vl5$cX)QOplfET*gKVpGSUJQ6MM%nBY?k;b0y@R&&9_Mg_dz1_{t z?4n~SNbt8#*+!ro2a7W_gyeUY)|ZK$<#KX4!X`}{7bUfrAVO+}N_#&!@!%!}Nc?H{ zyK*-y&!pHL}nZ8W}JGhOvdhjUNuy+a@R zA&IjtspT0oc`b~{P*lwc`BY;2@sHG=yLL{2wwK{nw>3)fQZ zZay02F%fQ(L91EhR}OG&V&fs3Cp{DT4GLDbFt>j-lbjlD2EZv7!!>e=Zl=~e?{S%Z zJzJK~c{xOx4-IZaVXmVq-R5q`I}ztWF8G83bR~dqqgnX=S5})ceGOkglyQEk;2$C6 Nzdp#P-s{gt{s9X%OcMYA literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a60f63f145e052bbaf8713642c478c2d05e97afd GIT binary patch literal 5366 zcmeHJeK?bQAHR)dX!2es6CM@4@SH;1Oqi6AcS*5I^e8WLXU8aH80vJa7u1uyl~asN zCz*+J=1d(&Wh!d2vF&(SEJlu(3On~yr|WtCIoI>gb6wAKxv%TKzu$fL`~B|w^Zk6^ z&iZ({BGp%`0|0<@+wI~9NdW-BD2Qc{HgncsEhLmkey+{{gNqu5eyD-F_mcp?V4dPt za`Rg|4jEC$UAzMz34?CvQgVVG79XWT*T9IhhA5C!adW{3Br3h(i;2>0S}x)yxkq_` z#{3q)bo|R&!$IrAtI|>$-nOBTH}4sj7#o)qXRJzQgiW9x95!+n_h&?Bgg1Jf4mbRz z^eHoyeKqUai*c%1p}}&di5TIE;mr3|Gb{&hTOOXe9MWaU^(u zvJ_?|4b>H9b%LwCK3eXDmLj8E8uy*(94JR)E9KkI!gQ*Q9&eEZ=NIhb?A4g;oT6on z7d@SQ({Q-vb|kdYRNxJ#FU(MbUnL_`Bpvk|ZEL7i21Di29QELcr{zx=+M@?EK}Y@X z-wSn#M@ipC^G*f&yVV2vRIaQ9^7=zKfIAv zHjQU#(#h7YnK6FjjkHWqs=^XEPPwu2$E_NED)3-Q9+8V&c4f9tbd8Yb$t68{?5QMe zVz*m)AeQ5)n+sAq)1#vDbs*(jrltN!8%aU$aTYtty45y>n8bTV{mfou0hjQIX25Un z`jgP&la>(ke^MN?>R8L_XJdN>Hm`b{Unib-Q9ApQHu%pkFzhus3C{hslJp<%RxUEv zIG)ht%F1u&Jxy3huJW67{{*Jm`nCT*cxT8p&Md+#uFp*HXh`oBqrW0Wi?O4!_la-l zHN81Tn5n={$!gX+MTfYq$%9cdq%+oVmtcO;HB`-*hh%Hp&+u3g z2Oob7^^X?AWj%Kkl%YMz=&$Yi%zT}0)P-S(zQ8#D@3Cw(XC%`m9-XxPy~t5m+0~@; z35<04`@C1SH+rkIh$|OpGl=@T*qux%CeoOy2oX|1_j)*68ic6#qG&Ammr#S%WS68> z4b&t?G|w$Opc~~~P2|PS&^;%D*(=3aMc+i`n+a^TsJUV+S=_9DNd z&Ft2@aBMV)hW!Gg-z3@G#v#e7dsCGn1`zccsjW%>lDMD1*kw2zCFdNQ*+}JQvirAl zs-wLwj@r?BR6j@hU#D#V)}fNni5AQVjfG#(=0lY=Z=w{j(%q{rAF_(x6=&Bu;!Grdur=B=x*D!YOmKxtFcG z)2U`q|3*s+Hm0ORc^vnk!!s;*V-$6tS+tYywH1EU?o~0=(On;v<}EIbfEG1ysd#O_ zTBz_Ph2Qaqr6g!VV-M?at`1F6LZeu9d+D>Wo7(j9hQpKS;>dJ@X)Erf2}C?6G()#STGF>k3`dczbEoK8HDDA^xh3;9H`5UL`i4NjM~5tKyK`=sPKe z?bm-pmugu-JwO*J&Z<+4TAx2k;x%-C8NECG(guspw)nEFDm8K@N*0s>Qa52qY_me| z5U7Kjg*+oq45;82(M1_M?xA)~+^!+K8_$-N?paOTiC*XaqR^ess7cja$s3%Yh5fAxC}O72cte%t?fdfM~n|<-mR4mKQ3G&a{Nv(CPVw4MJ_M zIl~Ql5#v~6Vq7`|9r91zdE@oYBZ)70jO5vt1fTWD`Wx4DZbo|DjN$Pd)zUf!h+T8{ zodV%WBX-osQ0;fw!3tGyadE4~y2Y`wA-92n-^eTtSAXN9M&xAyft9v9^-$nH{6T{V-u? ziqE9P;BD6t$EkRV^s*e?cZ`zMy#av&eX=allp%K{UphIj8TXsE-Yu=pBB5as7-8^1 zg%;bi3~V+!DOva>j+pEzf`C8m|E&m(fHX~(6uS_rRaqjrKbm`JL5zDaKy3`HPmhb_ zy2X_6*|jP^m47H5>aZN3KRi7m1$dV7*rsQ|%pHF%jHq{2@{bb3hpg;K4(xcTc7}Vz4ak3R%_8cJknf?Nq}QVJl~u>*OG9iO)a6C z2p{Y!%vflylC8WzFRP zIPH{n2LMB(%k%)QriGRPVD(*09<2G55}X5T&ioRBP(jBz_^Cmg1dIpzi-0O^4!NfY*UN^hG znl^CGw@5y$U!OkbXVa0O4R5(>{e@_@Un%`DS=_;$A32fR@Lc`v=IBrx@@}w41f?#{ z(fwXzK!PZ(7pk9Tm~Wn;@F7ko;4>lmfa0W&6FKn~TgAWN1?~2`l3Lz{M3F~F{ahxB zhoGLa{4@te_$gOLQ@kHOR(wV%U#&A-5?^;8c{Wos}R1!_Z48F z;LHVCX#Vq?IOL(kFk){`mgOojAWKRXOdIpI`3v8dG$wv@?V?GhSF_?7VN26T-{q%U zL$`kkddq;UkHA}Z{!GE|D|CQJpd;%{N;cQ<0 zL#s8rAmkU24|WPO{)t!04A~0R)BI*l7i;KYE%0%|3a$UAw9ZYB=JU+5a0T21Z*f@A zL4$0_K^gG($Z^SqcB273Sp)a|AXiyM;B5eq)ZG~kIH07gE8i33Hqmiw_5pCx1M_qo z;EM6iE`#V>3@Z!00oXJfN`!Szd(7#HyA^a!Y)UcCD(nPOAC+{9|2R7 zmEqIFG2C_Whzz#8sHF;qX;Uax?|#^0oJ_Ks5P^#D;h;HtdfhQ34HaQ|i^06D-~8aQ zecdHoQ0yT;B*)xU?Bo!^RfH?gho4IAie(lOHu*I>hbh&Mtwq7~Gsj0ufCRx$bd;vp zPVhi)9EIo=s;7Hs6)I;i_W6%#TbKNr?JlWBI#l7#KeKe~X1%tSV~jo1dJ{*{@v6_- zp(q|~5VeN^7LS?88v6JgMEVdJ(7QaOu6rAe_FC`kmZhOiR2r7iWUDP3bW5B%8w??K z@W)iyW>^V0y5HPPkd8N{?Lf@rYgJK_u}RN&k;?joCsXfeJxW*&i3-UDv(+~F%$-zC zZf8O!fH$%{6-1}!y zfRE}TYZrzu9p#@FovHpZ^=-L1oV0r3+QtWsm~ulpFOSM(eOx99cNk7rh&zEX-0G3fH*f&Pd( zuT>By`w|eOhu5E%a?-CGz4NqgWpHj&^;syySi zRz^yC@i+6vgqtOyM&#MaGh_cQ5ug31M4W!WO#;m`5Ycl%vv%*@s_f3VhUi+slY9?G zE}~ed|3b0;1aahkxG0_v-G^RWNI+|6K5=rDvL{Tl~XsXr+_1#qJ1c?&0m5%puG zXfY64COkEO;)L6Yp>!8FVtJLf$KR8X7jMXATKfBJ4r~2d%l%l1DrhoOL zG0TGqKB3Qt))`9+&yk3{x80?q&1MS~%Z9(C&1Uk3#s(k93Q^J$hLd^r{K2_S%H{kz zm1V;N#JZ^y3zSUX(oEQrFBKyhN!ESvBj8s#*X;V z4qsV3ux$8x&3tR$I~w)Ky-`q#t*3RDQ|ZEBiSyF{{IcQW7eyz3S}bY3wGx#7VNe{p z;UyFREGKsYb~L70-9J@un#d zd)nfq$Oto5`tnzPG{0-!tV+J6ZDKB$cBgW>MTRC+`sL|JeixcX(Ye8HW-9f3ILBrk znT%&DWr*#-dO2c-;MI$=zKTDLSvQ`&^1`~pOgvgfj{`@hg*LMIxZlU*EYCEz^(5ro zHG~Xt!(Epw1@gfyzmG>wIJsCQiII2RZIDI!%}<&SpD6w~E;>zKus(248-$kdaZ8tG zHrVK_Ciu&Y$TLdm;$gatz7B^V*bFhtK`4hVwu`ud#kOLwi4YTiJ9C2$NkfN#!xHbu wd`A~YtV~{s#>(DU$&?i-SP{lQ@t7Ew4Wi`2#N(O$$iJ9?ql2q`iCrM`AKVxT(*OVf literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..461c786922265fff8c12723e1ea07b33420c9fd0 GIT binary patch literal 5325 zcmeHKeK?fq8XwbeULV6uzA7Umgi@!NgW2(&!PX3hrZkdDA3|@&M@i8b+CEI2SnIP7 zMbZvBr9sh*T8oT|G8)BXWg{bL7HhE2P;2-6b^bcnbwTZ+e%|}K@8@^_?&rSW z_p~>egjQLmfVSuMkKhwrQie}tL8@eT#a;S5H|ouO;0yxzat9oMNcDo-7#)HGcy!I z3o0Iq+ofi6FyIq*=MvR}#U(Y-#VJuArBhPr&_PVnaJ5cQ(vtRT*0r@i;yo&Nj~a{C zDWz6UO*UG0-ApmViEr;LS5zk1^0AWP<Qrvj`(3IfTT>jj?J} zX4n_8^LS?s`2^=`$pSlExGxc^)6_bMrrd4PL18G5(L33j_xK6vl(LA1ItexR4b@Rs zoDyz-zX==oJE%@M zb&R9>OVbU~ZH6{KFgc;26{2;n`+#AUzM zoipSoG*wLKxP#%~A24Uy*Yf@Ov5nF;I-f|Z+^xA(8{g4ZDjF%9Y%dybZ{5fZ&Lx*c zvT{vL0XD_K{h5m|z^t>$YqSR%84yV51KhgFbqz$i^NTYG0q;YL z?>9?9Q@x+kh%K4fh}}({vns% z`-9B;w+2AwC&M&x_sE7nQKW{K^eKQ}>PmV=mm%55wb2#TL2LJp@kS*v45y zIPFSLUU&2wRw=r?$5DC~&ioJ5=`BQpE0tRu8ST>*UqVc@+P+TFr=4ezXl)|9*b`$75yZ z;LMheC&RMQ60g=+{!rRL=-cb%Lu$WfbjpokBo;El33qPOTdhv^;LpdgvxZ3j!SS6M zD-+!>l46{C5+L06?{tw63&L2TUM@kYBUu*~p_nL0mlduB<_Zh*g*f~VhU;!%Yi}ySqW5a<9(ldafE>A|LgRN^k?b5s!H3_K)1q) zafDoU{vMU4+mT;AhnKN4oXuz3wg4#Iis4^^O16ROMO-g5KY4;6?{D;S&p8DmUEkk+ zTI97}F(kllM<};oXY7xM8z{-bI;{?&uE9_v&_%tw}KJU}x1Z)UVkAsx#CPMW=Tf%0q(X2uAAN zJOU}l-wU#W;{O)%4Ay>?u_};ZA5L(X_JZ!2ja_=%i|o{XUowcX(w)}VLyJ@H7=o=f zUCNNt5A}QQeU%i2b8ghiIM7$?Q!ho;xUvZ^0Gxmr34uTH&_+(TjlM1o56MHqDrLyl zVSPKO1~sF7x(hV^|IrxweFFGXn3-u)V|+v)AWQl0!O#n5Hjaly@g`=9k5w56zgE{z zP4szEk-3Cwd-h_30TQ+TBqgTN2#H$GTdHu<+X*(Rq@qy!7NJ}ec*#OC7L$j@K#C~c zU4)HJFy6^f3q${ANK1j&iC9oxh{k^*H+n{o%K8J5t#vab+1MJT?rBRGxNK6D6n5zJ zP=aif0y9UiOpmNpS=%*;ocP7j<37BDJCUA1$Z4J_=s}rrK}s`K`(uvk;cKQB3{Rg=(TsXc3W@^$`8QSua8KNWC ztm<6m<(hkFy>1mU@T3S_!V!99?R_-h=!=|nH@J5@Pnir~P6S&Jg!g7v%T(s&p^x~m zcuQO0SqtomUlo4OH!H|!UJiVnl6pwd40uL@E!f^or44+vM9s_1qVkdN=fPHvcm5w-X3dr=to)!D|K{6FcpboK+Ox23~6^6EYh8Pvd>S4^8 z#GbH3r&WerjL00x{Fed}WTs14F$4*e7L@;L(QvF7DTH|&t{Q>w2ZZNFvKz-`C-XOt Cb5WxJ literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..31df2f6199a0fd2493027f480dee0629af16e4fc GIT binary patch literal 7346 zcmeHM`&ZLf690Zf0!dgUh?@F>COJT&)iR1CwW#YC@7z%aZG!w8>}onge-(h~=O3lpN&V72Y@^&n6(qmzXg zHhVF8;V}t5CK#N*V=ZqZAOT+RhcAJD@4rLzOcP$>e0C0y9kX`z#{Kw^w(Gd|Ze3)& zb&5B7DI`eBSUp7DoLyPEOpK?eEDEOcGAw%A*bPfZnU|NBZJFi|L!Mc;hE!yG8b0~O z@zt+N=Ud_>9qIiyMJ@Z{matU^MbqiDdm(kh1)-CgT+4yca)wNrU$EK6-^yW<>TfOL z?cdB{CQg+!*N2*}Wf7$QvKQ-=HV)vRh!~6$6Peosh33nEEThQOM@nS6bO`lbAeYC9 zCVm3BdT;i#agax^hCjap^V}o0=0vM!wrwizP}cCNg7z)fD>pJp!h1cx8vJF_Im>%V znps?RD%0E&KvnR44I@Lrt_r^Vrs?q$ih_UV_~_$ekUc{ULq|at1dX+HBmIp*CxZ_J z-(2d}B;`{FGERlRTnYZFca2vTkinpS;QXFNVDRv4*^N>#aE}N#&Hw}5)s@zDfckmi z!;^p|aiL>d0M+KO@mGL;#!&Mfz>0fewugY4vQ-8tVAlD*EIuT%U_Gz#A|&!4jb610 z66w-L)P4YoEPb?FGn1>78{+fP=+AYkprCdMH@` zwt9jHi)`r`h)t)_xa!GYQYX7+Bi%RW@>`Qgpi?!@5!w3C78;+*bSfX<9Dk5Gd67kh z)wz|G=4ovd!?FwxzGXf0MoRf#^LNIhGFY)tWX(ccny8l&9vRHT>%2SmCxFh}8kXO) z4Uv`|Yc2Mi0j>-G_KByt22s@?{qq{)6nq&eH#s8$R@-mh>yh=`Dpz2$-J(p5lF?N6i0rS@QDCBqVo_)mSP9$bR`rWO$5kCXiGuMi zQBG_CyduJcK|wNUNM2P6&sQc@=2(6;eh6~mcdliAAZLzHlAsK@kM*^|Q0Im%g>Adx zxNN)AIB*u`e{n~i6AvzHZtq!f6(T%TZEN2J+nuUHT^=i*8r2$(^fSwWDLZHWCa< z7klNog29V*3q%V6$K1coM>z+VR}}^T_FFv*a{-HW3l1y;?9|JPb^&&?Q`npXc{&Li-+l#o&dXz4+5zk7 zjU(A8Zw-6oIp8*p(!vL965$?u(K=?8w+}CbW3ZhWi`M;j-zpudpW#FqDvOZar`m-S z*sKYEt_KWPHpJ+ZXs4_*6=*o_$pv#N+Mgn7HyW zFCU|{twb^wkdc|JmXjT!g>3R+V5lfk^(CN)~uGB)Ky< zVot4?KTs6v=f@Fal6AOxSIfu;U_rY(i`J&ytLukHMkk!mV~^7!#V}b(kXBgUR8}IM zxg=t}`;#kR!C@qEm_k+6DGx*mM|=5a1+$Q+eyVo5;PsS?GO?p7Gz59!D8zi*%;?gc z1KP^d?+E>6%E6p{@8VMMm$%~m1F5WLX-e8e%qzI%>Ua+HSSJp7Nk zMrNo?nUszC6^*fhPHSsC=La27+)TjLsiu=#PomcNnjlSP4HJ>grwoS7HT=hb8K^bF z{b%ry9C|HIa3vCj>ZsgEWOK=tEa>a>K%JB?5%UAb?$$;ln;cwSGw%Qab8UaeLT<*be1AyMI;k( z^!Ie2tx)*zIPYAOUxH>)-9{t*u?dm;M{hx8!R8 literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1824a25085daea323ef17c3fe66240e90f0026cf GIT binary patch literal 10115 zcmeHNX;f2L5>5y-5CoAeB8zkj1Bi+{0!D%&f{qCg1!OTut3gpgA%YMRM1zeWC@##f zMRAD^5I~4FiwUHqU3wrQn_xqd!68Njq96pa&3nO|Ioj=B9|axFqomgpHC1B1}DQ{TA_O10F(o3Ba?szo)ENSD~#E0Ee0O+mxq6JwU4k+`tFE0{C*8Yr)@^?`;WJ?SZz=-^Y7TGW=tqcfL)% zp$^HBf293Zth5dt6d##$u*=ESqU7}r#pCTRP^`eGo-lSJ}U2?QNK#qvA#Y>T#p5^!N zu|^LaX$*^fX~|OA-IbVAD)$ELCbuaJI3soOKMU`0Ljb$z3FxjoyyY9Tk=Ydi%?uli1$>Lg6Y3SG!F z`hL-4Q3oI#=c(wPcK%9Lb@zJ>>;f$EB2~|dG-g)a(`^_=a+bJ#3?w(P)AJc7|`rtHzN%r6LH2E02@N;cCR{n*kC&!f-*~O^5{eVmbULd{y*GuQlrVQOXC5FsUS>sjx3<<-oq(H&%I7j`SCYxj8T- zjaZn2b^C)iVK#b0t0y9dl{>}40)rU3I7Ovh;j_75%vrZtfKp(K3n~L!@ zEDz(Kv-3t8HZpp4w>Zn0rWpMT39Lloh67E~ADOWUM+eM`x#J%2Yw?>H(Q_brNyo93 zMHzT$q+_~?k?5$n{U|s2)5*x*;jRYfO*B49%SjlIZSDDhS?s!nsYa)xLD&7dr2G7* ze+)G++a8ec@kq{yb^upce6h0o?0j_PcRkDX=b7Se9$t=^k1k|aQn2ZKQ}v(a?;BLdWj{8x%Tzvx7h}8r-V?pEkp1 zG#qX{5zKe05e}X=CA*?)oWLXt!$u&z_;_3$0!WOmeN5-NX0?{7xn_)<oLUwqW!F95Fm&=oI1uFK=;T;6n->F6IQ36-i;BC5LVLopL% zaa1`u98wCb=I-|#szSN$8)dK<{?Bqr5&OB5AQ{`&dqqL8SgP2wsDzYLu*wFpd5w}p z5LL-IE4Ii2rAQS|;@lsm8qJ)=2xX`(CD*U0>n{bV!VZUEpg7Zhky|Bw7=9bIONL@o zg|fT`D}rrJTO|5^+2jf4k?RB}pbi2SfmljWuGN0+Z_ifQ-kzf%snfd-cX!=t0;@bg z?T1Vwj4fRsm+am#{i72WA;B;9laX`maJ;mo@%5Gr%j;#zT`>h>^E7bQ+$!Z02~jOWsG!@*5CkrLrP7e zqvc?Li%z-39Q484mFvfgI8mTtH@~{hbwSmD0iF9S-E}sjBxA~oj_kXhFC>{^&Y^Qa ze?4*Q3}3DTDrS3^m6Tq|*bDmZ<#T62eR|R~@zVYk1OAzAT{|^EPaCw!hF$*xD4sMl z8J*10ScZl3bebByVke`;*tJc!f;B)NhsO-MQCmR8ahGEl#5)?xkjbkA0}9X+N%=xU zmIi43z}5-&jKN_*aoRZ_PxLa4W%%X<`1lwP@I=Ga1#$PUYkLLHa<5SaSo_rsl{76|t_hPfZB*AGO3+znVkp`PoFI?U<+i=Mg4i?&zI>&W&o4= zQYoF^Tojs4xu@OErB7J0c@hPy&A`j{B9+2>hW@rJY1WqKTCl}gn&B$V80Sl>D7Aj; zC|mw4(IKcW>e$4nW0QLcd#^0q1LfNwAGZcUGi&_$X- z{jU(qJ4f|+m7uL>Y*+1UCoM{CN}j1#yF|!NB|HI@(^28!p4( zT18qJoHMBKMRpiFl(D?H3!|m2cMPeg9F9D;Jsoy70wYNXLg+&sLOz714YX3A-~h!# nXitH*Q7ChOlBNHZ{7^v1G_o*8lQ-S~|0U Date: Mon, 7 Aug 2017 18:01:12 +0800 Subject: [PATCH 06/26] Fixed typo `UIKIt` (#497) --- docs/_docs/subclassing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/subclassing.md b/docs/_docs/subclassing.md index 966d83144d..44c3260eed 100755 --- a/docs/_docs/subclassing.md +++ b/docs/_docs/subclassing.md @@ -19,7 +19,7 @@ The most important thing to remember is that your init method must be capable of ### `-didLoad` -This method is conceptually similar to UIViewController's `-viewDidLoad` method; it’s called once and is the point where the backing view has been loaded. It is guaranteed to be called on the **main thread** and is the appropriate place to do any UIKit things (such as adding gesture recognizers, touching the view / layer, initializing UIKIt objects). +This method is conceptually similar to UIViewController's `-viewDidLoad` method; it’s called once and is the point where the backing view has been loaded. It is guaranteed to be called on the **main thread** and is the appropriate place to do any UIKit things (such as adding gesture recognizers, touching the view / layer, initializing UIKit objects). ### `-layoutSpecThatFits:` From d2adb8f1260ed255bb96d345ede2c207c06cb096 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Mon, 7 Aug 2017 11:04:31 -0700 Subject: [PATCH 07/26] Rename -[ASCellNode viewModel] to -[ASCellNode nodeViewModel] to avoid collisions (#499) * Rename -[ASCellNode viewModel] -> -[ASCellNode nodeViewModel] to avoid breaking existing code * Update test * Update the changelog --- CHANGELOG.md | 1 + Source/ASCellNode.h | 2 +- Source/ASCellNode.mm | 2 +- Source/Details/ASCollectionElement.mm | 2 +- Tests/ASCollectionModernDataSourceTests.m | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f2f007cd..94b88bd11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) +- Rename ASCellNode.viewModel to ASCellNode.nodeViewModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#499](https://github.com/TextureGroup/Texture/pull/499) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASCellNode.h b/Source/ASCellNode.h index ca9dda6294..bcd5b9ccf3 100644 --- a/Source/ASCellNode.h +++ b/Source/ASCellNode.h @@ -123,7 +123,7 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { * * This property may be set off the main thread, but this method will never be invoked concurrently on the */ -@property (atomic, nullable) id viewModel; +@property (atomic, nullable) id nodeViewModel; /** * Asks the node whether it can be updated to the given view model. diff --git a/Source/ASCellNode.mm b/Source/ASCellNode.mm index e3d2d25e54..6ff4b7d836 100644 --- a/Source/ASCellNode.mm +++ b/Source/ASCellNode.mm @@ -172,7 +172,7 @@ - (BOOL)canUpdateToViewModel:(id)viewModel { - return [self.viewModel class] == [viewModel class]; + return [self.nodeViewModel class] == [viewModel class]; } - (NSIndexPath *)indexPath diff --git a/Source/Details/ASCollectionElement.mm b/Source/Details/ASCollectionElement.mm index 955cc5f119..3c16011da2 100644 --- a/Source/Details/ASCollectionElement.mm +++ b/Source/Details/ASCollectionElement.mm @@ -64,7 +64,7 @@ node.owningNode = _owningNode; node.collectionElement = self; ASTraitCollectionPropagateDown(node, _traitCollection); - node.viewModel = _viewModel; + node.nodeViewModel = _viewModel; _node = node; } return _node; diff --git a/Tests/ASCollectionModernDataSourceTests.m b/Tests/ASCollectionModernDataSourceTests.m index ec97e8c644..5fd096f198 100644 --- a/Tests/ASCollectionModernDataSourceTests.m +++ b/Tests/ASCollectionModernDataSourceTests.m @@ -254,7 +254,7 @@ id viewModel = viewModels[indexPath.item]; XCTAssertEqualObjects(viewModel, [collectionNode viewModelForItemAtIndexPath:indexPath]); ASCellNode *node = [collectionNode nodeForItemAtIndexPath:indexPath]; - XCTAssertEqualObjects(node.viewModel, viewModel); + XCTAssertEqualObjects(node.nodeViewModel, viewModel); } } } From fdc1f0468cbeaa984bfbd8168e2d3f23e5b9a667 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 8 Aug 2017 19:11:40 +0100 Subject: [PATCH 08/26] Improvements in ASCollectionGalleryLayoutDelegate (#496) * Improvements in ASCollectionGalleryLayoutDelegate - It now can handle section inset, as well as interitem and line spacings - Other small changes * Fix build failure and update file licenses * Update CHANGELOG * Minor change * Another assertion on scrollable directions of gallery layout delegate --- AsyncDisplayKit.xcodeproj/project.pbxproj | 20 ++- CHANGELOG.md | 2 +- Source/ASPagerNode.m | 6 +- .../Details/ASCollectionFlowLayoutDelegate.m | 5 +- .../ASCollectionGalleryLayoutDelegate.h | 54 ++++++- .../ASCollectionGalleryLayoutDelegate.m | 93 ------------ .../ASCollectionGalleryLayoutDelegate.mm | 140 ++++++++++++++++++ Source/Details/ASCollectionLayoutState.h | 6 +- Source/Details/ASCollectionLayoutState.mm | 60 +++++--- Source/Layout/ASLayout.mm | 2 +- .../Private/_ASCollectionGalleryLayoutInfo.h | 30 ++++ .../Private/_ASCollectionGalleryLayoutInfo.m | 72 +++++++++ .../ASCollectionView/Sample/ViewController.m | 6 +- 13 files changed, 364 insertions(+), 132 deletions(-) delete mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.m create mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.mm create mode 100644 Source/Private/_ASCollectionGalleryLayoutInfo.h create mode 100644 Source/Private/_ASCollectionGalleryLayoutInfo.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ba9df64e5f..c02b9fd526 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -429,9 +429,11 @@ E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; + E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */; }; E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; - E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; }; + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */; }; E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -458,7 +460,7 @@ E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */; }; E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */; }; + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; /* End PBXBuildFile section */ @@ -917,6 +919,8 @@ E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = ""; }; E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutInfo.h; sourceTree = ""; }; + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASCollectionGalleryLayoutInfo.m; sourceTree = ""; }; E5711A2A1C840C81009619D4 /* ASCollectionElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionElement.h; sourceTree = ""; }; E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionElement.mm; sourceTree = ""; }; E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutItem.h; sourceTree = ""; }; @@ -946,7 +950,7 @@ E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; sourceTree = ""; }; E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutState.mm; sourceTree = ""; }; E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionGalleryLayoutDelegate.h; sourceTree = ""; }; - E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeExtrasTests.m; sourceTree = ""; }; FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; @@ -1670,6 +1674,8 @@ E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */, + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */, E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, ); @@ -1687,7 +1693,7 @@ E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, - E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1824,6 +1830,8 @@ CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */, E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, + E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */, + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, @@ -1896,7 +1904,6 @@ 254C6B751BF94DF4003EC431 /* ASTextKitComponents.h in Headers */, B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */, CCA282CC1E9EB73E0037E8B7 /* ASTipNode.h in Headers */, - E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, 25E327571C16819500A2170C /* ASPagerNode.h in Headers */, CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, @@ -2251,6 +2258,7 @@ CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, + E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m in Sources */, 25E327591C16819500A2170C /* ASPagerNode.m in Sources */, 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */, B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, @@ -2286,7 +2294,7 @@ CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, - E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */, + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b88bd11a..f4bf0db81b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) -- Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) +- Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) [#496](https://github.com/TextureGroup/Texture/pull/496) - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index a5f84178c7..02666993e8 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -29,7 +29,7 @@ #import #import -@interface ASPagerNode () +@interface ASPagerNode () { __weak id _pagerDataSource; ASPagerNodeProxy *_proxyDataSource; @@ -75,7 +75,7 @@ ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionHorizontalDirections]; self = [super initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; if (self) { - layoutDelegate.sizeProvider = self; + layoutDelegate.propertiesProvider = self; } return self; } @@ -137,7 +137,7 @@ return indexPath.row; } -#pragma mark - ASCollectionGalleryLayoutSizeProviding +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding - (CGSize)sizeForElements:(ASElementMap *)elements { diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 79287a5944..548fa01816 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -76,8 +76,9 @@ ASSizeRange sizeRange = ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, context.scrollableDirections); ASLayout *layout = [stackSpec layoutThatFits:sizeRange]; - return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nonnull(ASLayout * _Nonnull sublayout) { - return ((ASCellNode *)sublayout.layoutElement).collectionElement; + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nullable(ASLayout * _Nonnull sublayout) { + ASCellNode *node = ASDynamicCast(sublayout.layoutElement, ASCellNode); + return node ? node.collectionElement : nil; }]; } diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h index d2d41b7882..dfd0f5c87d 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.h +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN -@protocol ASCollectionGalleryLayoutSizeProviding +@protocol ASCollectionGalleryLayoutPropertiesProviding /** * Returns the fixed size of each and every element. @@ -30,6 +30,51 @@ NS_ASSUME_NONNULL_BEGIN */ - (CGSize)sizeForElements:(ASElementMap *)elements; +@optional + +/** + * Returns the minumum spacing to use between lines of items. + * + * @discussion This method will only be called on main thread. + * + * @discussion For a vertically scrolling layout, this value represents the minimum spacing between rows. + * For a horizontally scrolling one, it represents the minimum spacing between columns. + * It is not applied between the first line and the header, or between the last line and the footer. + * This is the same behavior as UICollectionViewFlowLayout's minimumLineSpacing. + * + * @param elements All elements in the layout. + * + * @return The interitem spacing + */ +- (CGFloat)minimumLineSpacingForElements:(ASElementMap *)elements; + +/** + * Returns the minumum spacing to use between items in the same row or column, depending on the scroll directions. + * + * @discussion This method will only be called on main thread. + * + * @discussion For a vertically scrolling layout, this value represents the minimum spacing between items in the same row. + * For a horizontally scrolling one, it represents the minimum spacing between items in the same column. + * It is considered while fitting items into lines, but the actual final spacing between some items might be larger. + * This is the same behavior as UICollectionViewFlowLayout's minimumInteritemSpacing. + * + * @param elements All elements in the layout. + * + * @return The interitem spacing + */ +- (CGFloat)minimumInteritemSpacingForElements:(ASElementMap *)elements; + +/** + * Returns the margins of each section. + * + * @discussion This method will only be called on main thread. + * + * @param elements All elements in the layout. + * + * @return The margins used to layout content in a section + */ +- (UIEdgeInsets)sectionInsetForElements:(ASElementMap *)elements; + @end /** @@ -40,8 +85,13 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASCollectionGalleryLayoutDelegate : NSObject -@property (nonatomic, weak) id sizeProvider; +@property (nonatomic, weak) id propertiesProvider; +/** + * Designated initializer. + * + * @param scrollableDirections The scrollable directions of this layout. Must be either vertical or horizontal directions. + */ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER; - (instancetype)init __unavailable; diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m deleted file mode 100644 index 5ab5c23d22..0000000000 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ /dev/null @@ -1,93 +0,0 @@ -// -// ASCollectionGalleryLayoutDelegate.m -// Texture -// -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import - -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import - -#pragma mark - ASCollectionGalleryLayoutDelegate - -@implementation ASCollectionGalleryLayoutDelegate { - ASScrollDirection _scrollableDirections; - CGSize _itemSize; -} - -- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections -{ - self = [super init]; - if (self) { - _scrollableDirections = scrollableDirections; - } - return self; -} - -- (ASScrollDirection)scrollableDirections -{ - ASDisplayNodeAssertMainThread(); - return _scrollableDirections; -} - -- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements -{ - ASDisplayNodeAssertMainThread(); - if (_sizeProvider == nil) { - return nil; - } - - return [NSValue valueWithCGSize:[_sizeProvider sizeForElements:elements]]; -} - -+ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context -{ - ASElementMap *elements = context.elements; - CGSize pageSize = context.viewportSize; - ASScrollDirection scrollableDirections = context.scrollableDirections; - - CGSize itemSize = context.additionalInfo ? ((NSValue *)context.additionalInfo).CGSizeValue : CGSizeZero; - if (CGSizeEqualToSize(CGSizeZero, itemSize)) { - return [[ASCollectionLayoutState alloc] initWithContext:context]; - } - - NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, - ASCollectionElement *element, - [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); - if (children.count == 0) { - return [[ASCollectionLayoutState alloc] initWithContext:context]; - } - - // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element - ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal - spacing:0 - justifyContent:ASStackLayoutJustifyContentStart - alignItems:ASStackLayoutAlignItemsStart - flexWrap:ASStackLayoutFlexWrapWrap - alignContent:ASStackLayoutAlignContentStart - children:children]; - stackSpec.concurrent = YES; - ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; - - return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { - return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; - }]; -} - -@end diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm new file mode 100644 index 0000000000..a01fdf7f76 --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -0,0 +1,140 @@ +// +// ASCollectionGalleryLayoutDelegate.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - ASCollectionGalleryLayoutDelegate + +@implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; + + struct { + unsigned int minimumLineSpacingForElements:1; + unsigned int minimumInteritemSpacingForElements:1; + unsigned int sectionInsetForElements:1; + } _propertiesProviderFlags; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections +{ + self = [super init]; + if (self) { + // Scrollable directions must be either vertical or horizontal, but not both + ASDisplayNodeAssertTrue(ASScrollDirectionContainsVerticalDirection(scrollableDirections) + || ASScrollDirectionContainsHorizontalDirection(scrollableDirections)); + ASDisplayNodeAssertFalse(ASScrollDirectionContainsVerticalDirection(scrollableDirections) + && ASScrollDirectionContainsHorizontalDirection(scrollableDirections)); + _scrollableDirections = scrollableDirections; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertMainThread(); + return _scrollableDirections; +} + +- (void)setPropertiesProvider:(id)propertiesProvider +{ + ASDisplayNodeAssertMainThread(); + if (propertiesProvider == nil) { + _propertiesProvider = nil; + _propertiesProviderFlags = {}; + } else { + _propertiesProvider = propertiesProvider; + _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumLineSpacingForElements:)]; + _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumInteritemSpacingForElements:)]; + _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(sectionInsetForElements:)]; + } +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + id propertiesProvider = _propertiesProvider; + if (propertiesProvider == nil) { + return nil; + } + + CGSize itemSize = [propertiesProvider sizeForElements:elements]; + UIEdgeInsets sectionInset = _propertiesProviderFlags.sectionInsetForElements ? [propertiesProvider sectionInsetForElements:elements] : UIEdgeInsetsZero; + CGFloat lineSpacing = _propertiesProviderFlags.minimumLineSpacingForElements ? [propertiesProvider minimumLineSpacingForElements:elements] : 0.0; + CGFloat interitemSpacing = _propertiesProviderFlags.minimumInteritemSpacingForElements ? [propertiesProvider minimumInteritemSpacingForElements:elements] : 0.0; + return [[_ASCollectionGalleryLayoutInfo alloc] initWithItemSize:itemSize + minimumLineSpacing:lineSpacing + minimumInteritemSpacing:interitemSpacing + sectionInset:sectionInset]; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + CGSize pageSize = context.viewportSize; + ASScrollDirection scrollableDirections = context.scrollableDirections; + + _ASCollectionGalleryLayoutInfo *info = ASDynamicCast(context.additionalInfo, _ASCollectionGalleryLayoutInfo); + CGSize itemSize = info.itemSize; + if (info == nil || CGSizeEqualToSize(CGSizeZero, itemSize)) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + ASStackLayoutDirection stackDirection = ASScrollDirectionContainsVerticalDirection(scrollableDirections) + ? ASStackLayoutDirectionHorizontal + : ASStackLayoutDirectionVertical; + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:stackDirection + spacing:info.minimumInteritemSpacing + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + lineSpacing:info.minimumLineSpacing + children:children]; + stackSpec.concurrent = YES; + + ASLayoutSpec *finalSpec = stackSpec; + UIEdgeInsets sectionInset = info.sectionInset; + if (UIEdgeInsetsEqualToEdgeInsets(sectionInset, UIEdgeInsetsZero) == NO) { + finalSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:sectionInset child:stackSpec]; + } + + ASLayout *layout = [finalSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nullable(ASLayout * _Nonnull sublayout) { + _ASGalleryLayoutItem *item = ASDynamicCast(sublayout.layoutElement, _ASGalleryLayoutItem); + return item ? item.collectionElement : nil; + }]; +} + +@end diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index c21a37dff4..88eafc4da0 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN +typedef ASCollectionElement * _Nullable (^ASCollectionLayoutStateGetElementBlock)(ASLayout *); + @interface NSMapTable (ASCollectionLayoutConvenience) + (NSMapTable *)elementToLayoutAttributesTable; @@ -70,11 +72,11 @@ AS_SUBCLASSING_RESTRICTED * * @param layout The layout describes size and position of all elements. * - * @param getElementBlock A block that can retrieve the collection element from a direct sublayout of the root layout. + * @param getElementBlock A block that can retrieve the collection element from a sublayout of the root layout. */ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; + getElementBlock:(ASCollectionLayoutStateGetElementBlock)getElementBlock; /** * Returns all layout attributes present in this object. diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 81f003ab41..eb59edf0ca 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -20,9 +20,12 @@ #import #import #import +#import #import #import +#import + @implementation NSMapTable (ASCollectionLayoutConvenience) + (NSMapTable *)elementToLayoutAttributesTable @@ -48,30 +51,49 @@ elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock + getElementBlock:(ASCollectionLayoutStateGetElementBlock)getElementBlock { ASElementMap *elements = context.elements; NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; - - for (ASLayout *sublayout in layout.sublayouts) { - ASCollectionElement *element = getElementBlock(sublayout); - if (element == nil) { - ASDisplayNodeFailAssert(@"Element not found!"); - continue; + + // Traverse the given layout tree in breadth first fashion. Generate layout attributes for all included elements along the way. + struct Context { + ASLayout *layout; + CGPoint absolutePosition; + }; + + std::queue queue; + queue.push({layout, CGPointZero}); + + while (!queue.empty()) { + Context context = queue.front(); + queue.pop(); + + ASLayout *layout = context.layout; + const CGPoint absolutePosition = context.absolutePosition; + + ASCollectionElement *element = getElementBlock(layout); + if (element != nil) { + NSIndexPath *indexPath = [elements indexPathForElement:element]; + NSString *supplementaryElementKind = element.supplementaryElementKind; + + UICollectionViewLayoutAttributes *attrs; + if (supplementaryElementKind == nil) { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; + } else { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; + } + + CGRect frame = layout.frame; + frame.origin = absolutePosition; + attrs.frame = frame; + [table setObject:attrs forKey:element]; } - - NSIndexPath *indexPath = [elements indexPathForElement:element]; - NSString *supplementaryElementKind = element.supplementaryElementKind; - - UICollectionViewLayoutAttributes *attrs; - if (supplementaryElementKind == nil) { - attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; - } else { - attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; + + // Add all sublayouts to process in next step + for (ASLayout *sublayout in layout.sublayouts) { + queue.push({sublayout, absolutePosition + sublayout.position}); } - - attrs.frame = sublayout.frame; - [table setObject:attrs forKey:element]; } return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 82eb62a87d..22904b8b6b 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -258,7 +258,7 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( } else if (sublayoutsCount > 0){ std::vector sublayoutContexts; for (ASLayout *sublayout in sublayouts) { - sublayoutContexts.push_back({sublayout, context.absolutePosition + sublayout.position}); + sublayoutContexts.push_back({sublayout, absolutePosition + sublayout.position}); } queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end()); } diff --git a/Source/Private/_ASCollectionGalleryLayoutInfo.h b/Source/Private/_ASCollectionGalleryLayoutInfo.h new file mode 100644 index 0000000000..9fcb08c155 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutInfo.h @@ -0,0 +1,30 @@ +// +// _ASCollectionGalleryLayoutInfo.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface _ASCollectionGalleryLayoutInfo : NSObject + +// Read-only properties +@property (nonatomic, assign, readonly) CGSize itemSize; +@property (nonatomic, assign, readonly) CGFloat minimumLineSpacing; +@property (nonatomic, assign, readonly) CGFloat minimumInteritemSpacing; +@property (nonatomic, assign, readonly) UIEdgeInsets sectionInset; + +- (instancetype)initWithItemSize:(CGSize)itemSize + minimumLineSpacing:(CGFloat)minimumLineSpacing + minimumInteritemSpacing:(CGFloat)minimumInteritemSpacing + sectionInset:(UIEdgeInsets)sectionInset NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end diff --git a/Source/Private/_ASCollectionGalleryLayoutInfo.m b/Source/Private/_ASCollectionGalleryLayoutInfo.m new file mode 100644 index 0000000000..4dc9209d0b --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutInfo.m @@ -0,0 +1,72 @@ +// +// _ASCollectionGalleryLayoutInfo.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation _ASCollectionGalleryLayoutInfo + +- (instancetype)initWithItemSize:(CGSize)itemSize + minimumLineSpacing:(CGFloat)minimumLineSpacing + minimumInteritemSpacing:(CGFloat)minimumInteritemSpacing + sectionInset:(UIEdgeInsets)sectionInset +{ + self = [super init]; + if (self) { + _itemSize = itemSize; + _minimumLineSpacing = minimumLineSpacing; + _minimumInteritemSpacing = minimumInteritemSpacing; + _sectionInset = sectionInset; + } + return self; +} + +- (BOOL)isEqualToInfo:(_ASCollectionGalleryLayoutInfo *)info +{ + if (info == nil) { + return NO; + } + + return CGSizeEqualToSize(_itemSize, info.itemSize) + && _minimumLineSpacing == info.minimumLineSpacing + && _minimumInteritemSpacing == info.minimumInteritemSpacing + && UIEdgeInsetsEqualToEdgeInsets(_sectionInset, info.sectionInset); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[_ASCollectionGalleryLayoutInfo class]]) { + return NO; + } + return [self isEqualToInfo:other]; +} + +- (NSUInteger)hash +{ + struct { + CGSize itemSize; + CGFloat minimumLineSpacing; + CGFloat minimumInteritemSpacing; + UIEdgeInsets sectionInset; + } data = { + _itemSize, + _minimumLineSpacing, + _minimumInteritemSpacing, + _sectionInset, + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 36391e8f37..b895e9c9b0 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -23,7 +23,7 @@ #define ASYNC_COLLECTION_LAYOUT 0 -@interface ViewController () +@interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @property (nonatomic, strong) NSArray *data; @@ -48,7 +48,7 @@ #if ASYNC_COLLECTION_LAYOUT ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections]; - layoutDelegate.sizeProvider = self; + layoutDelegate.propertiesProvider = self; self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; #else UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; @@ -108,7 +108,7 @@ [self.collectionNode reloadData]; } -#pragma mark - ASCollectionGalleryLayoutSizeProviding +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding - (CGSize)sizeForElements:(ASElementMap *)elements { From dc3be352af1b057d49391c710b7bfa72b07a50ba Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 10 Aug 2017 17:47:26 +0100 Subject: [PATCH 09/26] [ASTableNode][ASCollectionNode] Add content offset bridging property (#460) - Add content offset bridging property to table and collection node - And use it in `ASCollectionLayout` to avoid measuring unrelated nodes during the first layout. - Update CHANGELOG and highlight deprecated methods --- CHANGELOG.md | 1 + Source/ASCollectionNode.h | 14 ++++++++ Source/ASCollectionNode.mm | 30 +++++++++++++++++ Source/ASCollectionView.h | 7 ++++ Source/ASTableNode.h | 14 ++++++++ Source/ASTableNode.mm | 32 +++++++++++++++++++ Source/ASTableView.h | 18 +++++++---- Source/Details/ASCollectionLayoutContext.h | 1 + Source/Details/ASCollectionLayoutContext.m | 7 ++-- Source/Private/ASCollectionLayout.mm | 6 ++-- .../ASCollectionLayoutContext+Private.h | 1 + .../Private/ASCollectionView+Undeprecated.h | 4 +++ Source/Private/ASTableView+Undeprecated.h | 32 +++++++++---------- Tests/ASPagerNodeTests.m | 17 +++++++--- Tests/ASTableViewTests.mm | 4 +-- 15 files changed, 154 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4bf0db81b..3751a2a201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) - Rename ASCellNode.viewModel to ASCellNode.nodeViewModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#499](https://github.com/TextureGroup/Texture/pull/499) +- [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 22f4ae6016..0ca893c2de 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -128,6 +128,20 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, weak) id layoutInspector; +/** + * The offset of the content view's origin from the collection node's origin. Defaults to CGPointZero. + */ +@property (nonatomic, assign) CGPoint contentOffset; + +/** + * Sets the offset from the content node’s origin to the collection node’s origin. + * + * @param contentOffset The offset + * + * @param animated YES to animate to this new offset at a constant velocity, NO to not aniamte and immediately make the transition. + */ +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + /** * Tuning parameters for a range type in full mode. * diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 81c9a907a8..b867a0ea9e 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -49,6 +49,8 @@ @property (nonatomic, assign) BOOL usesSynchronousDataLoading; @property (nonatomic, assign) CGFloat leadingScreensForBatching; @property (weak, nonatomic) id layoutInspector; +@property (nonatomic, assign) CGPoint contentOffset; +@property (nonatomic, assign) BOOL animatesContentOffset; @end @implementation _ASCollectionPendingState @@ -61,6 +63,8 @@ _allowsSelection = YES; _allowsMultipleSelection = NO; _inverted = NO; + _contentOffset = CGPointZero; + _animatesContentOffset = NO; } return self; } @@ -189,6 +193,8 @@ if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } + + [view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset]; // Don't need to set collectionViewLayout to the view as the layout was already used to init the view in view block. } @@ -434,6 +440,30 @@ } } +- (void)setContentOffset:(CGPoint)contentOffset +{ + [self setContentOffset:contentOffset animated:NO]; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated +{ + if ([self pendingState]) { + _pendingState.contentOffset = contentOffset; + _pendingState.animatesContentOffset = animated; + } else { + [self.view setContentOffset:contentOffset animated:animated]; + } +} + +- (CGPoint)contentOffset +{ + if ([self pendingState]) { + return _pendingState.contentOffset; + } else { + return self.view.contentOffset; + } +} + - (ASScrollDirection)scrollDirection { return [self isNodeLoaded] ? self.view.scrollDirection : ASScrollDirectionNone; diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index b2f015cf57..f4dd891385 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -141,6 +141,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) BOOL zeroContentInsets ASDISPLAYNODE_DEPRECATED_MSG("Set automaticallyAdjustsScrollViewInsets=NO on your view controller instead."); +/** + * The point at which the origin of the content view is offset from the origin of the collection view. + */ +@property (nonatomic, assign) CGPoint contentOffset ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead."); + /** * The object that acts as the asynchronous delegate of the collection view * @@ -407,6 +412,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSArray<__kindof ASCellNode *> *)visibleNodes AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); + @end ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASCollectionDataSource.") diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 59fb7183e6..20d59d2e34 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -55,6 +55,20 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL inverted; +/** + * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. + */ +@property (nonatomic, assign) CGPoint contentOffset; + +/** + * Sets the offset from the content node’s origin to the table node’s origin. + * + * @param contentOffset The offset + * + * @param animated YES to animate to this new offset at a constant velocity, NO to not aniamte and immediately make the transition. + */ +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + /** * YES to automatically adjust the contentOffset when cells are inserted or deleted above * visible cells, maintaining the users' visible scroll position. diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index a6e7d8e978..4cd7fd923d 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -43,6 +43,8 @@ @property (nonatomic, assign) BOOL allowsMultipleSelectionDuringEditing; @property (nonatomic, assign) BOOL inverted; @property (nonatomic, assign) CGFloat leadingScreensForBatching; +@property (nonatomic, assign) CGPoint contentOffset; +@property (nonatomic, assign) BOOL animatesContentOffset; @property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; @end @@ -58,6 +60,8 @@ _allowsMultipleSelectionDuringEditing = NO; _inverted = NO; _leadingScreensForBatching = 2; + _contentOffset = CGPointZero; + _animatesContentOffset = NO; _automaticallyAdjustsContentOffset = NO; } return self; @@ -120,6 +124,7 @@ if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } + [view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset]; } } @@ -232,6 +237,33 @@ } } +- (void)setContentOffset:(CGPoint)contentOffset +{ + [self setContentOffset:contentOffset animated:NO]; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.contentOffset = contentOffset; + pendingState.animatesContentOffset = animated; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + [self.view setContentOffset:contentOffset animated:animated]; + } +} + +- (CGPoint)contentOffset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.contentOffset; + } else { + return self.view.contentOffset; + } +} + - (void)setAutomaticallyAdjustsContentOffset:(BOOL)automaticallyAdjustsContentOffset { _ASTablePendingState *pendingState = self.pendingState; diff --git a/Source/ASTableView.h b/Source/ASTableView.h index eb0c92c984..5877ab4d6d 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -67,6 +67,10 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGFloat leadingScreensForBatching ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); +/** + * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. + */ +@property (nonatomic, assign) CGPoint contentOffset ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); /** * YES to automatically adjust the contentOffset when cells are inserted or deleted above @@ -84,6 +88,12 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL inverted ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); +@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedRows ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + +@property (nonatomic, readonly, nullable) NSArray *indexPathsForVisibleRows ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); + /** * Tuning parameters for a range type in full mode. * @@ -138,12 +148,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); -@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); - -@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedRows ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); - -@property (nonatomic, readonly, nullable) NSArray *indexPathsForVisibleRows ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); - - (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); - (nullable NSArray *)indexPathsForRowsInRect:(CGRect)rect ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); @@ -241,6 +245,8 @@ NS_ASSUME_NONNULL_BEGIN /// Deprecated in 2.0. You should not call this method. - (void)clearFetchedData ASDISPLAYNODE_DEPRECATED_MSG("You should not call this method directly. Intead, rely on the Interstate State callback methods."); +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); + @end ASDISPLAYNODE_DEPRECATED_MSG("Renamed to ASTableDataSource.") diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index a20873a81d..5c2fdbcab3 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -27,6 +27,7 @@ AS_SUBCLASSING_RESTRICTED @interface ASCollectionLayoutContext : NSObject @property (nonatomic, assign, readonly) CGSize viewportSize; +@property (nonatomic, assign, readonly) CGPoint initialContentOffset; @property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; @property (nonatomic, weak, readonly) ASElementMap *elements; @property (nonatomic, strong, readonly, nullable) id additionalInfo; diff --git a/Source/Details/ASCollectionLayoutContext.m b/Source/Details/ASCollectionLayoutContext.m index eff48afb64..06dcd08f96 100644 --- a/Source/Details/ASCollectionLayoutContext.m +++ b/Source/Details/ASCollectionLayoutContext.m @@ -22,13 +22,11 @@ @implementation ASCollectionLayoutContext { Class _layoutDelegateClass; - - // This ivar doesn't directly involve in the layout calculation process, i.e contexts can be equal regardless of the layout caches. - // As a result, this ivar is ignored in -isEqualToContext: and -hash. __weak ASCollectionLayoutCache *_layoutCache; } - (instancetype)initWithViewportSize:(CGSize)viewportSize + initialContentOffset:(CGPoint)initialContentOffset scrollableDirections:(ASScrollDirection)scrollableDirections elements:(ASElementMap *)elements layoutDelegateClass:(Class)layoutDelegateClass @@ -38,6 +36,7 @@ self = [super init]; if (self) { _viewportSize = viewportSize; + _initialContentOffset = initialContentOffset; _scrollableDirections = scrollableDirections; _elements = elements; _layoutDelegateClass = layoutDelegateClass; @@ -57,6 +56,8 @@ return _layoutCache; } +// NOTE: Some properties, like initialContentOffset and layoutCache are ignored in -isEqualToContext: and -hash. +// That is because contexts can be equal regardless of the content offsets or layout caches. - (BOOL)isEqualToContext:(ASCollectionLayoutContext *)context { if (context == nil) { diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 2ecc312c78..e8ade9feba 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -70,11 +70,13 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh { ASDisplayNodeAssertMainThread(); CGSize viewportSize = [self _viewportSize]; + CGPoint contentOffset = _collectionNode.contentOffset; id additionalInfo = nil; if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize + initialContentOffset:contentOffset scrollableDirections:[_layoutDelegate scrollableDirections] elements:elements layoutDelegateClass:[_layoutDelegate class] @@ -93,8 +95,8 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly CGSize viewportSize = context.viewportSize; - // TODO Consider content offset of the collection node - CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); + CGPoint contentOffset = context.initialContentOffset; + CGRect initialRect = CGRectMake(contentOffset.x, contentOffset.y, viewportSize.width, viewportSize.height); CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, context.scrollableDirections, diff --git a/Source/Private/ASCollectionLayoutContext+Private.h b/Source/Private/ASCollectionLayoutContext+Private.h index 3c615aef27..0c6686e51a 100644 --- a/Source/Private/ASCollectionLayoutContext+Private.h +++ b/Source/Private/ASCollectionLayoutContext+Private.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak, readonly) ASCollectionLayoutCache *layoutCache; - (instancetype)initWithViewportSize:(CGSize)viewportSize + initialContentOffset:(CGPoint)initialContentOffset scrollableDirections:(ASScrollDirection)scrollableDirections elements:(ASElementMap *)elements layoutDelegateClass:(Class)layoutDelegateClass diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index be3a3f1a0b..811ba7bafa 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -75,6 +75,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id layoutInspector; +@property (nonatomic, assign) CGPoint contentOffset; + /** * Tuning parameters for a range type in full mode. * @@ -292,6 +294,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASTableView+Undeprecated.h b/Source/Private/ASTableView+Undeprecated.h index f7d0d2a264..70098940fe 100644 --- a/Source/Private/ASTableView+Undeprecated.h +++ b/Source/Private/ASTableView+Undeprecated.h @@ -33,6 +33,19 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id asyncDelegate; @property (nonatomic, weak) id asyncDataSource; +@property (nonatomic, assign) CGPoint contentOffset; +@property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; +@property (nonatomic, assign) BOOL inverted; +@property (nonatomic, readonly, nullable) NSArray *indexPathsForVisibleRows; +@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedRows; +@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow; + +/** + * The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called. + * + * Defaults to two screenfuls. + */ +@property (nonatomic, assign) CGFloat leadingScreensForBatching; /** * Initializer. @@ -44,10 +57,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style; -@property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; - -@property (nonatomic, assign) BOOL inverted; - /** * Tuning parameters for a range type in full mode. * @@ -109,12 +118,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition; -@property (nonatomic, readonly, nullable) NSArray *indexPathsForVisibleRows; - -@property (nonatomic, readonly, nullable) NSArray *indexPathsForSelectedRows; - -@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow; - - (nullable NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point; - (nullable NSArray *)indexPathsForRowsInRect:(CGRect)rect; @@ -135,13 +138,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; -/** - * The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called. - * - * Defaults to two screenfuls. - */ -@property (nonatomic, assign) CGFloat leadingScreensForBatching; - /** * Reload everything from scratch, destroying the working range and all cached nodes. * @@ -311,5 +307,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; + @end NS_ASSUME_NONNULL_END diff --git a/Tests/ASPagerNodeTests.m b/Tests/ASPagerNodeTests.m index 428247c534..4b45a8c1ee 100644 --- a/Tests/ASPagerNodeTests.m +++ b/Tests/ASPagerNodeTests.m @@ -2,8 +2,17 @@ // ASPagerNodeTests.m // Texture // -// Created by Luke Parham on 11/6/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 /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import @@ -128,7 +137,7 @@ #pragma clang diagnostic pop XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); - XCTAssertEqual(pagerNode.view.contentOffset.y, 0); + XCTAssertEqual(pagerNode.contentOffset.y, 0); XCTAssertEqual(pagerNode.view.contentInset.top, 0); e = [self expectationWithDescription:@"Transition completed"]; @@ -158,7 +167,7 @@ #pragma clang diagnostic pop XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); - XCTAssertEqual(pagerNode.view.contentOffset.y, 0); + XCTAssertEqual(pagerNode.contentOffset.y, 0); XCTAssertEqual(pagerNode.view.contentInset.top, 0); } diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index 6c53fc8498..e7388ccf78 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -815,7 +815,7 @@ [node waitUntilAllUpdatesAreCommitted]; CGFloat rowHeight = [node.view rectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]].size.height; // Scroll to row (0,1) + 10pt - node.view.contentOffset = CGPointMake(0, rowHeight + 10); + node.contentOffset = CGPointMake(0, rowHeight + 10); [node performBatchAnimated:NO updates:^{ // Delete row 0 from all sections. @@ -829,7 +829,7 @@ // Now that row (0,0) is deleted, we should have slid up to be at just 10 // i.e. we should have subtracted the deleted row height from our content offset. - XCTAssertEqual(node.view.contentOffset.y, 10); + XCTAssertEqual(node.contentOffset.y, 10); } @end From f58b0b3cd384bee69cc9136b4985440ebc12c0c4 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 10 Aug 2017 16:11:12 -0700 Subject: [PATCH 10/26] Rename the field again to nodeModel (#504) * Rename the field to nodeModel * A few more instances * Fix method name --- CHANGELOG.md | 2 +- Source/ASCellNode.h | 6 +- Source/ASCellNode.mm | 4 +- Source/ASCollectionNode.h | 8 +-- Source/ASCollectionNode.mm | 4 +- Source/ASCollectionView.mm | 10 ++-- Source/ASTableView.mm | 2 +- Source/Details/ASCollectionElement.h | 4 +- Source/Details/ASCollectionElement.mm | 6 +- Source/Details/ASDataController.h | 2 +- Source/Details/ASDataController.mm | 12 ++-- Tests/ASCollectionModernDataSourceTests.m | 72 +++++++++++------------ 12 files changed, 66 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3751a2a201..137725629c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) -- Rename ASCellNode.viewModel to ASCellNode.nodeViewModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#499](https://github.com/TextureGroup/Texture/pull/499) +- Rename ASCellNode.viewModel to ASCellNode.nodeModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#504](https://github.com/TextureGroup/Texture/pull/504) - [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) ##2.3.5 diff --git a/Source/ASCellNode.h b/Source/ASCellNode.h index bcd5b9ccf3..2c911f17ae 100644 --- a/Source/ASCellNode.h +++ b/Source/ASCellNode.h @@ -123,14 +123,14 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { * * This property may be set off the main thread, but this method will never be invoked concurrently on the */ -@property (atomic, nullable) id nodeViewModel; +@property (atomic, nullable) id nodeModel; /** - * Asks the node whether it can be updated to the given view model. + * Asks the node whether it can be updated to the given node model. * * The default implementation returns YES if the class matches that of the current view-model. */ -- (BOOL)canUpdateToViewModel:(id)viewModel; +- (BOOL)canUpdateToNodeModel:(id)nodeModel; /** * The backing view controller, or @c nil if the node wasn't initialized with backing view controller diff --git a/Source/ASCellNode.mm b/Source/ASCellNode.mm index 6ff4b7d836..400be07968 100644 --- a/Source/ASCellNode.mm +++ b/Source/ASCellNode.mm @@ -170,9 +170,9 @@ } } -- (BOOL)canUpdateToViewModel:(id)viewModel +- (BOOL)canUpdateToNodeModel:(id)nodeModel { - return [self.nodeViewModel class] == [viewModel class]; + return [self.nodeModel class] == [nodeModel class]; } - (NSIndexPath *)indexPath diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 0ca893c2de..9ba6fdcee1 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -433,15 +433,15 @@ NS_ASSUME_NONNULL_BEGIN - (nullable __kindof ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; /** - * Retrieves the view-model for the item at the given index path, if any. + * Retrieves the node-model for the item at the given index path, if any. * * @param indexPath The index path of the requested item. * - * @return The view-model for the given item, or @c nil if no item exists at the specified path or no view-model was provided. + * @return The node-model for the given item, or @c nil if no item exists at the specified path or no node-model was provided. * * @warning This API is beta and subject to change. We'll try to provide an easy migration path. */ -- (nullable id)viewModelForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; +- (nullable id)nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT; /** * Retrieve the index path for the item with the given node. @@ -537,7 +537,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An object that contains all the data for this item. */ -- (nullable id)collectionNode:(ASCollectionNode *)collectionNode viewModelForItemAtIndexPath:(NSIndexPath *)indexPath; +- (nullable id)collectionNode:(ASCollectionNode *)collectionNode nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath; /** * Similar to -collectionNode:nodeForItemAtIndexPath: diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index b867a0ea9e..994e261886 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -624,10 +624,10 @@ return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].node; } -- (id)viewModelForItemAtIndexPath:(NSIndexPath *)indexPath +- (id)nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath { [self reloadDataInitiallyIfNeeded]; - return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].viewModel; + return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].nodeModel; } - (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index da6eadcdc4..83bcc2447b 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -202,7 +202,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; unsigned int collectionViewNumberOfItemsInSection:1; unsigned int collectionNodeNodeForItem:1; unsigned int collectionNodeNodeBlockForItem:1; - unsigned int viewModelForItem:1; + unsigned int nodeModelForItem:1; unsigned int collectionNodeNodeForSupplementaryElement:1; unsigned int collectionNodeNodeBlockForSupplementaryElement:1; unsigned int collectionNodeSupplementaryElementKindsInSection:1; @@ -431,7 +431,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForSupplementaryElementOfKind:atIndexPath:)]; _asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForSupplementaryElementOfKind:atIndexPath:)]; _asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)]; - _asyncDataSourceFlags.viewModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:viewModelForItemAtIndexPath:)]; + _asyncDataSourceFlags.nodeModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeModelForItemAtIndexPath:)]; _asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)]; if (_asyncDataSourceFlags.interop) { @@ -1662,14 +1662,14 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section #pragma mark - ASDataControllerSource -- (id)dataController:(ASDataController *)dataController viewModelForItemAtIndexPath:(NSIndexPath *)indexPath +- (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath { - if (!_asyncDataSourceFlags.viewModelForItem) { + if (!_asyncDataSourceFlags.nodeModelForItem) { return nil; } GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); - return [_asyncDataSource collectionNode:collectionNode viewModelForItemAtIndexPath:indexPath]; + return [_asyncDataSource collectionNode:collectionNode nodeModelForItemAtIndexPath:indexPath]; } - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 3f230ba40d..d0e529c8bb 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -1628,7 +1628,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; #pragma mark - ASDataControllerSource -- (id)dataController:(ASDataController *)dataController viewModelForItemAtIndexPath:(NSIndexPath *)indexPath +- (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath { // Not currently supported for tables. Will be added when the collection API stabilizes. return nil; diff --git a/Source/Details/ASCollectionElement.h b/Source/Details/ASCollectionElement.h index 252d394331..0272aae261 100644 --- a/Source/Details/ASCollectionElement.h +++ b/Source/Details/ASCollectionElement.h @@ -30,9 +30,9 @@ AS_SUBCLASSING_RESTRICTED @property (nonatomic, assign) ASSizeRange constrainedSize; @property (nonatomic, readonly, weak) id owningNode; @property (nonatomic, assign) ASPrimitiveTraitCollection traitCollection; -@property (nonatomic, readonly, nullable) id viewModel; +@property (nonatomic, readonly, nullable) id nodeModel; -- (instancetype)initWithViewModel:(nullable id)viewModel +- (instancetype)initWithNodeModel:(nullable id)nodeModel nodeBlock:(ASCellNodeBlock)nodeBlock supplementaryElementKind:(nullable NSString *)supplementaryElementKind constrainedSize:(ASSizeRange)constrainedSize diff --git a/Source/Details/ASCollectionElement.mm b/Source/Details/ASCollectionElement.mm index 3c16011da2..75f75e9182 100644 --- a/Source/Details/ASCollectionElement.mm +++ b/Source/Details/ASCollectionElement.mm @@ -31,7 +31,7 @@ ASCellNode *_node; } -- (instancetype)initWithViewModel:(id)viewModel +- (instancetype)initWithNodeModel:(id)nodeModel nodeBlock:(ASCellNodeBlock)nodeBlock supplementaryElementKind:(NSString *)supplementaryElementKind constrainedSize:(ASSizeRange)constrainedSize @@ -41,7 +41,7 @@ NSAssert(nodeBlock != nil, @"Node block must not be nil"); self = [super init]; if (self) { - _viewModel = viewModel; + _nodeModel = nodeModel; _nodeBlock = nodeBlock; _supplementaryElementKind = [supplementaryElementKind copy]; _constrainedSize = constrainedSize; @@ -64,7 +64,7 @@ node.owningNode = _owningNode; node.collectionElement = self; ASTraitCollectionPropagateDown(node, _traitCollection); - node.nodeViewModel = _viewModel; + node.nodeModel = _nodeModel; _node = node; } return _node; diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 6fd85e89ae..82791a685d 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -78,7 +78,7 @@ extern NSString * const ASCollectionInvalidUpdateException; */ - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElement:(ASCollectionElement *)element matchesSize:(CGSize)size; -- (nullable id)dataController:(ASDataController *)dataController viewModelForItemAtIndexPath:(NSIndexPath *)indexPath; +- (nullable id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath; @optional diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 9e9b9348c2..102fca1239 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -330,18 +330,18 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; id node = self.node; for (NSIndexPath *indexPath in indexPaths) { ASCellNodeBlock nodeBlock; - id viewModel; + id nodeModel; if (isRowKind) { - viewModel = [dataSource dataController:self viewModelForItemAtIndexPath:indexPath]; + nodeModel = [dataSource dataController:self nodeModelForItemAtIndexPath:indexPath]; // Get the prior element and attempt to update the existing cell node. - if (viewModel != nil && !changeSet.includesReloadData) { + if (nodeModel != nil && !changeSet.includesReloadData) { NSIndexPath *oldIndexPath = [changeSet oldIndexPathForNewIndexPath:indexPath]; if (oldIndexPath != nil) { ASCollectionElement *oldElement = [previousMap elementForItemAtIndexPath:oldIndexPath]; ASCellNode *oldNode = oldElement.node; - if ([oldNode canUpdateToViewModel:viewModel]) { - // Just wrap the node in a block. The collection element will -setViewModel: + if ([oldNode canUpdateToNodeModel:nodeModel]) { + // Just wrap the node in a block. The collection element will -setNodeModel: nodeBlock = ^{ return oldNode; }; @@ -360,7 +360,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; } - ASCollectionElement *element = [[ASCollectionElement alloc] initWithViewModel:viewModel + ASCollectionElement *element = [[ASCollectionElement alloc] initWithNodeModel:nodeModel nodeBlock:nodeBlock supplementaryElementKind:isRowKind ? nil : kind constrainedSize:constrainedSize diff --git a/Tests/ASCollectionModernDataSourceTests.m b/Tests/ASCollectionModernDataSourceTests.m index 5fd096f198..a6daa9782e 100644 --- a/Tests/ASCollectionModernDataSourceTests.m +++ b/Tests/ASCollectionModernDataSourceTests.m @@ -24,7 +24,7 @@ @end @interface ASTestSection : NSObject -@property (nonatomic, readonly) NSMutableArray *viewModels; +@property (nonatomic, readonly) NSMutableArray *nodeModels; @end @implementation ASCollectionModernDataSourceTests { @@ -41,10 +41,10 @@ // Default is 2 sections: 2 items in first, 1 item in second. sections = [NSMutableArray array]; [sections addObject:[ASTestSection new]]; - [sections[0].viewModels addObject:[NSObject new]]; - [sections[0].viewModels addObject:[NSObject new]]; + [sections[0].nodeModels addObject:[NSObject new]]; + [sections[0].nodeModels addObject:[NSObject new]]; [sections addObject:[ASTestSection new]]; - [sections[1].viewModels addObject:[NSObject new]]; + [sections[1].nodeModels addObject:[NSObject new]]; window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; viewController = [[UIViewController alloc] init]; @@ -60,7 +60,7 @@ @selector(numberOfSectionsInCollectionNode:), @selector(collectionNode:numberOfItemsInSection:), @selector(collectionNode:nodeBlockForItemAtIndexPath:), - @selector(collectionNode:viewModelForItemAtIndexPath:), + @selector(collectionNode:nodeModelForItemAtIndexPath:), @selector(collectionNode:contextForSection:), nil]; [mockDataSource setExpectationOrderMatters:YES]; @@ -112,7 +112,7 @@ skippedReloadIndexPaths:nil]; } -- (void)testReloadingAnItemWithACompatibleViewModel +- (void)testReloadingAnItemWithACompatibleNodeModel { [self loadInitialData]; @@ -120,15 +120,15 @@ NSIndexPath *reloadedPath = [NSIndexPath indexPathForItem:1 inSection:0]; NSIndexPath *deletedPath = [NSIndexPath indexPathForItem:0 inSection:0]; - id viewModel = [NSObject new]; + id nodeModel = [NSObject new]; - // Cell node should get -canUpdateToViewModel: + // Cell node should get -canUpdateToNodeModel: id mockCellNode = [collectionNode nodeForItemAtIndexPath:reloadedPath]; - OCMExpect([mockCellNode canUpdateToViewModel:viewModel]) + OCMExpect([mockCellNode canUpdateToNodeModel:nodeModel]) .andReturn(YES); [self performUpdateReloadingSections:nil - reloadingItems:@{ reloadedPath: viewModel } + reloadingItems:@{ reloadedPath: nodeModel } reloadMappings:@{ reloadedPath: [NSIndexPath indexPathForItem:0 inSection:0] } insertingItems:nil deletingItems:@[ deletedPath ] @@ -168,12 +168,12 @@ // It reads the contents for each item. for (NSInteger section = 0; section < sections.count; section++) { - NSArray *viewModels = sections[section].viewModels; + NSArray *nodeModels = sections[section].nodeModels; // For each item: - for (NSInteger i = 0; i < viewModels.count; i++) { + for (NSInteger i = 0; i < nodeModels.count; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; - [self expectViewModelMethodForItemAtIndexPath:indexPath viewModel:viewModels[i]]; + [self expectNodeModelMethodForItemAtIndexPath:indexPath nodeModel:nodeModels[i]]; [self expectNodeBlockMethodForItemAtIndexPath:indexPath]; } } @@ -201,14 +201,14 @@ // Note: Skip fast enumeration for readability. for (NSInteger section = 0; section < sections.count; section++) { OCMExpect([mockDataSource collectionNode:collectionNode numberOfItemsInSection:section]) - .andReturn(sections[section].viewModels.count); + .andReturn(sections[section].nodeModels.count); } } -- (void)expectViewModelMethodForItemAtIndexPath:(NSIndexPath *)indexPath viewModel:(id)viewModel +- (void)expectNodeModelMethodForItemAtIndexPath:(NSIndexPath *)indexPath nodeModel:(id)nodeModel { - OCMExpect([mockDataSource collectionNode:collectionNode viewModelForItemAtIndexPath:indexPath]) - .andReturn(viewModel); + OCMExpect([mockDataSource collectionNode:collectionNode nodeModelForItemAtIndexPath:indexPath]) + .andReturn(nodeModel); } - (void)expectContextMethodForSection:(NSInteger)section @@ -240,21 +240,21 @@ for (NSInteger section = 0; section < sections.count; section++) { ASTestSection *sectionObject = sections[section]; - NSArray *viewModels = sectionObject.viewModels; + NSArray *nodeModels = sectionObject.nodeModels; // Assert section object XCTAssertEqualObjects([collectionNode contextForSection:section], sectionObject); // Assert item count - XCTAssertEqual([collectionNode numberOfItemsInSection:section], viewModels.count); - for (NSInteger item = 0; item < viewModels.count; item++) { - // Assert view model + XCTAssertEqual([collectionNode numberOfItemsInSection:section], nodeModels.count); + for (NSInteger item = 0; item < nodeModels.count; item++) { + // Assert node model // Could use pointer equality but the error message is less readable. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; - id viewModel = viewModels[indexPath.item]; - XCTAssertEqualObjects(viewModel, [collectionNode viewModelForItemAtIndexPath:indexPath]); + id nodeModel = nodeModels[indexPath.item]; + XCTAssertEqualObjects(nodeModel, [collectionNode nodeModelForItemAtIndexPath:indexPath]); ASCellNode *node = [collectionNode nodeForItemAtIndexPath:indexPath]; - XCTAssertEqualObjects(node.nodeViewModel, viewModel); + XCTAssertEqualObjects(node.nodeModel, nodeModel); } } } @@ -263,7 +263,7 @@ * Updates the collection node, with expectations and assertions about the call-order and the correctness of the * new data. You should update the data source _before_ calling this method. * - * skippedReloadIndexPaths are the old index paths for nodes that should use -canUpdateToViewModel: instead of being refetched. + * skippedReloadIndexPaths are the old index paths for nodes that should use -canUpdateToNodeModel: instead of being refetched. */ - (void)performUpdateReloadingSections:(NSDictionary *)reloadedSections reloadingItems:(NSDictionary *)reloadedItems @@ -275,7 +275,7 @@ [collectionNode performBatchUpdates:^{ // First update our data source. [reloadedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { - sections[key.section].viewModels[key.item] = obj; + sections[key.section].nodeModels[key.item] = obj; }]; [reloadedSections enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { sections[key.integerValue] = obj; @@ -283,13 +283,13 @@ // Deletion paths, sorted descending for (NSIndexPath *indexPath in [deletedItems sortedArrayUsingSelector:@selector(compare:)].reverseObjectEnumerator) { - [sections[indexPath.section].viewModels removeObjectAtIndex:indexPath.item]; + [sections[indexPath.section].nodeModels removeObjectAtIndex:indexPath.item]; } // Insertion paths, sorted ascending. NSArray *insertionsSortedAcending = [insertedItems.allKeys sortedArrayUsingSelector:@selector(compare:)]; for (NSIndexPath *indexPath in insertionsSortedAcending) { - [sections[indexPath.section].viewModels insertObject:insertedItems[indexPath] atIndex:indexPath.item]; + [sections[indexPath.section].nodeModels insertObject:insertedItems[indexPath] atIndex:indexPath.item]; } // Then update the collection node. @@ -314,10 +314,10 @@ // Go through reloaded sections and add all their items into `insertsPlusReloads` [reloadedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { [self expectContextMethodForSection:section]; - NSArray *viewModels = sections[section].viewModels; - for (NSInteger i = 0; i < viewModels.count; i++) { + NSArray *nodeModels = sections[section].nodeModels; + for (NSInteger i = 0; i < nodeModels.count; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; - insertsPlusReloads[indexPath] = viewModels[i]; + insertsPlusReloads[indexPath] = nodeModels[i]; } }]; @@ -326,7 +326,7 @@ }]; for (NSIndexPath *indexPath in [insertsPlusReloads.allKeys sortedArrayUsingSelector:@selector(compare:)]) { - [self expectViewModelMethodForItemAtIndexPath:indexPath viewModel:insertsPlusReloads[indexPath]]; + [self expectNodeModelMethodForItemAtIndexPath:indexPath nodeModel:insertsPlusReloads[indexPath]]; NSIndexPath *oldIndexPath = [reloadMappings allKeysForObject:indexPath].firstObject; BOOL isSkippedReload = oldIndexPath && [skippedReloadIndexPaths containsObject:oldIndexPath]; if (!isSkippedReload) { @@ -335,7 +335,7 @@ } } completion:nil]; - // Assert that the counts and view models are all correct now. + // Assert that the counts and node models are all correct now. [self assertCollectionNodeContent]; } @@ -345,9 +345,9 @@ @implementation ASTestCellNode -- (BOOL)canUpdateToViewModel:(id)viewModel +- (BOOL)canUpdateToNodeModel:(id)nodeModel { - // Our tests default to NO for migrating view models. We use OCMExpect to return YES when we specifically want to. + // Our tests default to NO for migrating node models. We use OCMExpect to return YES when we specifically want to. return NO; } @@ -360,7 +360,7 @@ - (instancetype)init { if (self = [super init]) { - _viewModels = [NSMutableArray array]; + _nodeModels = [NSMutableArray array]; } return self; } From 6b3e2ba4e0f64053b4e9b594f4016c3a1e4f43a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ka=C5=A1par?= Date: Sat, 12 Aug 2017 08:03:42 -0700 Subject: [PATCH 11/26] [ASCoreAnimationExtras] Update documentation for resizbale images #trivial (#492) * Update documentation for resizbale images * Add documentation to ASDisplayNodeSetResizableContents --- Source/Private/_ASCoreAnimationExtras.h | 7 ++++++- Source/Private/_ASCoreAnimationExtras.mm | 10 +++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Source/Private/_ASCoreAnimationExtras.h b/Source/Private/_ASCoreAnimationExtras.h index 3272549173..69c3053954 100644 --- a/Source/Private/_ASCoreAnimationExtras.h +++ b/Source/Private/_ASCoreAnimationExtras.h @@ -38,7 +38,12 @@ ASDISPLAYNODE_EXTERN_C_BEGIN @interface ASDisplayNode (ASResizableContents) @end -// This function can operate on either an ASDisplayNode (including un-loaded) or CALayer directly. +/** + This function can operate on either an ASDisplayNode (including un-loaded) or CALayer directly. More info about resizing mode: https://github.com/TextureGroup/Texture/issues/439 + + @param obj ASDisplayNode, CALayer or object that conforms to `ASResizableContents` protocol + @param image Image you would like to resize + */ extern void ASDisplayNodeSetResizableContents(id obj, UIImage *image); /** diff --git a/Source/Private/_ASCoreAnimationExtras.mm b/Source/Private/_ASCoreAnimationExtras.mm index 3cd18c8e85..65172962ef 100644 --- a/Source/Private/_ASCoreAnimationExtras.mm +++ b/Source/Private/_ASCoreAnimationExtras.mm @@ -26,20 +26,16 @@ extern void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UI extern void ASDisplayNodeSetResizableContents(id obj, UIImage *image) { - // FIXME: This method does not currently handle UIImageResizingModeTile, which is the default on iOS 6. - // I'm not sure of a way to use CALayer directly to perform such tiling on the GPU, though the stretch is handled by the GPU, - // and CALayer.h documents the fact that contentsCenter is used to stretch the pixels. - if (image) { + ASDisplayNodeCAssert(image.resizingMode == UIImageResizingModeStretch || UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero), + @"Image insets must be all-zero or resizingMode has to be UIImageResizingModeStretch. XCode assets default value is UIImageResizingModeTile which is not supported by Texture because of GPU-accelerated CALayer features."); + // Image may not actually be stretchable in one or both dimensions; this is handled obj.contents = (id)[image CGImage]; obj.contentsScale = [image scale]; obj.rasterizationScale = [image scale]; CGSize imageSize = [image size]; - ASDisplayNodeCAssert(image.resizingMode == UIImageResizingModeStretch || UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero), - @"the resizing mode of image should be stretch; if not, then its insets must be all-zero"); - UIEdgeInsets insets = [image capInsets]; // These are lifted from what UIImageView does by experimentation. Without these exact values, the stretching is slightly off. From a929950d2c071e13574af3f2b9195855b0784c3e Mon Sep 17 00:00:00 2001 From: Max Wang Date: Mon, 14 Aug 2017 08:31:52 -0700 Subject: [PATCH 12/26] Fix SIMULATE_WEB_RESPONSE not imported (#450) * fix SIMULATE_WEB_RESPONSE not imported (Reported in #449). * changes per review * Update license of ViewController.m --- examples/ASCollectionView/Sample/ViewController.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index b895e9c9b0..3d945929dc 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -16,7 +16,7 @@ // #import "ViewController.h" - +#import "AppDelegate.h" #import #import "SupplementaryNode.h" #import "ItemNode.h" @@ -82,18 +82,18 @@ { NSLog(@"ViewController is not nil"); strongSelf->_data = [[NSArray alloc] init]; - [strongSelf->_collectionView performBatchUpdates:^{ - [strongSelf->_collectionView insertSections:[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, 100)]]; + [strongSelf->_collectionNode performBatchUpdates:^{ + [strongSelf->_collectionNode insertSections:[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, 100)]]; } completion:nil]; - NSLog(@"ViewController finished updating collectionView"); + NSLog(@"ViewController finished updating collectionNode"); } else { - NSLog(@"ViewController is nil - won't update collectionView"); + NSLog(@"ViewController is nil - won't update collectionNode"); } }; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), mockWebService); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.navigationController popViewControllerAnimated:YES]; }); #endif From afeb25a6b4dd1f4fa147e809927e89aa3aa50d8e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 14 Aug 2017 16:52:45 +0100 Subject: [PATCH 13/26] [examples/ASCollectionView] Register supplementary kinds (#508) --- examples/ASCollectionView/Sample/ViewController.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 3d945929dc..fa093f80ad 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -55,6 +55,8 @@ layout.headerReferenceSize = CGSizeMake(50.0, 50.0); layout.footerReferenceSize = CGSizeMake(50.0, 50.0); self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; + [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; #endif self.collectionNode.dataSource = self; From 42b5633bcc38f1985994bbe53191d63c90b60587 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 15 Aug 2017 08:04:12 -0700 Subject: [PATCH 14/26] Avoid re-entrant call to self.view when applying initial pending state (#510) * Avoid re-entrant call to .view * Increment the changelog --- CHANGELOG.md | 1 + Source/ASDisplayNode.mm | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137725629c..64cad36ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) - Rename ASCellNode.viewModel to ASCellNode.nodeModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#504](https://github.com/TextureGroup/Texture/pull/504) - [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) +- Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index d902740973..2b58f943da 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -3093,12 +3093,13 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { - (void)_locked_applyPendingViewState { ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert([self _locked_isNodeLoaded], @"Expected node to be loaded before applying pending state."); if (_flags.layerBacked) { - [_pendingViewState applyToLayer:self.layer]; + [_pendingViewState applyToLayer:_layer]; } else { BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); - [_pendingViewState applyToView:self.view withSpecialPropertiesHandling:specialPropertiesHandling]; + [_pendingViewState applyToView:_view withSpecialPropertiesHandling:specialPropertiesHandling]; } // _ASPendingState objects can add up very quickly when adding From b2d1ed95efd728f97d2e10bcd82a8782091bbec8 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 15 Aug 2017 13:37:06 -0700 Subject: [PATCH 15/26] Update changelog and podspec for 2.4 (#512) --- CHANGELOG.md | 18 ++++++++---------- Texture.podspec | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64cad36ac3..88b72a277a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,27 @@ ## master * Add your own contributions to the next release on the line below this with your name. -- [ASStackLayoutSpec] Add lineSpacing property working with flex wrap. [Flo Vouin](https://github.com/flovouin) -- [ASStackLayoutSpec] Fix flex wrap overflow in some cases using item spacing. [Flo Vouin](https://github.com/flovouin) -- [ASNodeController] Add -nodeDidLayout callback. Allow switching retain behavior at runtime. [Scott Goodson](https://github.com/appleguy) -- [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) -- [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) -- [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) -- Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) [#496](https://github.com/TextureGroup/Texture/pull/496) -- Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) -- Rename ASCellNode.viewModel to ASCellNode.nodeModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#504](https://github.com/TextureGroup/Texture/pull/504) - [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) -##2.3.5 + +##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) - Overhaul logging and add activity tracing support. [Adlai Holler](https://github.com/Adlai-Holler) - Fix a crash where scrolling a table view after entering editing mode could lead to bad internal states in the table. [Huy Nguyen](https://github.com/nguyenhuy) [#416](https://github.com/TextureGroup/Texture/pull/416/) - Fix a crash in collection view that occurs if batch updates are performed while scrolling [Huy Nguyen](https://github.com/nguyenhuy) [#378](https://github.com/TextureGroup/Texture/issues/378) - Some improvements in ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#407](https://github.com/TextureGroup/Texture/pull/407) - Small refactors in ASDataController [Huy Nguyen](https://github.com/TextureGroup/Texture/pull/443) [#443](https://github.com/TextureGroup/Texture/pull/443) +- [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) +- [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) +- [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) +- Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) +- Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) +- Rename ASCellNode.viewModel to ASCellNode.nodeModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#504](https://github.com/TextureGroup/Texture/pull/504) ##2.3.4 - [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy) diff --git a/Texture.podspec b/Texture.podspec index 5cb0b9366a..9c53d9f4c1 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '2.3.4' + spec.version = '2.4' spec.license = { :type => 'BSD and Apache 2', } spec.homepage = 'http://texturegroup.org' spec.authors = { 'Huy Nguyen' => 'huy@pinterest.com', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'schneider@pinterest.com', 'Adlai Holler' => 'adlai@pinterest.com' } From 46e949460afc5534d6cd015d2686b4c641ce1ed8 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 17 Aug 2017 14:43:54 +0100 Subject: [PATCH 16/26] ASCollectionLayout to return a zero content size if its state is unavailable (#509) --- CHANGELOG.md | 2 +- Source/Private/ASCollectionLayout.mm | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b72a277a..3ec7a40001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) - [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) - +- Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index e8ade9feba..5f65f066d9 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -140,8 +140,9 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh - (CGSize)collectionViewContentSize { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssertNotNil(_layout, @"Collection layout state should not be nil at this point"); - return _layout.contentSize; + // The content size can be queried right after a layout invalidation (https://github.com/TextureGroup/Texture/pull/509). + // In that case, return zero. + return _layout ? _layout.contentSize : CGSizeZero; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)blockingRect From 5e13ebac8b886ee7a41075369b2954c78f836ec1 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 17 Aug 2017 15:30:34 +0100 Subject: [PATCH 17/26] ASCollectionLayout improvements (#513) - During the first layout calculation, measure more than just elements in the visible viewport. - Remove unnecessary params in `-[ASCollectionLayoutState getAndRemoveUnmeasuredLayoutAttributesPageTableInRect`.] --- CHANGELOG.md | 2 +- Source/Details/ASCollectionLayoutState.mm | 5 +++-- Source/Private/ASCollectionLayout.mm | 16 +++++++++------- Source/Private/ASCollectionLayoutState+Private.h | 4 +--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec7a40001..8a07d9188d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) - [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) -- Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) +- Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) [#513](https://github.com/TextureGroup/Texture/pull/513) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index eb59edf0ca..83226a1437 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -170,9 +170,10 @@ elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; } - (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize { + CGSize pageSize = _context.viewportSize; + CGSize contentSize = _contentSize; + ASDN::MutexLocker l(__instanceLock__); if (_unmeasuredPageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { return nil; diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 5f65f066d9..97451c32a6 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -93,7 +93,7 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh ASCollectionLayoutState *layout = [context.layoutDelegateClass calculateLayoutWithContext:context]; [context.layoutCache setLayout:layout forContext:context]; - // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly + // Measure elements in the measure range ahead of time CGSize viewportSize = context.viewportSize; CGPoint contentOffset = context.initialContentOffset; CGRect initialRect = CGRectMake(contentOffset.x, contentOffset.y, viewportSize.width, viewportSize.height); @@ -101,7 +101,11 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh kASDefaultMeasureRangeTuningParameters, context.scrollableDirections, kASStaticScrollDirection); - [self _measureElementsInRect:measureRect blockingRect:initialRect layout:layout]; + // The first call to -layoutAttributesForElementsInRect: will be with a rect that is way bigger than initialRect here. + // If we only block on initialRect, a few elements that are outside of initialRect but inside measureRect + // may not be available by the time -layoutAttributesForElementsInRect: is called. + // Since this method is usually run off main, let's spawn more threads to measure and block on all elements in measureRect. + [self _measureElementsInRect:measureRect blockingRect:measureRect layout:layout]; return layout; } @@ -248,11 +252,7 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh } // Step 2: Get layout attributes of all elements within the specified outer rect - ASCollectionLayoutContext *context = layout.context; - CGSize pageSize = context.viewportSize; - ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect - contentSize:contentSize - pageSize:pageSize]; + ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect]; if (attrsTable.count == 0) { // No elements in this rect! Bail early return; @@ -260,6 +260,8 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh // Step 3: Split all those attributes into blocking and non-blocking buckets // Use ordered sets here because some items may span multiple pages, and the sets will be accessed by indexes later on. + ASCollectionLayoutContext *context = layout.context; + CGSize pageSize = context.viewportSize; NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; NSMutableOrderedSet *nonBlockingAttrs = [NSMutableOrderedSet orderedSet]; for (id pagePtr in attrsTable) { diff --git a/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h index 170e57acc5..97304b4afb 100644 --- a/Source/Private/ASCollectionLayoutState+Private.h +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -22,9 +22,7 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion This method is atomic and thread-safe */ -- (nullable ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize; +- (nullable ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect; @end From cae9517ebe48b68f4e2f5bccfe7c03c207596e07 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 17 Aug 2017 18:02:11 +0100 Subject: [PATCH 18/26] BuildKite to ignore all markdown files (#517) --- CI/exclude-from-build.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CI/exclude-from-build.json b/CI/exclude-from-build.json index abc6f913a6..999e736a52 100644 --- a/CI/exclude-from-build.json +++ b/CI/exclude-from-build.json @@ -1,5 +1,6 @@ [ "^plans/", "^docs/", - "^CI/exclude-from-build.json$" -] \ No newline at end of file + "^CI/exclude-from-build.json$", + "^**/*.md$" +] From 1204737330416f0b3e5c2f0f014770a376e49206 Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Sun, 20 Aug 2017 11:46:02 +0300 Subject: [PATCH 19/26] Update corner-rounding.md (#482) * Update corner-rounding.md If I understand right, both Precomposited corners techniques uses `[path clip]`, right? If so, I think it will be better to move it to the general description of the section, as currently it seems as only the second option uses `[path clip]`. * Update corner-rounding.md Improved positioning. * Update corner-rounding.md Syntax consistency: Changed `off-screen` to `offscreen`, as both syntaxes currently appears in the text. --- docs/_docs/corner-rounding.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_docs/corner-rounding.md b/docs/_docs/corner-rounding.md index d0fff0237a..a69f8fa6a2 100755 --- a/docs/_docs/corner-rounding.md +++ b/docs/_docs/corner-rounding.md @@ -17,7 +17,7 @@ When it comes to corner rounding, many developers stick with CALayer's `.cornerR ## CALayer's .cornerRadius is Expensive -Why is `.cornerRadius` so expensive? Use of CALayer's `.cornerRadius` property triggers off-screen rendering to perform the clipping operation on every frame - 60 FPS during scrolling - even if the content in that area isn't changing! This means that the GPU has to switch contexts on every frame, between compositing the overall frame + additional passes for each use of `.cornerRadius`. +Why is `.cornerRadius` so expensive? Use of CALayer's `.cornerRadius` property triggers offscreen rendering to perform the clipping operation on every frame - 60 FPS during scrolling - even if the content in that area isn't changing! This means that the GPU has to switch contexts on every frame, between compositing the overall frame + additional passes for each use of `.cornerRadius`. Importantly, these costs don't show up in the Time Profiler, because they affect work done by the CoreAnimation Render Server on your app's behalf. This intensive thrash annihilates performance for a lot of devices. On the iPhone 4, 4S, and 5 / 5C (along with comparable iPads / iPods), expect to see notably degraded performance. On the iPhone 5S and newer, even if you can't see the impact directly, it will reduce headroom so that it takes less to cause a frame drop. @@ -53,11 +53,11 @@ The final consideration is to determine if all four corners cover the same node ### Precomposited Corners -Precomposited corners refer to corners drawn using bezier paths to clip the content in a CGContext / UIGraphicsContext. In this scenario, the corners become part of the image itself — and are "baked in" to the single CALayer. There are two types of precomposited corners. +Precomposited corners refer to corners drawn using bezier paths to clip the content in a CGContext / UIGraphicsContext (`[path clip]`). In this scenario, the corners become part of the image itself — and are "baked in" to the single CALayer. There are two types of precomposited corners. The absolute best method is to use **precomposited opaque corners**. This is the most efficient method available, resulting in zero alpha blending (although this is much less critical than avoiding offscreen rendering). Unfortunately, this method is also the least flexible; the background behind the corners will need to be a solid color if the rounded image needs to move around on top of it. It's possible, but tricky to make precomposited corners with a textured or photo background - usually it's best to use precomposited alpha corners instead'.' -The second method involves using bezier paths with **precomposited alpha corners** (`[path clip]`). This method is pretty flexible and should be one of the most frequently used. It does incur the cost of alpha blending across the full size of the content, and including an alpha channel increases memory impact by 25% over opaque precompositing - but these costs are tiny on modern devices, and a different order of magnitude than `.cornerRadius` offscreen rendering. +The second method involves using bezier paths with **precomposited alpha corners**. This method is pretty flexible and should be one of the most frequently used. It does incur the cost of alpha blending across the full size of the content, and including an alpha channel increases memory impact by 25% over opaque precompositing - but these costs are tiny on modern devices, and a different order of magnitude than `.cornerRadius` offscreen rendering. A key limitation of precomposited corners is that the corners must only touch one node and not intersect with any subnodes. If either of these conditions exist, clip corners must be used. From 65fabf49d70c6afc9985cbb2d4eac4e37380c3e9 Mon Sep 17 00:00:00 2001 From: appleguy Date: Sun, 20 Aug 2017 03:17:46 -0700 Subject: [PATCH 20/26] [ASImageNode] Enable .clipsToBounds by default (fix .cornerRadius, GIFs overflow). (#466) * [ASImageNode] Enable .clipsToBounds by default (fix .cornerRadius, GIFs overflow). We've seen a number of bugs reported over time that .cornerRadius didn't work on ASNetworkImageNode. This wasn't much of a concern because cornerRadius is very inefficient anyway, and there are better ways to round corners, but it should certainly work. It turns out that clipsToBounds has been off for images, and this ultimately was behind another issue recently seen wehre decoded GIFs would spill outside the bounds area to overlap nearby content. Although there is some risk of behavior change from this, I think the risk is fairly small, and in most cases it will probably fix behaviors in a way that doesn't cause problems for the app. We should consider if this property should be on for all ASDisplayNodes, but for now it would be a great step to be confident it's on for all ASImageNodes. * Update changelog for ImageNode Clipping. --- CHANGELOG.md | 1 + Source/ASImageNode.mm | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a07d9188d..078fa9e171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASImageNode] Enabled .clipsToBounds by default, fixing the use of .cornerRadius and clipping of GIFs. [Scott Goodson](https://github.com/appleguy) [#466](https://github.com/TextureGroup/Texture/pull/466) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index d36509d566..dce952d2c2 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -175,7 +175,8 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); self.contentsScale = ASScreenScale(); self.contentMode = UIViewContentModeScaleAspectFill; self.opaque = NO; - + self.clipsToBounds = YES; + // If no backgroundColor is set to the image node and it's a subview of UITableViewCell, UITableView is setting // the opaque value of all subviews to YES if highlighting / selection is happening and does not set it back to the // initial value. With setting a explicit backgroundColor we can prevent that change. From 5cf05f3c1777739b8a8117b3c176a79a713576bc Mon Sep 17 00:00:00 2001 From: appleguy Date: Sun, 20 Aug 2017 13:17:05 -0700 Subject: [PATCH 21/26] [Accessibility] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. (#468) After consulting Apple documentation and working with some a11y experts, we've found that aggregating objects that have a11y labels but are not themselves interactable is significantly preferred for these users. It makes it much quicker to navigate scrolling content if VoiceOver only stops to select entire cells, and then allows drilling down into the cell to select individual components. This implementation achieves that behavior. We should consider enabling isAccessibilityContainer by default on ASCellNode. This would be an improvement for 95% of a11y use cases. Aggregation can be enabled or disabled on any node. --- CHANGELOG.md | 1 + Source/ASDisplayNode+Beta.h | 13 +++ Source/ASDisplayNode+Yoga.mm | 6 ++ Source/ASDisplayNode.mm | 13 +++ Source/Details/_ASDisplayViewAccessiblity.mm | 89 +++++++++++++++++++- Source/Private/ASDisplayNodeInternal.h | 1 + 6 files changed, 120 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078fa9e171..e8302e826a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [Accessibility] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. [#468][Scott Goodson](https://github.com/appleguy) - [ASImageNode] Enabled .clipsToBounds by default, fixing the use of .cornerRadius and clipping of GIFs. [Scott Goodson](https://github.com/appleguy) [#466](https://github.com/TextureGroup/Texture/pull/466) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 8235c24c9f..8e413a5a35 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -112,6 +112,19 @@ typedef struct { @property (nonatomic, strong, readonly) ASEventLog *eventLog; #endif +/** + * @abstract Whether this node acts as an accessibility container. If set to YES, then this node's accessibility label will represent + * an aggregation of all child nodes' accessibility labels. Nodes in this node's subtree that are also accessibility containers will + * not be included in this aggregation, and will be exposed as separate accessibility elements to UIKit. + */ +@property (nonatomic, assign) BOOL isAccessibilityContainer; + +/** + * @abstract Invoked when a user performs a custom action on an accessible node. Nodes that are children of accessibility containers, have + * an accessibity label and have an interactive UIAccessibilityTrait will automatically receive custom-action handling. + */ +- (void)performAccessibilityCustomAction:(UIAccessibilityCustomAction *)action; + /** * @abstract Currently used by ASNetworkImageNode and ASMultiplexImageNode to allow their placeholders to stay if they are loading an image from the network. * Otherwise, a display pass is scheduled and completes, but does not actually draw anything - and ASDisplayNode considers the element finished. diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index cc41593a65..281bf1e781 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -19,6 +19,7 @@ #if YOGA /* YOGA */ +#import #import #import #import @@ -235,6 +236,11 @@ yogaFloatForCGFloat(rootConstrainedSize.max.height), YGDirectionInherit); + // Reset accessible elements, since layout may have changed. + ASPerformBlockOnMainThread(^{ + [(_ASDisplayView *)self.view setAccessibleElements:nil]; + }); + ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { [node setupYogaCalculatedLayout]; node.yogaLayoutInProgress = NO; diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 2b58f943da..e1ce6f8bd9 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -3171,6 +3171,19 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { return measurements; } +#pragma mark - Accessibility + +- (void)setIsAccessibilityContainer:(BOOL)isAccessibilityContainer +{ + ASDN::MutexLocker l(__instanceLock__); + _isAccessibilityContainer = isAccessibilityContainer; +} + +- (BOOL)isAccessibilityContainer +{ + ASDN::MutexLocker l(__instanceLock__); + return _isAccessibilityContainer; +} #pragma mark - Debugging (Private) diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index e2f937403e..80ae2a9223 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -23,9 +23,21 @@ #import #import +#import + +NS_INLINE UIAccessibilityTraits InteractiveAccessibilityTraitsMask() { + return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; +} + #pragma mark - UIAccessibilityElement -typedef NSComparisonResult (^SortAccessibilityElementsComparator)(UIAccessibilityElement *, UIAccessibilityElement *); +@protocol ASAccessibilityElementPositioning + +@property (nonatomic, readonly) CGRect accessibilityFrame; + +@end + +typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id, id); /// Sort accessiblity elements first by y and than by x origin. static void SortAccessibilityElements(NSMutableArray *elements) @@ -35,7 +47,7 @@ static void SortAccessibilityElements(NSMutableArray *elements) static SortAccessibilityElementsComparator comparator = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - comparator = ^NSComparisonResult(UIAccessibilityElement *a, UIAccessibilityElement *b) { + comparator = ^NSComparisonResult(id a, id b) { CGPoint originA = a.accessibilityFrame.origin; CGPoint originB = b.accessibilityFrame.origin; if (originA.y == originB.y) { @@ -50,7 +62,7 @@ static void SortAccessibilityElements(NSMutableArray *elements) [elements sortUsingComparator:comparator]; } -@interface ASAccessibilityElement : UIAccessibilityElement +@interface ASAccessibilityElement : UIAccessibilityElement @property (nonatomic, strong) ASDisplayNode *node; @property (nonatomic, strong) ASDisplayNode *containerNode; @@ -85,6 +97,25 @@ static void SortAccessibilityElements(NSMutableArray *elements) #pragma mark - _ASDisplayView / UIAccessibilityContainer +@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction + +@property (nonatomic, strong) UIView *container; +@property (nonatomic, strong) ASDisplayNode *node; +@property (nonatomic, strong) ASDisplayNode *containerNode; + +@end + +@implementation ASAccessibilityCustomAction + +- (CGRect)accessibilityFrame +{ + CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; + accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.container); + return accessibilityFrame; +} + +@end + /// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) { @@ -100,12 +131,64 @@ static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplay }); } +static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, _ASDisplayView *view, NSMutableArray *elements) { + UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:container containerNode:container]; + + NSMutableArray *labeledNodes = [NSMutableArray array]; + NSMutableArray *actions = [NSMutableArray array]; + std::queue queue; + queue.push(container); + + ASDisplayNode *node; + while (!queue.empty()) { + node = queue.front(); + queue.pop(); + + if (node != container && node.isAccessibilityContainer) { + CollectAccessibilityElementsForContainer(node, view, elements); + continue; + } + + if (node.accessibilityLabel.length > 0) { + if (node.accessibilityTraits & InteractiveAccessibilityTraitsMask()) { + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)]; + action.node = node; + action.containerNode = node.supernode; + action.container = node.supernode.view; + [actions addObject:action]; + } else { + // Even though not surfaced to UIKit, create a non-interactive element for purposes of building sorted aggregated label. + ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node containerNode:container]; + [labeledNodes addObject:nonInteractiveElement]; + } + } + + for (ASDisplayNode *subnode in node.subnodes) { + queue.push(subnode); + } + } + + SortAccessibilityElements(labeledNodes); + NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; + accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + + SortAccessibilityElements(actions); + accessiblityElement.accessibilityCustomActions = actions; + + [elements addObject:accessiblityElement]; +} + /// Collect all accessibliity elements for a given view and view node static void CollectAccessibilityElementsForView(_ASDisplayView *view, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); ASDisplayNode *node = view.asyncdisplaykit_node; + + if (node.isAccessibilityContainer) { + CollectAccessibilityElementsForContainer(node, view, elements); + return; + } // Handle rasterize case if (node.rasterizesSubtree) { diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 7555e09389..36683954dd 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -195,6 +195,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo NSArray *_accessibilityHeaderElements; CGPoint _accessibilityActivationPoint; UIBezierPath *_accessibilityPath; + BOOL _isAccessibilityContainer; // performance measurement ASDisplayNodePerformanceMeasurementOptions _measurementOptions; From 359b5f0b5bece3f41fcc6e1a1da1f1cbf510f626 Mon Sep 17 00:00:00 2001 From: Phil Larson Date: Mon, 21 Aug 2017 04:49:11 -0700 Subject: [PATCH 22/26] ASImageNode+AnimatedImage playbackReadyCallback retain cycle (#520) * ASImageNode+AnimatedImage playbackReadyCallback causes strong retain cycle * Add CHANGELOG entry for #520 --- CHANGELOG.md | 1 + Source/ASImageNode+AnimatedImage.mm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8302e826a..4565679196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) - Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) [#513](https://github.com/TextureGroup/Texture/pull/513) +- Fix retain cycle between ASImageNode and PINAnimatedImage [Phil Larson](https://github.com/plarson) [#520](https://github.com/TextureGroup/Texture/pull/520) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 0ff21c4b4b..0d12351dd6 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -70,7 +70,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } else { animatedImage.playbackReadyCallback = ^{ // In this case the lock is already gone we have to call the unlocked version therefore - [self setShouldAnimate:YES]; + [weakSelf setShouldAnimate:YES]; }; } } From 884a4f56f1792e0f90ffddba25f2a2594e7675f1 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 22 Aug 2017 22:14:01 +0100 Subject: [PATCH 23/26] [Table and collection views] Consider content inset when calculating (default) element size range (#525) * Table and collection views to consider their content inset when calculating element size range * Update CHANGELOG * Address comments * -[ASPagerNode currentPageIndex] to use pageSize instead of bounds * Update documentation in ASPagerNode * Minor change --- CHANGELOG.md | 1 + Source/ASCollectionView.mm | 4 ---- Source/ASPagerNode.h | 5 +++++ Source/ASPagerNode.m | 15 ++++++++++++--- Source/ASTableView.mm | 3 ++- Source/Details/ASCollectionViewLayoutInspector.m | 3 +++ 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4565679196..26d7771852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) - Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) [#513](https://github.com/TextureGroup/Texture/pull/513) - Fix retain cycle between ASImageNode and PINAnimatedImage [Phil Larson](https://github.com/plarson) [#520](https://github.com/TextureGroup/Texture/pull/520) +- Table and collection views to consider content inset when calculating (default) element size range [Huy Nguyen](https://github.com/nguyenhuy) [#525](https://github.com/TextureGroup/Texture/pull/525) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 83bcc2447b..f7ce6859da 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1536,10 +1536,6 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section return [self.layoutInspector scrollableDirections]; } -- (ASScrollDirection)flowLayoutScrollableDirections:(UICollectionViewFlowLayout *)flowLayout { - return (flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal) ? ASScrollDirectionHorizontalDirections : ASScrollDirectionVerticalDirections; -} - - (void)layoutSubviews { if (_cellsForLayoutUpdates.count > 0) { diff --git a/Source/ASPagerNode.h b/Source/ASPagerNode.h index f5c0164120..51e57898c6 100644 --- a/Source/ASPagerNode.h +++ b/Source/ASPagerNode.h @@ -75,6 +75,9 @@ NS_ASSUME_NONNULL_BEGIN @end +/** + * A horizontal, paging collection node. + */ @interface ASPagerNode : ASCollectionNode /** @@ -84,6 +87,8 @@ NS_ASSUME_NONNULL_BEGIN /** * Initializer with custom-configured flow layout properties. + * + * NOTE: The flow layout must have a horizontal scroll direction. */ - (instancetype)initWithCollectionViewLayout:(ASPagerFlowLayout *)flowLayout; diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index 02666993e8..86062f6649 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -66,6 +66,7 @@ - (instancetype)initWithCollectionViewLayout:(ASPagerFlowLayout *)flowLayout; { ASDisplayNodeAssert([flowLayout isKindOfClass:[ASPagerFlowLayout class]], @"ASPagerNode requires a flow layout."); + ASDisplayNodeAssertTrue(flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal); self = [super initWithCollectionViewLayout:flowLayout]; return self; } @@ -112,7 +113,15 @@ - (NSInteger)currentPageIndex { - return (self.view.contentOffset.x / CGRectGetWidth(self.view.bounds)); + return (self.view.contentOffset.x / [self pageSize].width); +} + +- (CGSize)pageSize +{ + UIEdgeInsets contentInset = self.view.contentInset; + CGSize pageSize = self.bounds.size; + pageSize.height -= (contentInset.top + contentInset.bottom); + return pageSize; } #pragma mark - Helpers @@ -142,7 +151,7 @@ - (CGSize)sizeForElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - return self.bounds.size; + return [self pageSize]; } #pragma mark - ASCollectionDataSource @@ -179,7 +188,7 @@ } #pragma clang diagnostic pop - return ASSizeRangeMake(self.bounds.size); + return ASSizeRangeMake([self pageSize]); } #pragma mark - Data Source Proxy diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index d0e529c8bb..38bfe6836f 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -751,7 +751,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { // Remeasure all rows if our row width has changed. _remeasuringCellNodes = YES; - CGFloat constrainedWidth = self.bounds.size.width - [self sectionIndexWidth]; + UIEdgeInsets contentInset = self.contentInset; + CGFloat constrainedWidth = self.bounds.size.width - [self sectionIndexWidth] - contentInset.left - contentInset.right; if (constrainedWidth > 0 && _nodesConstrainedWidth != constrainedWidth) { _nodesConstrainedWidth = constrainedWidth; diff --git a/Source/Details/ASCollectionViewLayoutInspector.m b/Source/Details/ASCollectionViewLayoutInspector.m index b77f22172e..26100cec66 100644 --- a/Source/Details/ASCollectionViewLayoutInspector.m +++ b/Source/Details/ASCollectionViewLayoutInspector.m @@ -27,9 +27,12 @@ // of the collection view ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionView) { CGSize maxSize = collectionView.bounds.size; + UIEdgeInsets contentInset = collectionView.contentInset; if (ASScrollDirectionContainsHorizontalDirection(collectionView.scrollableDirections)) { maxSize.width = CGFLOAT_MAX; + maxSize.height -= (contentInset.top + contentInset.bottom); } else { + maxSize.width -= (contentInset.left + contentInset.right); maxSize.height = CGFLOAT_MAX; } return ASSizeRangeMake(CGSizeZero, maxSize); From 16ce3c9a33e83b50a01323e93963a1a584939b33 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 22 Aug 2017 14:48:42 -0700 Subject: [PATCH 24/26] Add a function to disable all logging at runtime (#528) * Implement a runtime disable for all logging * Update the changelog * Inline the function * Flip the scrip --- CHANGELOG.md | 1 + Source/Base/ASLog.h | 19 +++++++++++++++---- Source/Base/ASLog.m | 26 ++++++++++++++++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26d7771852..4b08e8ea71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) - Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) [#513](https://github.com/TextureGroup/Texture/pull/513) - Fix retain cycle between ASImageNode and PINAnimatedImage [Phil Larson](https://github.com/plarson) [#520](https://github.com/TextureGroup/Texture/pull/520) +- Change the API for disabling logging from a compiler flag to a runtime C function ASDisableLogging(). [Adlai Holler](https://github.com/Adlai-Holler) [#528](https://github.com/TextureGroup/Texture/pull/528) - Table and collection views to consider content inset when calculating (default) element size range [Huy Nguyen](https://github.com/nguyenhuy) [#525](https://github.com/TextureGroup/Texture/pull/525) ##2.4 diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index 412b9d905c..e4f54fd812 100644 --- a/Source/Base/ASLog.h +++ b/Source/Base/ASLog.h @@ -21,16 +21,27 @@ #import #import -#ifndef ASEnableLogs - #define ASEnableLogs 1 -#endif - #ifndef ASEnableVerboseLogging #define ASEnableVerboseLogging 0 #endif ASDISPLAYNODE_EXTERN_C_BEGIN +/** + * Disable all logging. + * + * You should only use this function if the default log level is + * annoying during development. By default, logging is run at + * the appropriate system log level (see the os_log_* functions), + * so you do not need to worry generally about the performance + * implications of log messages. + * + * For example, virtually all log messages generated by Texture + * are at the `debug` log level, which the system + * disables in production. + */ +void ASDisableLogging(); + /// Log for general node events e.g. interfaceState, didLoad. #define ASNodeLogEnabled 1 os_log_t ASNodeLog(); diff --git a/Source/Base/ASLog.m b/Source/Base/ASLog.m index 42148de9f3..8e65c4b55b 100644 --- a/Source/Base/ASLog.m +++ b/Source/Base/ASLog.m @@ -11,27 +11,41 @@ // #import +#import + +static atomic_bool __ASLogEnabled = ATOMIC_VAR_INIT(YES); + +void ASDisableLogging() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + atomic_store(&__ASLogEnabled, NO); + }); +} + +ASDISPLAYNODE_INLINE BOOL ASLoggingIsEnabled() { + return atomic_load(&__ASLogEnabled); +} os_log_t ASNodeLog() { - return ASCreateOnce((ASEnableLogs && ASNodeLogEnabled) ? as_log_create("org.TextureGroup.Texture", "Node") : OS_LOG_DISABLED); + return (ASNodeLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Node")) : OS_LOG_DISABLED; } os_log_t ASLayoutLog() { - return ASCreateOnce((ASEnableLogs && ASLayoutLogEnabled) ? as_log_create("org.TextureGroup.Texture", "Layout") : OS_LOG_DISABLED); + return (ASLayoutLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Layout")) : OS_LOG_DISABLED; } os_log_t ASCollectionLog() { - return ASCreateOnce((ASEnableLogs && ASCollectionLogEnabled) ? as_log_create("org.TextureGroup.Texture", "Collection") : OS_LOG_DISABLED); + return (ASCollectionLogEnabled && ASLoggingIsEnabled()) ?ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Collection")) : OS_LOG_DISABLED; } os_log_t ASDisplayLog() { - return ASCreateOnce((ASEnableLogs && ASDisplayLogEnabled) ? as_log_create("org.TextureGroup.Texture", "Display") : OS_LOG_DISABLED); + return (ASDisplayLogEnabled && ASLoggingIsEnabled()) ?ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Display")) : OS_LOG_DISABLED; } os_log_t ASImageLoadingLog() { - return ASCreateOnce((ASEnableLogs && ASImageLoadingLogEnabled) ? as_log_create("org.TextureGroup.Texture", "ImageLoading") : OS_LOG_DISABLED); + return (ASImageLoadingLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "ImageLoading")) : OS_LOG_DISABLED; } os_log_t ASMainThreadDeallocationLog() { - return ASCreateOnce((ASEnableLogs && ASMainThreadDeallocationLogEnabled) ? as_log_create("org.TextureGroup.Texture", "MainDealloc") : OS_LOG_DISABLED); + return (ASMainThreadDeallocationLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "MainDealloc")) : OS_LOG_DISABLED; } From ccc57860322f791dd1410641a15a889a639815d9 Mon Sep 17 00:00:00 2001 From: appleguy Date: Wed, 23 Aug 2017 03:16:21 -0700 Subject: [PATCH 25/26] [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. (#522) * [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. Over time, there have actually been a lot of legitimate uses for an API like this. In fact, I'm not quite sure what has held us back from adding one! I believe that at least some portion of -wait calls (even if less than 50%) could be replaced with -onDidFinishProcessingUpdates:, which could potentially improve the performance of applications using -wait by a significant amount. Please take a close look at implementation correctness. Although I'm in a bit of a rush, I'm aiming to make this properly documented and added a basic test -- but it could certainly use some more detailed testing as a followup. * [ASCollectionNode] Improvements to the implementation of -isProcessingUpdates and -onDidFinishProcessingUpdates: * Add lock to ASMainSerialQueue count method. * [ASTableNode] Implement -isProcessingUpdates and -onDidFinishProcessingUpdates:. Rename -waitUntil to consistent naming. --- CHANGELOG.md | 1 + Source/ASCollectionNode.h | 37 +++++++++++++++++++-- Source/ASCollectionNode.mm | 23 +++++++++++-- Source/ASCollectionView.h | 6 ++-- Source/ASCollectionView.mm | 16 ++++++++-- Source/ASTableNode.h | 39 +++++++++++++++++++++-- Source/ASTableNode.mm | 21 +++++++++++- Source/ASTableView.h | 6 ++-- Source/ASTableView.mm | 16 ++++++++-- Source/Details/ASDataController.h | 7 +++- Source/Details/ASDataController.mm | 33 +++++++++++++++++-- Source/Details/ASMainSerialQueue.h | 1 + Source/Details/ASMainSerialQueue.mm | 6 ++++ Tests/ASCollectionModernDataSourceTests.m | 2 +- Tests/ASCollectionViewTests.mm | 31 +++++++++++------- Tests/ASTableViewTests.mm | 16 +++++----- 16 files changed, 220 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b08e8ea71..3ba86ee3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. [#522](https://github.com/TextureGroup/Texture/pull/522) [Scott Goodson](https://github.com/appleguy) - [Accessibility] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. [#468][Scott Goodson](https://github.com/appleguy) - [ASImageNode] Enabled .clipsToBounds by default, fixing the use of .cornerRadius and clipping of GIFs. [Scott Goodson](https://github.com/appleguy) [#466](https://github.com/TextureGroup/Texture/pull/466) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 9ba6fdcee1..352867e9f5 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -252,10 +252,39 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +/** + * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. + * This is typically the concurrent allocation (calling nodeBlocks) and layout of newly inserted + * ASCellNodes. If YES is returned, then calling -waitUntilAllUpdatesAreProcessed may take tens of + * milliseconds to return as it blocks on these concurrent operations. + * + * Returns NO if ASCollectionNode is fully synchronized with the underlying UICollectionView. This + * means that until the next performBatchUpdates: is called, it is safe to compare UIKit values + * (such as from UICollectionViewLayout) with your app's data source. + * + * This method will always return NO if called immediately after -waitUntilAllUpdatesAreProcessed. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; + +/** + * Schedules a block to be performed (on the main thread) after processing of performBatchUpdates: + * is finished (completely synchronized to UIKit). The blocks will be run at the moment that + * -isProcessingUpdates changes from YES to NO; + * + * When isProcessingUpdates == NO, the block is run block immediately (before the method returns). + * + * Blocks scheduled by this mechanism are NOT guaranteed to run in the order they are scheduled. + * They may also be delayed if performBatchUpdates continues to be called; the blocks will wait until + * all running updates are finished. + * + * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. + */ +- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; + /** * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. */ -- (void)waitUntilAllUpdatesAreCommitted; +- (void)waitUntilAllUpdatesAreProcessed; /** * Inserts one or more sections. @@ -501,9 +530,11 @@ NS_ASSUME_NONNULL_BEGIN * @warning This method is substantially more expensive than UICollectionView's version. * * @deprecated This method is deprecated in 2.0. Use @c reloadDataWithCompletion: and - * then @c waitUntilAllUpdatesAreCommitted instead. + * then @c waitUntilAllUpdatesAreProcessed instead. */ -- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use -reloadData / -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreCommitted instead."); +- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use -reloadData / -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreProcessed instead."); + +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -waitUntilAllUpdatesAreProcessed."); @end diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 994e261886..5bb12befd4 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -705,7 +705,21 @@ [self performBatchAnimated:UIView.areAnimationsEnabled updates:updates completion:completion]; } -- (void)waitUntilAllUpdatesAreCommitted +- (BOOL)isProcessingUpdates +{ + return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO); +} + +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +{ + if (!self.nodeLoaded) { + completion(); + } else { + [self.view onDidFinishProcessingUpdates:completion]; + } +} + +- (void)waitUntilAllUpdatesAreProcessed { ASDisplayNodeAssertMainThread(); if (self.nodeLoaded) { @@ -713,6 +727,11 @@ } } +- (void)waitUntilAllUpdatesAreCommitted +{ + [self waitUntilAllUpdatesAreProcessed]; +} + - (void)reloadDataWithCompletion:(void (^)())completion { ASDisplayNodeAssertMainThread(); @@ -738,7 +757,7 @@ { ASDisplayNodeAssertMainThread(); [self reloadData]; - [self waitUntilAllUpdatesAreCommitted]; + [self waitUntilAllUpdatesAreProcessed]; } - (void)relayoutItems diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index f4dd891385..2a1eb18567 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -296,9 +296,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)relayoutItems ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); /** - * Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread. + * See ASCollectionNode.h for full documentation of these methods. */ -- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); +@property (nonatomic, readonly) BOOL isProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASCollectionNode waitUntilAllUpdatesAreProcessed] instead."); /** * Registers the given kind of supplementary node for use in creating node-backed supplementary views. diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index f7ce6859da..906577fde2 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -359,6 +359,16 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; [_dataController relayoutAllNodes]; } +- (BOOL)isProcessingUpdates +{ + return [_dataController isProcessingUpdates]; +} + +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +{ + [_dataController onDidFinishProcessingUpdates:completion]; +} + - (void)waitUntilAllUpdatesAreCommitted { ASDisplayNodeAssertMainThread(); @@ -367,8 +377,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; // ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); return; } - - [_dataController waitUntilAllUpdatesAreCommitted]; + + [_dataController waitUntilAllUpdatesAreProcessed]; } - (void)setDataSource:(id)dataSource @@ -2193,7 +2203,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if (changedInNonScrollingDirection) { [_dataController relayoutAllNodes]; - [_dataController waitUntilAllUpdatesAreCommitted]; + [_dataController waitUntilAllUpdatesAreProcessed]; // We need to ensure the size requery is done before we update our layout. [self.collectionViewLayout invalidateLayout]; } diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 20d59d2e34..08b26a6fd7 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -207,9 +207,38 @@ NS_ASSUME_NONNULL_BEGIN - (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; /** - * Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread. + * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. + * This is typically the concurrent allocation (calling nodeBlocks) and layout of newly inserted + * ASCellNodes. If YES is returned, then calling -waitUntilAllUpdatesAreProcessed may take tens of + * milliseconds to return as it blocks on these concurrent operations. + * + * Returns NO if ASCollectionNode is fully synchronized with the underlying UICollectionView. This + * means that until the next performBatchUpdates: is called, it is safe to compare UIKit values + * (such as from UICollectionViewLayout) with your app's data source. + * + * This method will always return NO if called immediately after -waitUntilAllUpdatesAreProcessed. */ -- (void)waitUntilAllUpdatesAreCommitted; +@property (nonatomic, readonly) BOOL isProcessingUpdates; + +/** + * Schedules a block to be performed (on the main thread) after processing of performBatchUpdates: + * is finished (completely synchronized to UIKit). The blocks will be run at the moment that + * -isProcessingUpdates changes from YES to NO; + * + * When isProcessingUpdates == NO, the block is run block immediately (before the method returns). + * + * Blocks scheduled by this mechanism are NOT guaranteed to run in the order they are scheduled. + * They may also be delayed if performBatchUpdates continues to be called; the blocks will wait until + * all running updates are finished. + * + * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. + */ +- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; + +/** + * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. + */ +- (void)waitUntilAllUpdatesAreProcessed; /** * Inserts one or more sections, with an option to animate the insertion. @@ -699,4 +728,10 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface ASTableNode (Deprecated) + +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -waitUntilAllUpdatesAreProcessed."); + +@end + NS_ASSUME_NONNULL_END diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index 4cd7fd923d..b4a3e06db9 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -733,7 +733,21 @@ ASLayoutElementCollectionTableSetTraitCollection(_environmentStateLock) } } -- (void)waitUntilAllUpdatesAreCommitted +- (BOOL)isProcessingUpdates +{ + return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO); +} + +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +{ + if (!self.nodeLoaded) { + completion(); + } else { + [self.view onDidFinishProcessingUpdates:completion]; + } +} + +- (void)waitUntilAllUpdatesAreProcessed { ASDisplayNodeAssertMainThread(); if (self.nodeLoaded) { @@ -741,6 +755,11 @@ ASLayoutElementCollectionTableSetTraitCollection(_environmentStateLock) } } +- (void)waitUntilAllUpdatesAreCommitted +{ + [self waitUntilAllUpdatesAreProcessed]; +} + #pragma mark - Debugging (Private) - (NSMutableArray *)propertiesForDebugDescription diff --git a/Source/ASTableView.h b/Source/ASTableView.h index 5877ab4d6d..0a3d07ec44 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -219,9 +219,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)endUpdatesAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL completed))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's -performBatchUpdates:completion: instead."); /** - * Blocks execution of the main thread until all section and row updates are committed. This method must be called from the main thread. + * See ASTableNode.h for full documentation of these methods. */ -- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); +@property (nonatomic, readonly) BOOL isProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASTableNode waitUntilAllUpdatesAreProcessed] instead."); - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 38bfe6836f..8f41cffac4 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -548,7 +548,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASDisplayNodeAssertMainThread(); [self reloadData]; - [_dataController waitUntilAllUpdatesAreCommitted]; + [_dataController waitUntilAllUpdatesAreProcessed]; } - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated @@ -735,6 +735,16 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } +- (BOOL)isProcessingUpdates +{ + return [_dataController isProcessingUpdates]; +} + +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +{ + [_dataController onDidFinishProcessingUpdates:completion]; +} + - (void)waitUntilAllUpdatesAreCommitted { ASDisplayNodeAssertMainThread(); @@ -743,8 +753,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); return; } - - [_dataController waitUntilAllUpdatesAreCommitted]; + + [_dataController waitUntilAllUpdatesAreProcessed]; } - (void)layoutSubviews diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 82791a685d..b2d2f3ab8a 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -254,7 +254,12 @@ extern NSString * const ASCollectionInvalidUpdateException; */ - (void)relayoutNodes:(id)nodes nodesSizeChanged:(NSMutableArray * _Nonnull)nodesSizesChanged; -- (void)waitUntilAllUpdatesAreCommitted; +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly) BOOL isProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)waitUntilAllUpdatesAreProcessed; /** * Notifies the data controller object that its environment has changed. The object will request its environment delegate for new information diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 102fca1239..889599506a 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -430,13 +430,42 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; #pragma mark - Batching (External API) -- (void)waitUntilAllUpdatesAreCommitted +- (void)waitUntilAllUpdatesAreProcessed { // Schedule block in main serial queue to wait until all operations are finished that are // where scheduled while waiting for the _editingTransactionQueue to finish [self _scheduleBlockOnMainSerialQueue:^{ }]; } +- (BOOL)isProcessingUpdates +{ + ASDisplayNodeAssertMainThread(); + if (_mainSerialQueue.numberOfScheduledBlocks > 0) { + return YES; + } else if (dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_NOW) != 0) { + // After waiting for zero duration, a nonzero value is returned if blocks are still running. + return YES; + } + // Both the _mainSerialQueue and _editingTransactionQueue are drained; we are fully quiesced. + return NO; +} + +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +{ + ASDisplayNodeAssertMainThread(); + if ([self isProcessingUpdates] == NO) { + ASPerformBlockOnMainThread(completion); + } else { + dispatch_async(_editingTransactionQueue, ^{ + // Retry the block. If we're done processing updates, it'll run immediately, otherwise + // wait again for updates to quiesce completely. + [_mainSerialQueue performBlockOnMainThread:^{ + [self onDidFinishProcessingUpdates:completion]; + }]; + }); + } +} + - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet { ASDisplayNodeAssertMainThread(); @@ -563,7 +592,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; }); if (_usesSynchronousDataLoading) { - [self waitUntilAllUpdatesAreCommitted]; + [self waitUntilAllUpdatesAreProcessed]; } } diff --git a/Source/Details/ASMainSerialQueue.h b/Source/Details/ASMainSerialQueue.h index ef939effc3..e94451d64c 100644 --- a/Source/Details/ASMainSerialQueue.h +++ b/Source/Details/ASMainSerialQueue.h @@ -21,6 +21,7 @@ AS_SUBCLASSING_RESTRICTED @interface ASMainSerialQueue : NSObject +@property (nonatomic, readonly) NSUInteger numberOfScheduledBlocks; - (void)performBlockOnMainThread:(dispatch_block_t)block; @end diff --git a/Source/Details/ASMainSerialQueue.mm b/Source/Details/ASMainSerialQueue.mm index e79481fa42..4a3d929c18 100644 --- a/Source/Details/ASMainSerialQueue.mm +++ b/Source/Details/ASMainSerialQueue.mm @@ -40,6 +40,12 @@ return self; } +- (NSUInteger)numberOfScheduledBlocks +{ + ASDN::MutexLocker l(_serialQueueLock); + return _blocks.count; +} + - (void)performBlockOnMainThread:(dispatch_block_t)block { ASDN::MutexLocker l(_serialQueueLock); diff --git a/Tests/ASCollectionModernDataSourceTests.m b/Tests/ASCollectionModernDataSourceTests.m index a6daa9782e..ff084a7b16 100644 --- a/Tests/ASCollectionModernDataSourceTests.m +++ b/Tests/ASCollectionModernDataSourceTests.m @@ -71,7 +71,7 @@ - (void)tearDown { - [collectionNode waitUntilAllUpdatesAreCommitted]; + [collectionNode waitUntilAllUpdatesAreProcessed]; [super tearDown]; } diff --git a/Tests/ASCollectionViewTests.mm b/Tests/ASCollectionViewTests.mm index 00cf58d2b5..f3567b4373 100644 --- a/Tests/ASCollectionViewTests.mm +++ b/Tests/ASCollectionViewTests.mm @@ -260,7 +260,7 @@ [window makeKeyAndVisible]; [testController.collectionNode reloadData]; - [testController.collectionNode waitUntilAllUpdatesAreCommitted]; + [testController.collectionNode waitUntilAllUpdatesAreProcessed]; [testController.collectionView layoutIfNeeded]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; @@ -397,7 +397,7 @@ window.rootViewController = testController;\ \ [cn reloadData];\ - [cn waitUntilAllUpdatesAreCommitted]; \ + [cn waitUntilAllUpdatesAreProcessed]; \ [testController.collectionView layoutIfNeeded]; - (void)testThatSubmittingAValidInsertDoesNotThrowAnException @@ -620,7 +620,7 @@ [window makeKeyAndVisible]; for (NSInteger i = 0; i < 2; i++) { - // NOTE: waitUntilAllUpdatesAreCommitted or reloadDataImmediately is not sufficient here!! + // NOTE: waitUntilAllUpdatesAreProcessed or reloadDataImmediately is not sufficient here!! XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]]; [cn reloadDataWithCompletion:^{ [done fulfill]; @@ -755,7 +755,7 @@ del.sectionGeneration++; [cn reloadData]; - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; NSInteger sectionCount = del->_itemCounts.size(); for (NSInteger section = 0; section < sectionCount; section++) { @@ -857,7 +857,7 @@ [window layoutIfNeeded]; ASCollectionNode *cn = testController.collectionNode; - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; [cn.view layoutIfNeeded]; ASCellNode *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertTrue(node.visible); @@ -880,7 +880,7 @@ [window layoutIfNeeded]; ASCollectionNode *cn = testController.collectionNode; - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; XCTAssertGreaterThan(cn.bounds.size.height, cn.view.contentSize.height, @"Expected initial data not to fill collection view area."); __block NSUInteger batchFetchCount = 0; @@ -926,7 +926,7 @@ [window layoutIfNeeded]; ASCollectionNode *cn = testController.collectionNode; - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; __block NSUInteger batchFetchCount = 0; XCTestExpectation *e = [self expectationWithDescription:@"Batch fetching completed"]; @@ -1020,7 +1020,7 @@ [view layoutIfNeeded]; // Wait for ASDK reload to finish - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; // Force UIKit to read updated data & range controller to update and account for it [cn.view layoutIfNeeded]; [self waitForExpectationsWithTimeout:60 handler:nil]; @@ -1050,8 +1050,17 @@ // Trigger the initial reload to start [window layoutIfNeeded]; + // Test the APIs that monitor ASCollectionNode update handling + XCTAssertTrue(cn.isProcessingUpdates, @"ASCollectionNode should still be processing updates after initial layoutIfNeeded call (reloadData)"); + [cn onDidFinishProcessingUpdates:^{ + XCTAssertTrue(!cn.isProcessingUpdates, @"ASCollectionNode should no longer be processing updates inside -onDidFinishProcessingUpdates: block"); + }]; + // Wait for ASDK reload to finish - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; + + XCTAssertTrue(!cn.isProcessingUpdates, @"ASCollectionNode should no longer be processing updates after -wait call"); + // Force UIKit to read updated data & range controller to update and account for it [cn.view layoutIfNeeded]; @@ -1093,7 +1102,7 @@ traitCollection.containerSize = screenBounds.size; cn.primitiveTraitCollection = traitCollection; - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; [cn.view layoutIfNeeded]; // Assert that the new trait collection is picked up by all cell nodes, including ones that were not allocated but are forced to allocate now @@ -1124,7 +1133,7 @@ [window makeKeyAndVisible]; [window layoutIfNeeded]; - [cn waitUntilAllUpdatesAreCommitted]; + [cn waitUntilAllUpdatesAreProcessed]; for (NSInteger i = 0; i < itemCount; i++) { ASTextCellNodeWithSetSelectedCounter *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; XCTAssert(node.automaticallyManagesSubnodes, @"Expected test cell node to use automatic subnode management. Can modify the test with a different class if needed."); diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index e7388ccf78..2bf7936418 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -610,7 +610,7 @@ [UITableView as_recordEditingCallsIntoArray:selectors]; XCTAssertGreaterThan(node.numberOfSections, 0); - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; XCTAssertGreaterThan(node.view.numberOfSections, 0); // The first reloadData call helps prevent UITableView from calling it multiple times while ASDataController is working. @@ -635,13 +635,13 @@ // Load initial data. XCTAssertGreaterThan(node.numberOfSections, 0); - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; XCTAssertGreaterThan(node.view.numberOfSections, 0); // Reload data. [UITableView as_recordEditingCallsIntoArray:selectors]; [node reloadData]; - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; // Assert that the beginning of the call pattern is correct. // There is currently noise that comes after that we will allow for this test. @@ -668,7 +668,7 @@ // Trigger data load BEFORE first layout pass, to ensure constrained size is correct. XCTAssertGreaterThan(node.numberOfSections, 0); - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; ASSizeRange expectedSizeRange = ASSizeRangeMake(CGSizeMake(cellWidth, 0)); expectedSizeRange.max.height = CGFLOAT_MAX; @@ -703,7 +703,7 @@ // So we need to force a new layout pass so that the table will pick up a new constrained size and apply to its node. [node setNeedsLayout]; [node.view layoutIfNeeded]; - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; UITableViewCell *cell = [node.view cellForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertNotNil(cell); @@ -758,7 +758,7 @@ [window makeKeyAndVisible]; [window layoutIfNeeded]; - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; XCTAssertEqual(node.view.numberOfSections, NumberOfSections); ASXCTAssertEqualRects(CGRectMake(0, 32, 375, 44), [node rectForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]], @"This text requires very specific geometry. The rect for the first row should match up."); @@ -812,7 +812,7 @@ node.dataSource = ds; [node.view layoutIfNeeded]; - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; CGFloat rowHeight = [node.view rectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]].size.height; // Scroll to row (0,1) + 10pt node.contentOffset = CGPointMake(0, rowHeight + 10); @@ -825,7 +825,7 @@ [node deleteRowsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:i]] withRowAnimation:UITableViewRowAnimationAutomatic]; } } completion:nil]; - [node waitUntilAllUpdatesAreCommitted]; + [node waitUntilAllUpdatesAreProcessed]; // Now that row (0,0) is deleted, we should have slid up to be at just 10 // i.e. we should have subtracted the deleted row height from our content offset. From 19a9d29aa873edcf5624bed7fa9ee86662da0bfd Mon Sep 17 00:00:00 2001 From: Yan S Date: Mon, 28 Aug 2017 11:49:41 -0700 Subject: [PATCH 26/26] SEP-491 prerequisite: add textViewShouldBeginEditing: to ASEditableTextNodeDelegate (#535) * SEP-491 prerequisite: add textViewShouldBeginEditing: to ASEditableTextNodeDelegate * - added entry to CHANGELOG.md, addressed nit --- CHANGELOG.md | 1 + Source/ASEditableTextNode.h | 7 +++++++ Source/ASEditableTextNode.mm | 14 ++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba86ee3b9..7deeff472a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Fix retain cycle between ASImageNode and PINAnimatedImage [Phil Larson](https://github.com/plarson) [#520](https://github.com/TextureGroup/Texture/pull/520) - Change the API for disabling logging from a compiler flag to a runtime C function ASDisableLogging(). [Adlai Holler](https://github.com/Adlai-Holler) [#528](https://github.com/TextureGroup/Texture/pull/528) - Table and collection views to consider content inset when calculating (default) element size range [Huy Nguyen](https://github.com/nguyenhuy) [#525](https://github.com/TextureGroup/Texture/pull/525) +- [ASEditableTextNode] added -editableTextNodeShouldBeginEditing to ASEditableTextNodeDelegate to mirror the corresponding method from UITextViewDelegate. [Yan S.](https://github.com/yans) [#535](https://github.com/TextureGroup/Texture/pull/535) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASEditableTextNode.h b/Source/ASEditableTextNode.h index db310f06b9..31dbfe7d03 100644 --- a/Source/ASEditableTextNode.h +++ b/Source/ASEditableTextNode.h @@ -158,6 +158,13 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASEditableTextNodeDelegate @optional +/** + @abstract Asks the delegate if editing should begin for the text node. + @param editableTextNode An editable text node. + @discussion YES if editing should begin; NO if editing should not begin -- the default returns YES. + */ +- (BOOL)editableTextNodeShouldBeginEditing:(ASEditableTextNode *)editableTextNode; + /** @abstract Indicates to the delegate that the text node began editing. @param editableTextNode An editable text node. diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index fd35ea5e1f..d68182ed16 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -699,6 +699,12 @@ } #pragma mark - UITextView Delegate +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +{ + // Delegateify. + return [self _delegateShouldBeginEditing]; +} + - (void)textViewDidBeginEditing:(UITextView *)textView { // Delegateify. @@ -793,6 +799,14 @@ } #pragma mark - +- (BOOL)_delegateShouldBeginEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeShouldBeginEditing:)]) { + return [_delegate editableTextNodeShouldBeginEditing:self]; + } + return YES; +} + - (void)_delegateDidBeginEditing { if ([_delegate respondsToSelector:@selector(editableTextNodeDidBeginEditing:)])