diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 056424d52a..45c9c4df3f 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 */, @@ -2254,6 +2261,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 */, @@ -2289,7 +2297,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 9f85d51016..7deeff472a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,34 @@ ## 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) -- 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) +- [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) - 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) +- [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) +- 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.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/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$" +] diff --git a/Source/ASCellNode.h b/Source/ASCellNode.h index eb7b19b6fe..3c74366a99 100644 --- a/Source/ASCellNode.h +++ b/Source/ASCellNode.h @@ -125,14 +125,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 viewModel; +@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 655643bdb8..7710120652 100644 --- a/Source/ASCellNode.mm +++ b/Source/ASCellNode.mm @@ -172,9 +172,9 @@ } } -- (BOOL)canUpdateToViewModel:(id)viewModel +- (BOOL)canUpdateToNodeModel:(id)nodeModel { - return [self.viewModel class] == [viewModel class]; + return [self.nodeModel class] == [nodeModel class]; } - (NSIndexPath *)indexPath diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 1a6b9bda31..542a8bf444 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -130,6 +130,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. * @@ -240,10 +254,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. @@ -421,15 +464,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. @@ -489,9 +532,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 @@ -525,7 +570,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 c253045a01..5db5f8f63d 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -50,6 +50,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 @@ -62,6 +64,8 @@ _allowsSelection = YES; _allowsMultipleSelection = NO; _inverted = NO; + _contentOffset = CGPointZero; + _animatesContentOffset = NO; } return self; } @@ -190,6 +194,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. } @@ -435,6 +441,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; @@ -595,10 +625,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 @@ -676,7 +706,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) { @@ -684,6 +728,11 @@ } } +- (void)waitUntilAllUpdatesAreCommitted +{ + [self waitUntilAllUpdatesAreProcessed]; +} + - (void)reloadDataWithCompletion:(void (^)())completion { ASDisplayNodeAssertMainThread(); @@ -709,7 +758,7 @@ { ASDisplayNodeAssertMainThread(); [self reloadData]; - [self waitUntilAllUpdatesAreCommitted]; + [self waitUntilAllUpdatesAreProcessed]; } - (void)relayoutItems diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index 510a0bcaa2..45dc040c5e 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -143,6 +143,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 * @@ -293,9 +298,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. @@ -409,6 +416,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/ASCollectionView.mm b/Source/ASCollectionView.mm index 4217beadcd..52a7e53f9f 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; @@ -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 @@ -431,7 +441,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) { @@ -1536,10 +1546,6 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section return [self.layoutInspector scrollableDirections]; } -- (ASScrollDirection)flowLayoutScrollableDirections:(UICollectionViewFlowLayout *)flowLayout { - return (flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal) ? ASScrollDirectionHorizontalDirections : ASScrollDirectionVerticalDirections; -} - - (void)layoutSubviews { if (_cellsForLayoutUpdates.count > 0) { @@ -1662,14 +1668,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 @@ -2197,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/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+Layout.mm b/Source/ASDisplayNode+Layout.mm index 66a5687685..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]; @@ -850,8 +857,8 @@ ASPrimitiveTraitCollectionDeprecatedImplementation if (pendingLayoutTransition != nil) { [self _setCalculatedDisplayNodeLayout:pendingLayoutTransition.pendingLayout]; [self _completeLayoutTransition:pendingLayoutTransition]; + [self _pendingLayoutTransitionDidComplete]; } - [self _pendingLayoutTransitionDidComplete]; } /** 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 148317f8da..55be0bfb8c 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -3095,12 +3095,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 @@ -3172,6 +3173,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/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:)]) diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 11117f1947..e681c9d0d9 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -72,7 +72,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]; }; } } diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index a56333d3c2..1861199a7d 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -176,7 +176,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. diff --git a/Source/ASPagerNode.h b/Source/ASPagerNode.h index 61e16bb6b0..cd227a8b51 100644 --- a/Source/ASPagerNode.h +++ b/Source/ASPagerNode.h @@ -77,6 +77,9 @@ NS_ASSUME_NONNULL_BEGIN @end +/** + * A horizontal, paging collection node. + */ @interface ASPagerNode : ASCollectionNode /** @@ -86,6 +89,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 ef5108e606..aa9852d506 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -30,7 +30,7 @@ #import #import -@interface ASPagerNode () +@interface ASPagerNode () { __weak id _pagerDataSource; ASPagerNodeProxy *_proxyDataSource; @@ -67,6 +67,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; } @@ -76,7 +77,7 @@ ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionHorizontalDirections]; self = [super initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; if (self) { - layoutDelegate.sizeProvider = self; + layoutDelegate.propertiesProvider = self; } return self; } @@ -113,7 +114,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 @@ -138,12 +147,12 @@ return indexPath.row; } -#pragma mark - ASCollectionGalleryLayoutSizeProviding +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding - (CGSize)sizeForElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - return self.bounds.size; + return [self pageSize]; } #pragma mark - ASCollectionDataSource @@ -180,7 +189,7 @@ } #pragma clang diagnostic pop - return ASSizeRangeMake(self.bounds.size); + return ASSizeRangeMake([self pageSize]); } #pragma mark - Data Source Proxy diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index dec0be7eb8..110e3acd2f 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. @@ -193,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. @@ -685,6 +728,12 @@ 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 #endif diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index 11ed40ac39..5d11775b70 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -44,6 +44,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 @@ -59,6 +61,8 @@ _allowsMultipleSelectionDuringEditing = NO; _inverted = NO; _leadingScreensForBatching = 2; + _contentOffset = CGPointZero; + _animatesContentOffset = NO; _automaticallyAdjustsContentOffset = NO; } return self; @@ -121,6 +125,7 @@ if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } + [view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset]; } } @@ -233,6 +238,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; @@ -702,7 +734,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) { @@ -710,6 +756,11 @@ ASLayoutElementCollectionTableSetTraitCollection(_environmentStateLock) } } +- (void)waitUntilAllUpdatesAreCommitted +{ + [self waitUntilAllUpdatesAreProcessed]; +} + #pragma mark - Debugging (Private) - (NSMutableArray *)propertiesForDebugDescription diff --git a/Source/ASTableView.h b/Source/ASTableView.h index 14d183ccf1..b2285a846a 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -69,6 +69,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 @@ -86,6 +90,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. * @@ -140,12 +150,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."); @@ -217,9 +221,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."); @@ -243,6 +249,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/ASTableView.mm b/Source/ASTableView.mm index 65be828800..6801a97a52 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -346,6 +346,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; } @@ -542,7 +549,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASDisplayNodeAssertMainThread(); [self reloadData]; - [_dataController waitUntilAllUpdatesAreCommitted]; + [_dataController waitUntilAllUpdatesAreProcessed]; } - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated @@ -729,6 +736,16 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } +- (BOOL)isProcessingUpdates +{ + return [_dataController isProcessingUpdates]; +} + +- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +{ + [_dataController onDidFinishProcessingUpdates:completion]; +} + - (void)waitUntilAllUpdatesAreCommitted { ASDisplayNodeAssertMainThread(); @@ -737,15 +754,16 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); return; } - - [_dataController waitUntilAllUpdatesAreCommitted]; + + [_dataController waitUntilAllUpdatesAreProcessed]; } - (void)layoutSubviews { // 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; @@ -1622,7 +1640,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/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. 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; } diff --git a/Source/Details/ASCollectionElement.h b/Source/Details/ASCollectionElement.h index 5dfea067cd..6b866d68b8 100644 --- a/Source/Details/ASCollectionElement.h +++ b/Source/Details/ASCollectionElement.h @@ -32,9 +32,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 6b4f197cbd..dc41895cf2 100644 --- a/Source/Details/ASCollectionElement.mm +++ b/Source/Details/ASCollectionElement.mm @@ -32,7 +32,7 @@ ASCellNode *_node; } -- (instancetype)initWithViewModel:(id)viewModel +- (instancetype)initWithNodeModel:(id)nodeModel nodeBlock:(ASCellNodeBlock)nodeBlock supplementaryElementKind:(NSString *)supplementaryElementKind constrainedSize:(ASSizeRange)constrainedSize @@ -42,7 +42,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; @@ -65,7 +65,7 @@ node.owningNode = _owningNode; node.collectionElement = self; ASTraitCollectionPropagateDown(node, _traitCollection); - node.viewModel = _viewModel; + node.nodeModel = _nodeModel; _node = node; } return _node; diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 74de7b4264..5a0d9c02d8 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -78,8 +78,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 d6ea225cff..ff49473865 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.h +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN -@protocol ASCollectionGalleryLayoutSizeProviding +@protocol ASCollectionGalleryLayoutPropertiesProviding /** * Returns the fixed size of each and every element. @@ -32,6 +32,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 /** @@ -42,8 +87,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.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm new file mode 100644 index 0000000000..690a5ac62f --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -0,0 +1,144 @@ +// +// 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 +// + +#ifndef MINIMAL_ASDK + +#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 + +#endif diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index 0868a68235..44e639b846 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -29,6 +29,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 83bae5fd64..ad8b8177cf 100644 --- a/Source/Details/ASCollectionLayoutContext.m +++ b/Source/Details/ASCollectionLayoutContext.m @@ -24,13 +24,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 @@ -40,6 +38,7 @@ self = [super init]; if (self) { _viewportSize = viewportSize; + _initialContentOffset = initialContentOffset; _scrollableDirections = scrollableDirections; _elements = elements; _layoutDelegateClass = layoutDelegateClass; @@ -59,6 +58,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/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index 52e60aa4da..abea9b42e9 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -25,6 +25,8 @@ NS_ASSUME_NONNULL_BEGIN +typedef ASCollectionElement * _Nullable (^ASCollectionLayoutStateGetElementBlock)(ASLayout *); + @interface NSMapTable (ASCollectionLayoutConvenience) + (NSMapTable *)elementToLayoutAttributesTable; @@ -72,11 +74,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 1bcf6b703c..a270a16d67 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -22,9 +22,12 @@ #import #import #import +#import #import #import +#import + @implementation NSMapTable (ASCollectionLayoutConvenience) + (NSMapTable *)elementToLayoutAttributesTable @@ -50,30 +53,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]; @@ -150,9 +172,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/Details/ASCollectionViewLayoutInspector.m b/Source/Details/ASCollectionViewLayoutInspector.m index 04809fe63f..4b2cc7c49f 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); diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 05709771a2..00dd9b36b9 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -79,7 +79,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 @@ -255,7 +255,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 0829aea2aa..ea6258a6c1 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -332,18 +332,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; }; @@ -362,7 +362,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 @@ -432,13 +432,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(); @@ -565,7 +594,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/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/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/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 0c0e313eab..ffc9b28430 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -72,11 +72,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,15 +95,19 @@ 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; - // 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, 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; } @@ -140,8 +146,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 @@ -247,11 +254,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; @@ -259,6 +262,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/ASCollectionLayoutContext+Private.h b/Source/Private/ASCollectionLayoutContext+Private.h index 8e6e3ccffe..6a313d0c98 100644 --- a/Source/Private/ASCollectionLayoutContext+Private.h +++ b/Source/Private/ASCollectionLayoutContext+Private.h @@ -30,6 +30,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/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h index 1423b077cb..3181610daa 100644 --- a/Source/Private/ASCollectionLayoutState+Private.h +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -24,9 +24,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 diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index 06c0e65fbb..1602a5c11b 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -76,6 +76,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id layoutInspector; +@property (nonatomic, assign) CGPoint contentOffset; + /** * Tuning parameters for a range type in full mode. * @@ -293,6 +295,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/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; 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/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/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/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. diff --git a/Tests/ASCollectionModernDataSourceTests.m b/Tests/ASCollectionModernDataSourceTests.m index ec97e8c644..ff084a7b16 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]; @@ -71,7 +71,7 @@ - (void)tearDown { - [collectionNode waitUntilAllUpdatesAreCommitted]; + [collectionNode waitUntilAllUpdatesAreProcessed]; [super tearDown]; } @@ -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.viewModel, 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; } 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/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/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/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index 6c53fc8498..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,10 +812,10 @@ 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.view.contentOffset = CGPointMake(0, rowHeight + 10); + node.contentOffset = CGPointMake(0, rowHeight + 10); [node performBatchAnimated:NO updates:^{ // Delete row 0 from all sections. @@ -825,11 +825,11 @@ [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. - XCTAssertEqual(node.view.contentOffset.y, 10); + XCTAssertEqual(node.contentOffset.y, 10); } @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 0000000000..188cd6007e Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentCenter@2x.png differ diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..18f614cf47 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentEnd@2x.png differ 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 0000000000..188cd6007e Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceAround@2x.png differ diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..20d303b586 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentSpaceBetween@2x.png differ 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 0000000000..20d303b586 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingOverflow_alignContentStart@2x.png differ 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 0000000000..27b26426db Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentCenter@2x.png differ 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 0000000000..740100fce2 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentEnd@2x.png differ diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..40bae9ef39 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceAround@2x.png differ 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 0000000000..a60f63f145 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentSpaceBetween@2x.png differ diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000..a89a941bf1 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStart@2x.png differ 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 0000000000..461c786922 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentAndLineSpacingUnderflow_alignContentStretch@2x.png differ diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png new file mode 100644 index 0000000000..31df2f6199 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacings@2x.png differ diff --git a/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png new file mode 100644 index 0000000000..1824a25085 Binary files /dev/null and b/Tests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testFlexWrapWithItemSpacingsBeingResetOnNewLines@2x.png differ 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' } 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. 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:` 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 diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 36391e8f37..fa093f80ad 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -16,14 +16,14 @@ // #import "ViewController.h" - +#import "AppDelegate.h" #import #import "SupplementaryNode.h" #import "ItemNode.h" #define ASYNC_COLLECTION_LAYOUT 0 -@interface ViewController () +@interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @property (nonatomic, strong) NSArray *data; @@ -48,13 +48,15 @@ #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]; 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; @@ -82,18 +84,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 @@ -108,7 +110,7 @@ [self.collectionNode reloadData]; } -#pragma mark - ASCollectionGalleryLayoutSizeProviding +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding - (CGSize)sizeForElements:(ASElementMap *)elements {