From 4ba6f451f64c95568f435557d2898820a3e07ec7 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 7 Sep 2017 19:25:42 +0100 Subject: [PATCH 01/86] [Cleanup] Remove deprecated APIs (#529) * Remove preferredFrameSize * Remove -measure: * Remove -measureWithSizeRange: * Remove ASLayoutable * Remove .name * Remove deprecated style forwardings That includes following properties that are declared on ASDisplayNode and ASLayoutSpec: spacingBefore, spacingAfter, flexGrow, flexShrink, flexBasis, alignSelf, ascender, descender, sizeRange and layoutPosition. * Remove usesImplicitHierarchyManagement * Remove deprecated range update callbacks: -visibilityDidChange: -visibleStateDidChange: -displayStateDidChange: -loadStateDidChange: * Remove -clearFetchedData * Remove -cancelLayoutTransitionsInProgress * Remve ASDisplayNode+Deprecated.h * Remove ASLayoutRangeTypeRender and ASLayoutRangeTypeFetchData * Remove -[ASTableView clearContents] * Remove reloadDataImmediately * Remove ASStaticLayoutSpec * Remove ASDimensionDeprecated * Remove optional -pagerNode:constrainedSizeForNodeAtIndex: delegate method in ASPagerDelegate * Remove suppressesInvalidCollectionUpdateExceptions * Remove -[ASCollectionViewLayoutInspector initWithCollectionView] * Remove ASVideoPlayerNode.loadAssetWhenNodeBecomesVisible * Update CHANGELOG * Update license of ASLayoutSpecTests.m * Update examples/PagerNode * Remove ASEnvironmentTraitCollection * Remove -ASViewController.nodeConstrainedSize * More on removing ASLayoutable --- AsyncDisplayKit.xcodeproj/project.pbxproj | 12 -- CHANGELOG.md | 1 + Source/ASCollectionNode.h | 10 -- Source/ASCollectionNode.mm | 7 - Source/ASCollectionView.h | 8 - Source/ASDisplayNode+Beta.h | 13 -- Source/ASDisplayNode+Deprecated.h | 142 ------------------ Source/ASDisplayNode+Layout.mm | 13 +- Source/ASDisplayNode+Subclasses.h | 8 +- Source/ASDisplayNode.h | 2 +- Source/ASDisplayNode.mm | 134 +---------------- Source/ASPagerNode.h | 12 -- Source/ASPagerNode.m | 18 --- Source/ASTableView.h | 14 -- Source/ASTableView.mm | 7 - Source/ASVideoPlayerNode.h | 8 - Source/ASVideoPlayerNode.mm | 22 --- Source/ASViewController.h | 13 -- Source/ASViewController.mm | 6 - Source/AsyncDisplayKit.h | 2 - .../Details/ASCollectionViewLayoutInspector.h | 3 - .../Details/ASCollectionViewLayoutInspector.m | 7 - Source/Details/ASLayoutRangeType.h | 3 - Source/Details/ASTraitCollection.h | 21 --- Source/Layout/ASAbsoluteLayoutElement.h | 6 - Source/Layout/ASAbsoluteLayoutSpec.h | 9 -- Source/Layout/ASAbsoluteLayoutSpec.mm | 15 -- Source/Layout/ASDimensionDeprecated.h | 102 ------------- Source/Layout/ASDimensionDeprecated.mm | 102 ------------- Source/Layout/ASLayout.h | 17 --- Source/Layout/ASLayout.mm | 24 --- Source/Layout/ASLayoutElement.h | 16 -- Source/Layout/ASLayoutElement.mm | 18 --- Source/Layout/ASLayoutElementPrivate.h | 136 ----------------- Source/Layout/ASLayoutSpec.h | 6 - Source/Layout/ASLayoutSpec.mm | 20 --- Source/Private/ASDisplayNodeInternal.h | 2 - Source/Private/ASTableView+Undeprecated.h | 8 - .../Layout/ASStackUnpositionedLayout.mm | 2 +- .../Private/_ASCollectionGalleryLayoutItem.mm | 1 - Source/Private/_ASHierarchyChangeSet.mm | 14 +- Tests/ASCollectionViewTests.mm | 2 +- Tests/ASDisplayNodeLayoutTests.mm | 2 +- Tests/ASDisplayNodeTests.mm | 22 --- Tests/ASLayoutSpecTests.m | 15 +- Tests/ASTableViewTests.mm | 3 +- Tests/ASTableViewThrashTests.m | 3 +- examples/PagerNode/Sample/PageNode.m | 26 ++-- 48 files changed, 42 insertions(+), 1015 deletions(-) delete mode 100644 Source/ASDisplayNode+Deprecated.h delete mode 100644 Source/Layout/ASDimensionDeprecated.h delete mode 100644 Source/Layout/ASDimensionDeprecated.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c02b9fd526..41389f7219 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -106,7 +106,6 @@ 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */; }; 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */; }; 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 683489281D70DE3400327501 /* ASDisplayNode+Deprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; 68355B3E1CB57A60001D4E68 /* ASPINRemoteImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */; }; 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.m in Sources */ = {isa = PBXBuildFile; fileRef = 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.m */; }; @@ -129,8 +128,6 @@ 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */; }; 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */; }; 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 690C35631E055C7B00069B91 /* ASDimensionInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 690C35671E0567C600069B91 /* ASDimensionDeprecated.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690C35651E0567C600069B91 /* ASDimensionDeprecated.mm */; }; - 690C356B1E05680300069B91 /* ASDimensionDeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = 690C356A1E05680300069B91 /* ASDimensionDeprecated.h */; settings = {ATTRIBUTES = (Public, ); }; }; 690ED58E1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 690ED5961E36D118000627C0 /* ASControlNode+tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 690ED5921E36D118000627C0 /* ASControlNode+tvOS.h */; settings = {ATTRIBUTES = (Private, ); }; }; 690ED5981E36D118000627C0 /* ASControlNode+tvOS.m in Sources */ = {isa = PBXBuildFile; fileRef = 690ED5931E36D118000627C0 /* ASControlNode+tvOS.m */; }; @@ -627,7 +624,6 @@ 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableLayoutController.m; sourceTree = ""; }; 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; - 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Deprecated.h"; sourceTree = ""; }; 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPINRemoteImageDownloader.m; sourceTree = ""; }; 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = ""; }; @@ -652,8 +648,6 @@ 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASObjectDescriptionHelpers.m; sourceTree = ""; }; 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDimensionInternal.mm; sourceTree = ""; }; 690C35631E055C7B00069B91 /* ASDimensionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDimensionInternal.h; sourceTree = ""; }; - 690C35651E0567C600069B91 /* ASDimensionDeprecated.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDimensionDeprecated.mm; sourceTree = ""; }; - 690C356A1E05680300069B91 /* ASDimensionDeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDimensionDeprecated.h; sourceTree = ""; }; 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementStylePrivate.h; sourceTree = ""; }; 690ED5921E36D118000627C0 /* ASControlNode+tvOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+tvOS.h"; sourceTree = ""; }; 690ED5931E36D118000627C0 /* ASControlNode+tvOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASControlNode+tvOS.m"; sourceTree = ""; }; @@ -1081,7 +1075,6 @@ 058D09D9195D050800B7D73C /* ASDisplayNode.mm */, 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */, 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */, - 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */, 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */, CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */, @@ -1524,8 +1517,6 @@ ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */, ACF6ED071B17843500DA7C62 /* ASDimension.h */, ACF6ED081B17843500DA7C62 /* ASDimension.mm */, - 690C356A1E05680300069B91 /* ASDimensionDeprecated.h */, - 690C35651E0567C600069B91 /* ASDimensionDeprecated.mm */, 690C35631E055C7B00069B91 /* ASDimensionInternal.h */, 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */, ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */, @@ -1731,8 +1722,6 @@ 696F01EC1DD2AF450049FBD5 /* ASEventLog.h in Headers */, 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */, 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */, - 690C356B1E05680300069B91 /* ASDimensionDeprecated.h in Headers */, - 683489281D70DE3400327501 /* ASDisplayNode+Deprecated.h in Headers */, 698371DB1E4379CD00437585 /* ASNodeController+Beta.h in Headers */, 6907C2581DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h in Headers */, 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */, @@ -2204,7 +2193,6 @@ CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.m in Sources */, 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.m in Sources */, B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, - 690C35671E0567C600069B91 /* ASDimensionDeprecated.mm in Sources */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, CCA282B91E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.m in Sources */, 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 7deeff472a..d3d6a3d332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - 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) +- [Breaking] Remove APIs that have been deprecated since 2.0 and/or for at least 6 months [Huy Nguyen](https://github.com/nguyenhuy) [#529](https://github.com/TextureGroup/Texture/pull/529) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 352867e9f5..68e5d752f3 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -524,16 +524,6 @@ NS_ASSUME_NONNULL_BEGIN @interface ASCollectionNode (Deprecated) -/** - * Reload everything from scratch, destroying the working range and all cached nodes. - * - * @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 waitUntilAllUpdatesAreProcessed instead. - */ -- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use -reloadData / -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreProcessed instead."); - - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -waitUntilAllUpdatesAreProcessed."); @end diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 5bb12befd4..e5cde4c8e6 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -753,13 +753,6 @@ [self reloadDataWithCompletion:nil]; } -- (void)reloadDataImmediately -{ - ASDisplayNodeAssertMainThread(); - [self reloadData]; - [self waitUntilAllUpdatesAreProcessed]; -} - - (void)relayoutItems { ASDisplayNodeAssertMainThread(); diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index 2a1eb18567..2c1f87984c 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -280,14 +280,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData AS_UNAVAILABLE("Use ASCollectionNode method instead."); -/** - * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UICollectionView's version and will block the main thread - * while all the cells load. - */ -- (void)reloadDataImmediately AS_UNAVAILABLE("Use ASCollectionNode method instead."); - /** * Triggers a relayout of all nodes. * diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 8e413a5a35..e8b08a9a57 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -61,19 +61,6 @@ typedef struct { @interface ASDisplayNode (Beta) -/** - * ASTableView and ASCollectionView now throw exceptions on invalid updates - * like their UIKit counterparts. If YES, these classes will log messages - * on invalid updates rather than throwing exceptions. - * - * Note that even if AsyncDisplayKit's exception is suppressed, the app may still crash - * as it proceeds with an invalid update. - * - * This property defaults to NO. It will be removed in a future release. - */ -+ (BOOL)suppressesInvalidCollectionUpdateExceptions AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled."); -+ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled."); - /** * @abstract Recursively ensures node and all subnodes are displayed. * @see Full documentation in ASDisplayNode+FrameworkPrivate.h diff --git a/Source/ASDisplayNode+Deprecated.h b/Source/ASDisplayNode+Deprecated.h deleted file mode 100644 index 696169fa2e..0000000000 --- a/Source/ASDisplayNode+Deprecated.h +++ /dev/null @@ -1,142 +0,0 @@ -// -// ASDisplayNode+Deprecated.h -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#pragma once - -#import - -@interface ASDisplayNode (Deprecated) - -/** - * @abstract The name of this node, which will be displayed in `description`. The default value is nil. - * - * @deprecated Deprecated in version 2.0: Use .debugName instead. This value will display in - * results of the -asciiArtString method (@see ASLayoutElementAsciiArtProtocol). - */ -@property (nullable, nonatomic, copy) NSString *name ASDISPLAYNODE_DEPRECATED_MSG("Use .debugName instead."); - -/** - * @abstract Provides a default intrinsic content size for calculateSizeThatFits:. This is useful when laying out - * a node that either has no intrinsic content size or should be laid out at a different size than its intrinsic content - * size. For example, this property could be set on an ASImageNode to display at a size different from the underlying - * image size. - * - * @return Try to create a CGSize for preferredFrameSize of this node from the width and height property of this node. It will return CGSizeZero if width and height dimensions are not of type ASDimensionUnitPoints. - * - * @deprecated Deprecated in version 2.0: Just calls through to set the height and width property of the node. Convert to use sizing properties instead: height, minHeight, maxHeight, width, minWidth, maxWidth. - */ -@property (nonatomic, assign, readwrite) CGSize preferredFrameSize ASDISPLAYNODE_DEPRECATED_MSG("Use .style.preferredSize instead OR set individual values with .style.height and .style.width."); - -/** - * @abstract Asks the node to measure and return the size that best fits its subnodes. - * - * @param constrainedSize The maximum size the receiver should fit in. - * - * @return A new size that fits the receiver's subviews. - * - * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the - * constraint and the result. - * - * @warning Subclasses must not override this; it calls -measureWithSizeRange: with zero min size. - * -measureWithSizeRange: caches results from -calculateLayoutThatFits:. Calling this method may - * be expensive if result is not cached. - * - * @see measureWithSizeRange: - * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] - * - * @deprecated Deprecated in version 2.0: Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout - */ -- (CGSize)measure:(CGSize)constrainedSize ASDISPLAYNODE_DEPRECATED_MSG("Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout."); - -ASLayoutElementStyleForwardingDeclaration - -/** - * @abstract Called whenever the visiblity of the node changed. - * - * @discussion Subclasses may use this to monitor when they become visible. - * - * @deprecated @see didEnterVisibleState @see didExitVisibleState - */ -- (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead."); - -/** - * @abstract Called whenever the visiblity of the node changed. - * - * @discussion Subclasses may use this to monitor when they become visible. - * - * @deprecated @see didEnterVisibleState @see didExitVisibleState - */ -- (void)visibleStateDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead."); - -/** - * @abstract Called whenever the the node has entered or exited the display state. - * - * @discussion Subclasses may use this to monitor when a node should be rendering its content. - * - * @note This method can be called from any thread and should therefore be thread safe. - * - * @deprecated @see didEnterDisplayState @see didExitDisplayState - */ -- (void)displayStateDidChange:(BOOL)inDisplayState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterDisplayState / -didExitDisplayState instead."); - -/** - * @abstract Called whenever the the node has entered or left the load state. - * - * @discussion Subclasses may use this to monitor data for a node should be loaded, either from a local or remote source. - * - * @note This method can be called from any thread and should therefore be thread safe. - * - * @deprecated @see didEnterPreloadState @see didExitPreloadState - */ -- (void)loadStateDidChange:(BOOL)inLoadState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState / -didExitPreloadState instead."); - -/** - * @abstract Cancels all performing layout transitions. Can be called on any thread. - * - * @deprecated Deprecated in version 2.0: Use cancelLayoutTransition - */ -- (void)cancelLayoutTransitionsInProgress ASDISPLAYNODE_DEPRECATED_MSG("Use -cancelLayoutTransition instead."); - -/** - * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or - * absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method. - * - * @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence - * or absence of subnodes is completely determined in its layoutSpecThatFits: method. - * - * @deprecated Deprecated in version 2.0: Use automaticallyManagesSubnodes - */ -@property (nonatomic, assign) BOOL usesImplicitHierarchyManagement ASDISPLAYNODE_DEPRECATED_MSG("Set .automaticallyManagesSubnodes instead."); - -/** - * @abstract Indicates that the node should fetch any external data, such as images. - * - * @discussion Subclasses may override this method to be notified when they should begin to preload. Fetching - * should be done asynchronously. The node is also responsible for managing the memory of any data. - * The data may be remote and accessed via the network, but could also be a local database query. - */ -- (void)fetchData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState instead."); - -/** - * Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node. - * - * @discussion This will not clear data recursively for all subnodes. Either call -recursivelyClearPreloadedData or - * selectively clear fetched data. - */ -- (void)clearFetchedData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didExitPreloadState instead."); - -@end diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 5674997d00..6727cd23ba 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -57,15 +57,6 @@ #pragma mark Measurement Pass - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // For now we just call the deprecated measureWithSizeRange: method to not break old API - return [self measureWithSizeRange:constrainedSize]; -#pragma clang diagnostic pop -} - -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; } @@ -127,8 +118,6 @@ ASLayoutElementStyleExtensibilityForwarding return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; } -ASPrimitiveTraitCollectionDeprecatedImplementation - #pragma mark - ASLayoutElementAsciiArtProtocol - (NSString *)asciiArtString @@ -938,7 +927,7 @@ ASPrimitiveTraitCollectionDeprecatedImplementation // Grab lock after calling out to subclass ASDN::MutexLocker l(__instanceLock__); - // We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go. + // We generate placeholders at -layoutThatFits: time so that a node is guaranteed to have a placeholder ready to go. // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. if (_placeholderEnabled && !_placeholderImage && [self _locked_displaysAsynchronously]) { diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index b506124d48..9cc98aab39 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -116,12 +116,12 @@ NS_ASSUME_NONNULL_BEGIN * @discussion For node subclasses that implement manual layout (e.g., they have a custom -layout method), * calculatedLayout may be accessed on subnodes to retrieved cached information about their size. * This allows -layout to be very fast, saving time on the main thread. - * Note: .calculatedLayout will only be set for nodes that have had -measure: called on them. - * For manual layout, make sure you call -measure: in your implementation of -calculateSizeThatFits:. + * Note: .calculatedLayout will only be set for nodes that have had -layoutThatFits: called on them. + * For manual layout, make sure you call -layoutThatFits: in your implementation of -calculateSizeThatFits:. * * For node subclasses that use automatic layout (e.g., they implement -layoutSpecThatFits:), * it is typically not necessary to use .calculatedLayout at any point. For these nodes, - * the ASLayoutSpec implementation will automatically call -measureWithSizeRange: on all of the subnodes, + * the ASLayoutSpec implementation will automatically call -layoutThatFits: on all of the subnodes, * and the ASDisplayNode base class implementation of -layout will automatically make use of .calculatedLayout on the subnodes. * * @return Layout that wraps calculated size returned by -calculateSizeThatFits: (in manual layout mode), @@ -183,7 +183,7 @@ NS_ASSUME_NONNULL_BEGIN * or -calculateSizeThatFits:, whichever method is overriden. Subclasses rarely need to override this method, * override -layoutSpecThatFits: or -calculateSizeThatFits: instead. * - * @note This method should not be called directly outside of ASDisplayNode; use -measure: or -calculatedLayout instead. + * @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or -calculatedLayout instead. */ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize; diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 24347d09b5..c221e3c6ec 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -785,7 +785,7 @@ extern NSInteger const ASDefaultDrawingPriority; * @abstract Return the calculated size. * * @discussion Ideal for use by subclasses in -layout, having already prompted their subnodes to calculate their size by - * calling -measure: on them in -calculateLayoutThatFits. + * calling -layoutThatFits: on them in -calculateLayoutThatFits. * * @return Size already calculated by -calculateLayoutThatFits:. * diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index e1ce6f8bd9..f91b33849e 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -20,7 +20,6 @@ #import #import #import -#import #import #import #import @@ -77,19 +76,8 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; @synthesize threadSafeBounds = _threadSafeBounds; -static BOOL suppressesInvalidCollectionUpdateExceptions = NO; static std::atomic_bool storesUnflattenedLayouts = ATOMIC_VAR_INIT(NO); -+ (BOOL)suppressesInvalidCollectionUpdateExceptions -{ - return suppressesInvalidCollectionUpdateExceptions; -} - -+ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses -{ - suppressesInvalidCollectionUpdateExceptions = suppresses; -} - BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) { return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); @@ -182,12 +170,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { overrides |= ASDisplayNodeMethodOverrideCalcSizeThatFits; } - if (ASDisplayNodeSubclassOverridesSelector(c, @selector(fetchData))) { - overrides |= ASDisplayNodeMethodOverrideFetchData; - } - if (ASDisplayNodeSubclassOverridesSelector(c, @selector(clearFetchedData))) { - overrides |= ASDisplayNodeMethodOverrideClearFetchedData; - } return overrides; } @@ -202,8 +184,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method.", classString); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measure:)), @"Subclass %@ must not override measure: method", classString); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measureWithSizeRange:)), @"Subclass %@ must not override measureWithSizeRange: method. Instead override calculateLayoutThatFits:", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:)), @"Subclass %@ must not override layoutThatFits: method. Instead override calculateLayoutThatFits:.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:parentSize:)), @"Subclass %@ must not override layoutThatFits:parentSize method. Instead override calculateLayoutThatFits:.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method.", classString); @@ -906,7 +886,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (CGRectEqualToRect(bounds, CGRectZero)) { // Performing layout on a zero-bounds view often results in frame calculations // with negative sizes after applying margins, which will cause - // measureWithSizeRange: on subnodes to assert. + // layoutThatFits: on subnodes to assert. as_log_debug(OS_LOG_DISABLED, "Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); return; } @@ -2935,13 +2915,6 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); [_interfaceStateDelegate didEnterPreloadState]; - - if (_methodOverrides & ASDisplayNodeMethodOverrideFetchData) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [self fetchData]; -#pragma clang diagnostic pop - } } - (void)didExitPreloadState @@ -2949,13 +2922,6 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); [_interfaceStateDelegate didExitPreloadState]; - - if (_methodOverrides & ASDisplayNodeMethodOverrideClearFetchedData) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [self clearFetchedData]; -#pragma clang diagnostic pop - } } - (void)clearContents @@ -3504,101 +3470,3 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; } @end - -#pragma mark - ASDisplayNode (Deprecated) - -@implementation ASDisplayNode (Deprecated) - -- (NSString *)name -{ - return self.debugName; -} - -- (void)setName:(NSString *)name -{ - self.debugName = name; -} - -- (void)setPreferredFrameSize:(CGSize)preferredFrameSize -{ - // Deprecated preferredFrameSize just calls through to set width and height - self.style.preferredSize = preferredFrameSize; - [self setNeedsLayout]; -} - -- (CGSize)preferredFrameSize -{ - ASLayoutSize size = self.style.preferredLayoutSize; - BOOL isPoints = (size.width.unit == ASDimensionUnitPoints && size.height.unit == ASDimensionUnitPoints); - return isPoints ? CGSizeMake(size.width.value, size.height.value) : CGSizeZero; -} - -- (BOOL)usesImplicitHierarchyManagement -{ - return self.automaticallyManagesSubnodes; -} - -- (void)setUsesImplicitHierarchyManagement:(BOOL)enabled -{ - self.automaticallyManagesSubnodes = enabled; -} - -- (CGSize)measure:(CGSize)constrainedSize -{ - return [self layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; -} - -ASLayoutElementStyleForwarding - -- (void)visibilityDidChange:(BOOL)isVisible -{ - if (isVisible) { - [self didEnterVisibleState]; - } else { - [self didExitVisibleState]; - } -} - -- (void)visibleStateDidChange:(BOOL)isVisible -{ - if (isVisible) { - [self didEnterVisibleState]; - } else { - [self didExitVisibleState]; - } -} - -- (void)displayStateDidChange:(BOOL)inDisplayState -{ - if (inDisplayState) { - [self didEnterVisibleState]; - } else { - [self didExitVisibleState]; - } -} - -- (void)loadStateDidChange:(BOOL)inLoadState -{ - if (inLoadState) { - [self didEnterPreloadState]; - } else { - [self didExitPreloadState]; - } -} - -- (void)fetchData -{ - // subclass override -} - -- (void)clearFetchedData -{ - // subclass override -} - -- (void)cancelLayoutTransitionsInProgress -{ - [self cancelLayoutTransition]; -} - -@end diff --git a/Source/ASPagerNode.h b/Source/ASPagerNode.h index 51e57898c6..2b489659ae 100644 --- a/Source/ASPagerNode.h +++ b/Source/ASPagerNode.h @@ -61,18 +61,6 @@ NS_ASSUME_NONNULL_BEGIN @end @protocol ASPagerDelegate - -@optional - -/** - * Provides the constrained size range for measuring the node at the index. - * - * @param pagerNode The sender. - * @param index The index of the node. - * @return A constrained size range for layout the node at this index. - */ -- (ASSizeRange)pagerNode:(ASPagerNode *)pagerNode constrainedSizeForNodeAtIndex:(NSInteger)index ASDISPLAYNODE_DEPRECATED_MSG("Pages in a pager node should be the exact size of the collection node (default behavior)."); - @end /** diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index 86062f6649..1ef893d9d6 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -39,9 +39,6 @@ } _pagerDataSourceFlags; __weak id _pagerDelegate; - struct { - unsigned constrainedSizeForNode:1; - } _pagerDelegateFlags; ASPagerNodeProxy *_proxyDelegate; } @@ -181,13 +178,6 @@ - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (_pagerDelegateFlags.constrainedSizeForNode) { - return [_pagerDelegate pagerNode:self constrainedSizeForNodeAtIndex:indexPath.item]; - } -#pragma clang diagnostic pop - return ASSizeRangeMake([self pageSize]); } @@ -220,15 +210,7 @@ { if (delegate != _pagerDelegate) { _pagerDelegate = delegate; - - if (delegate == nil) { - memset(&_pagerDelegateFlags, 0, sizeof(_pagerDelegateFlags)); - } else { - _pagerDelegateFlags.constrainedSizeForNode = [_pagerDelegate respondsToSelector:@selector(pagerNode:constrainedSizeForNodeAtIndex:)]; - } - _proxyDelegate = delegate ? [[ASPagerNodeProxy alloc] initWithTarget:delegate interceptor:self] : nil; - super.delegate = (id )_proxyDelegate; } } diff --git a/Source/ASTableView.h b/Source/ASTableView.h index 0a3d07ec44..04e1dbd128 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -184,14 +184,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); -/** - * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UITableView's version and will block the main thread while - * all the cells load. - */ -- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's reloadDataWithCompletion: followed by ASTableNode's -waitUntilAllUpdatesAreCommitted instead."); - /** * Triggers a relayout of all nodes. * @@ -241,12 +233,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); -/// Deprecated in 2.0. You should not call this method. -- (void)clearContents ASDISPLAYNODE_DEPRECATED_MSG("You should not call this method directly. Intead, rely on the Interstate State callback methods."); - -/// 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 diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 8f41cffac4..4606726f23 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -544,13 +544,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [self reloadDataWithCompletion:nil]; } -- (void)reloadDataImmediately -{ - ASDisplayNodeAssertMainThread(); - [self reloadData]; - [_dataController waitUntilAllUpdatesAreProcessed]; -} - - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated { if ([self validateIndexPath:indexPath]) { diff --git a/Source/ASVideoPlayerNode.h b/Source/ASVideoPlayerNode.h index e68b15c5f5..f878c152f2 100644 --- a/Source/ASVideoPlayerNode.h +++ b/Source/ASVideoPlayerNode.h @@ -44,8 +44,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL controlsDisabled; -@property (nonatomic, assign, readonly) BOOL loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state. This flag does nothing."); - #pragma mark - ASVideoNode property proxy /** * When shouldAutoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState. @@ -79,12 +77,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithAsset:(AVAsset *)asset; - (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix; -#pragma mark Lifecycle Deprecated -- (instancetype)initWithUrl:(NSURL *)url ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); -- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); -- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); -- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); - #pragma mark - Public API - (void)seekToTime:(CGFloat)percentComplete; - (void)play; diff --git a/Source/ASVideoPlayerNode.mm b/Source/ASVideoPlayerNode.mm index ca05c05898..e6f766b41f 100644 --- a/Source/ASVideoPlayerNode.mm +++ b/Source/ASVideoPlayerNode.mm @@ -154,28 +154,6 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; [self addSubnode:_videoNode]; } -#pragma mark Deprecated - -- (instancetype)initWithUrl:(NSURL *)url -{ - return [self initWithURL:url]; -} - -- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible -{ - return [self initWithURL:url]; -} - -- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible -{ - return [self initWithAsset:asset]; -} - -- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible -{ - return [self initWithAsset:asset videoComposition:videoComposition audioMix:audioMix]; -} - #pragma mark - Setter / Getter - (void)setAssetURL:(NSURL *)assetURL diff --git a/Source/ASViewController.h b/Source/ASViewController.h index c2e71a2747..bdd343b30d 100644 --- a/Source/ASViewController.h +++ b/Source/ASViewController.h @@ -92,17 +92,4 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface ASViewController (Deprecated) - -/** - * The constrained size used to measure the backing node. - * - * @discussion Defaults to providing a size range that uses the view controller view's bounds as - * both the min and max definitions. Override this method to provide a custom size range to the - * backing node. - */ -- (ASSizeRange)nodeConstrainedSize AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Set the size directly to the view's frame"); - -@end - NS_ASSUME_NONNULL_END diff --git a/Source/ASViewController.mm b/Source/ASViewController.mm index d4dab1a624..2826fae9dc 100644 --- a/Source/ASViewController.mm +++ b/Source/ASViewController.mm @@ -146,12 +146,9 @@ [self propagateNewTraitCollection:traitCollection]; }]; } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" // Call layoutThatFits: to let the node prepare for a layout that will happen shortly in the layout pass of the view. // If the node's constrained size didn't change between the last layout pass it's a no-op [_node layoutThatFits:[self nodeConstrainedSize]]; -#pragma clang diagnostic pop } } @@ -296,13 +293,10 @@ ASVisibilityDepthImplementation; ASTraitCollectionPropagateDown(child, traitCollection); } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" // Once we've propagated all the traits, layout this node. // Remeasure the node with the latest constrained size – old constrained size may be incorrect. as_activity_scope_verbose(as_activity_create("Layout ASViewController node with new traits", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); [_node layoutThatFits:[self nodeConstrainedSize]]; -#pragma clang diagnostic pop } } diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 73a5288bcc..b1903945ef 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -75,7 +75,6 @@ #import #import #import -#import #import #import #import @@ -125,7 +124,6 @@ #import #import -#import #import #import diff --git a/Source/Details/ASCollectionViewLayoutInspector.h b/Source/Details/ASCollectionViewLayoutInspector.h index 3f6302fea7..ba2aadf28f 100644 --- a/Source/Details/ASCollectionViewLayoutInspector.h +++ b/Source/Details/ASCollectionViewLayoutInspector.h @@ -86,9 +86,6 @@ extern ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *colle * @warning This class is not meant to be subclassed and will be restricted in the future. */ @interface ASCollectionViewLayoutInspector : NSObject - -- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Use -init instead."); - @end NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASCollectionViewLayoutInspector.m b/Source/Details/ASCollectionViewLayoutInspector.m index 26100cec66..9b8a302d99 100644 --- a/Source/Details/ASCollectionViewLayoutInspector.m +++ b/Source/Details/ASCollectionViewLayoutInspector.m @@ -47,13 +47,6 @@ ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionVi } _delegateFlags; } -#pragma mark Lifecycle - -- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView -{ - return [self init]; -} - #pragma mark ASCollectionViewLayoutInspecting - (void)didChangeCollectionViewDelegate:(id)delegate diff --git a/Source/Details/ASLayoutRangeType.h b/Source/Details/ASLayoutRangeType.h index 45e1635441..2f2be52063 100644 --- a/Source/Details/ASLayoutRangeType.h +++ b/Source/Details/ASLayoutRangeType.h @@ -74,6 +74,3 @@ typedef NS_ENUM(NSInteger, ASLayoutRangeType) { }; static NSInteger const ASLayoutRangeTypeCount = 2; - -#define ASLayoutRangeTypeRender ASLayoutRangeTypeDisplay -#define ASLayoutRangeTypeFetchData ASLayoutRangeTypePreload diff --git a/Source/Details/ASTraitCollection.h b/Source/Details/ASTraitCollection.h index 760919c0dd..fdff5c0b17 100644 --- a/Source/Details/ASTraitCollection.h +++ b/Source/Details/ASTraitCollection.h @@ -66,10 +66,6 @@ extern NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollecti */ extern void ASTraitCollectionPropagateDown(id element, ASPrimitiveTraitCollection traitCollection); -/// For backward compatibility reasons we redefine the old layout element trait collection struct name -#define ASEnvironmentTraitCollection ASPrimitiveTraitCollection -#define ASEnvironmentTraitCollectionMakeDefault ASPrimitiveTraitCollectionMakeDefault - ASDISPLAYNODE_EXTERN_C_END /** @@ -92,13 +88,6 @@ ASDISPLAYNODE_EXTERN_C_END */ - (ASTraitCollection *)asyncTraitCollection; -/** - * Deprecated and should be replaced by the methods from above - */ -- (ASEnvironmentTraitCollection)environmentTraitCollection; -- (void)setEnvironmentTraitCollection:(ASEnvironmentTraitCollection)traitCollection; - - @end #define ASPrimitiveTraitCollectionDefaults \ @@ -111,16 +100,6 @@ ASDISPLAYNODE_EXTERN_C_END _primitiveTraitCollection = traitCollection;\ }\ -#define ASPrimitiveTraitCollectionDeprecatedImplementation \ -- (ASEnvironmentTraitCollection)environmentTraitCollection\ -{\ - return self.primitiveTraitCollection;\ -}\ -- (void)setEnvironmentTraitCollection:(ASEnvironmentTraitCollection)traitCollection\ -{\ - [self setPrimitiveTraitCollection:traitCollection];\ -}\ - #define ASLayoutElementCollectionTableSetTraitCollection(lock) \ - (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection\ {\ diff --git a/Source/Layout/ASAbsoluteLayoutElement.h b/Source/Layout/ASAbsoluteLayoutElement.h index 39ce04f1b1..384830f5e1 100644 --- a/Source/Layout/ASAbsoluteLayoutElement.h +++ b/Source/Layout/ASAbsoluteLayoutElement.h @@ -16,7 +16,6 @@ // #import -#import NS_ASSUME_NONNULL_BEGIN @@ -30,11 +29,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGPoint layoutPosition; - -#pragma mark Deprecated - -@property (nonatomic, assign) ASRelativeSizeRange sizeRange ASDISPLAYNODE_DEPRECATED; - @end NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASAbsoluteLayoutSpec.h b/Source/Layout/ASAbsoluteLayoutSpec.h index 7d218dce0e..0c250ec689 100644 --- a/Source/Layout/ASAbsoluteLayoutSpec.h +++ b/Source/Layout/ASAbsoluteLayoutSpec.h @@ -50,13 +50,4 @@ NS_ASSUME_NONNULL_BEGIN @end - -#pragma mark - Deprecated - -@interface ASStaticLayoutSpec : ASAbsoluteLayoutSpec - -+ (instancetype)staticLayoutSpecWithChildren:(NSArray> *)children AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED; - -@end - NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASAbsoluteLayoutSpec.mm b/Source/Layout/ASAbsoluteLayoutSpec.mm index c8d136a582..96ab7a705b 100644 --- a/Source/Layout/ASAbsoluteLayoutSpec.mm +++ b/Source/Layout/ASAbsoluteLayoutSpec.mm @@ -107,18 +107,3 @@ @end -#pragma mark - ASStaticLayoutSpec - -@implementation ASStaticLayoutSpec : ASAbsoluteLayoutSpec - -+ (instancetype)staticLayoutSpecWithChildren:(NSArray> *)children -{ - return [self absoluteLayoutSpecWithSizing:ASAbsoluteLayoutSpecSizingSizeToFit children:children]; -} - -- (instancetype)initWithChildren:(NSArray *)children -{ - return [super initWithSizing:ASAbsoluteLayoutSpecSizingSizeToFit children:children]; -} - -@end diff --git a/Source/Layout/ASDimensionDeprecated.h b/Source/Layout/ASDimensionDeprecated.h deleted file mode 100644 index 94483edc2e..0000000000 --- a/Source/Layout/ASDimensionDeprecated.h +++ /dev/null @@ -1,102 +0,0 @@ -// -// ASDimensionDeprecated.h -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#pragma once -#import -#import - -ASDISPLAYNODE_EXTERN_C_BEGIN -NS_ASSUME_NONNULL_BEGIN - -/** - * A dimension relative to constraints to be provided in the future. - * A ASDimension can be one of three types: - * - * "Auto" - This indicated "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. - * - * "Points" - Just a number. It will always resolve to exactly this amount. - * - * "Percent" - Multiplied to a provided parent amount to resolve a final amount. - */ -typedef NS_ENUM(NSInteger, ASRelativeDimensionType) { - /** This indicates "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. */ - ASRelativeDimensionTypeAuto, - /** Just a number. It will always resolve to exactly this amount. This is the default type. */ - ASRelativeDimensionTypePoints, - /** Multiplied to a provided parent amount to resolve a final amount. */ - ASRelativeDimensionTypeFraction, -}; - -#define ASRelativeDimension ASDimension -#define ASRelativeSize ASLayoutSize -#define ASRelativeDimensionMakeWithPoints ASDimensionMakeWithPoints -#define ASRelativeDimensionMakeWithFraction ASDimensionMakeWithFraction - -/** - * Function is deprecated. Use ASSizeRangeMake instead. - */ -extern AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMakeExactSize(CGSize size) ASDISPLAYNODE_DEPRECATED_MSG("Use ASSizeRangeMake instead."); - -/** - Expresses an inclusive range of relative sizes. Used to provide additional constraint to layout. - Used by ASStaticLayoutSpec. - */ -typedef struct { - ASLayoutSize min; - ASLayoutSize max; -} ASRelativeSizeRange; - -extern ASRelativeSizeRange const ASRelativeSizeRangeUnconstrained; - -#pragma mark - ASRelativeDimension - -extern ASDimension ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value) ASDISPLAYNODE_DEPRECATED; - -#pragma mark - ASRelativeSize - -extern ASLayoutSize ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height) ASDISPLAYNODE_DEPRECATED; - -/** Convenience constructor to provide size in points. */ -extern ASLayoutSize ASRelativeSizeMakeWithCGSize(CGSize size) ASDISPLAYNODE_DEPRECATED; - -/** Convenience constructor to provide size as a fraction. */ -extern ASLayoutSize ASRelativeSizeMakeWithFraction(CGFloat fraction) ASDISPLAYNODE_DEPRECATED; - -extern BOOL ASRelativeSizeEqualToRelativeSize(ASLayoutSize lhs, ASLayoutSize rhs) ASDISPLAYNODE_DEPRECATED; - -extern NSString *NSStringFromASRelativeSize(ASLayoutSize size) ASDISPLAYNODE_DEPRECATED; - -#pragma mark - ASRelativeSizeRange - -extern ASRelativeSizeRange ASRelativeSizeRangeMake(ASLayoutSize min, ASLayoutSize max) ASDISPLAYNODE_DEPRECATED; - -#pragma mark Convenience constructors to provide an exact size (min == max). -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeSize(ASLayoutSize exact) ASDISPLAYNODE_DEPRECATED; - -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact) ASDISPLAYNODE_DEPRECATED; - -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactFraction(CGFloat fraction) ASDISPLAYNODE_DEPRECATED; - -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeDimensions(ASRelativeDimension exactWidth, ASRelativeDimension exactHeight) ASDISPLAYNODE_DEPRECATED; - -extern BOOL ASRelativeSizeRangeEqualToRelativeSizeRange(ASRelativeSizeRange lhs, ASRelativeSizeRange rhs) ASDISPLAYNODE_DEPRECATED; - -/** Provided a parent size, compute final dimensions for this RelativeSizeRange to arrive at a SizeRange. */ -extern ASSizeRange ASRelativeSizeRangeResolve(ASRelativeSizeRange relativeSizeRange, CGSize parentSize) ASDISPLAYNODE_DEPRECATED; - -NS_ASSUME_NONNULL_END -ASDISPLAYNODE_EXTERN_C_END diff --git a/Source/Layout/ASDimensionDeprecated.mm b/Source/Layout/ASDimensionDeprecated.mm deleted file mode 100644 index bca453d08a..0000000000 --- a/Source/Layout/ASDimensionDeprecated.mm +++ /dev/null @@ -1,102 +0,0 @@ -// -// ASDimensionDeprecated.mm -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import - -ASDimension ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value) -{ - if (type == ASRelativeDimensionTypePoints) { - return ASDimensionMakeWithPoints(value); - } else if (type == ASRelativeDimensionTypeFraction) { - return ASDimensionMakeWithFraction(value); - } - - ASDisplayNodeCAssert(NO, @"ASRelativeDimensionMake does not support the given ASRelativeDimensionType"); - return ASDimensionMakeWithPoints(0); -} - -ASSizeRange ASSizeRangeMakeExactSize(CGSize size) -{ - return ASSizeRangeMake(size); -} - -ASRelativeSizeRange const ASRelativeSizeRangeUnconstrained = {}; - -#pragma mark - ASRelativeSize - -ASLayoutSize ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height) -{ - return ASLayoutSizeMake(width, height); -} - -ASLayoutSize ASRelativeSizeMakeWithCGSize(CGSize size) -{ - return ASRelativeSizeMake(ASRelativeDimensionMakeWithPoints(size.width), - ASRelativeDimensionMakeWithPoints(size.height)); -} - -ASLayoutSize ASRelativeSizeMakeWithFraction(CGFloat fraction) -{ - return ASRelativeSizeMake(ASRelativeDimensionMakeWithFraction(fraction), - ASRelativeDimensionMakeWithFraction(fraction)); -} - -BOOL ASRelativeSizeEqualToRelativeSize(ASLayoutSize lhs, ASLayoutSize rhs) -{ - return ASDimensionEqualToDimension(lhs.width, rhs.width) - && ASDimensionEqualToDimension(lhs.height, rhs.height); -} - - -#pragma mark - ASRelativeSizeRange - -ASRelativeSizeRange ASRelativeSizeRangeMake(ASLayoutSize min, ASLayoutSize max) -{ - ASRelativeSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeSize(ASLayoutSize exact) -{ - return ASRelativeSizeRangeMake(exact, exact); -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact) -{ - return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMakeWithCGSize(exact)); -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactFraction(CGFloat fraction) -{ - return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMakeWithFraction(fraction)); -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeDimensions(ASRelativeDimension exactWidth, ASRelativeDimension exactHeight) -{ - return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMake(exactWidth, exactHeight)); -} - -BOOL ASRelativeSizeRangeEqualToRelativeSizeRange(ASRelativeSizeRange lhs, ASRelativeSizeRange rhs) -{ - return ASRelativeSizeEqualToRelativeSize(lhs.min, rhs.min) && ASRelativeSizeEqualToRelativeSize(lhs.max, rhs.max); -} - -ASSizeRange ASRelativeSizeRangeResolve(ASRelativeSizeRange relativeSizeRange, - CGSize parentSize) -{ - return ASSizeRangeMake(ASLayoutSizeResolveSize(relativeSizeRange.min, parentSize, parentSize), - ASLayoutSizeResolveSize(relativeSizeRange.max, parentSize, parentSize)); -} diff --git a/Source/Layout/ASLayout.h b/Source/Layout/ASLayout.h index a0aff5b7b1..fa13719854 100644 --- a/Source/Layout/ASLayout.h +++ b/Source/Layout/ASLayout.h @@ -147,23 +147,6 @@ ASDISPLAYNODE_EXTERN_C_END @end -#pragma mark - Deprecated - -@interface ASLayout (Deprecated) - -- (id )layoutableObject ASDISPLAYNODE_DEPRECATED; - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size ASDISPLAYNODE_DEPRECATED; - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size - sublayouts:(nullable NSArray *)sublayouts AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED; - -@end - #pragma mark - Debugging @interface ASLayout (Debugging) diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 22904b8b6b..031120100c 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -353,30 +353,6 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( @end -@implementation ASLayout (Deprecation) - -- (id )layoutableObject -{ - return self.layoutElement; -} - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size -{ - return [self layoutWithLayoutElement:layoutElement size:size]; -} - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size - sublayouts:(nullable NSArray *)sublayouts -{ - return [self layoutWithLayoutElement:layoutElement size:size sublayouts:sublayouts]; -} - -@end - ASLayout *ASCalculateLayout(id layoutElement, const ASSizeRange sizeRange, const CGSize parentSize) { ASDisplayNodeCAssertNotNil(layoutElement, @"Not valid layoutElement passed in."); diff --git a/Source/Layout/ASLayoutElement.h b/Source/Layout/ASLayoutElement.h index 2ebd55a46c..ca56c67222 100644 --- a/Source/Layout/ASLayoutElement.h +++ b/Source/Layout/ASLayoutElement.h @@ -147,22 +147,6 @@ typedef NS_ENUM(NSUInteger, ASLayoutElementType) { - (BOOL)implementsLayoutMethod; -#pragma mark - Deprecated - -#define ASLayoutable ASLayoutElement - -/** - * @abstract Calculate a layout based on given size range. - * - * @param constrainedSize The minimum and maximum sizes the receiver should fit in. - * - * @return An ASLayout instance defining the layout of the receiver and its children. - * - * @deprecated Deprecated in version 2.0: Use layoutThatFits: or layoutThatFits:parentSize: if used in - * ASLayoutSpec subclasses - */ -- (nonnull ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize ASDISPLAYNODE_DEPRECATED_MSG("Use layoutThatFits: instead."); - @end #pragma mark - ASLayoutElementStyle diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 8d647c09aa..9fc52b3bfb 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -804,22 +804,4 @@ do {\ #endif /* YOGA */ -#pragma mark Deprecated - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -- (ASRelativeSizeRange)sizeRange -{ - return ASRelativeSizeRangeMake(self.minLayoutSize, self.maxLayoutSize); -} - -- (void)setSizeRange:(ASRelativeSizeRange)sizeRange -{ - self.minLayoutSize = sizeRange.min; - self.maxLayoutSize = sizeRange.max; -} - -#pragma clang diagnostic pop - @end diff --git a/Source/Layout/ASLayoutElementPrivate.h b/Source/Layout/ASLayoutElementPrivate.h index 68c46de611..9bc3101b06 100644 --- a/Source/Layout/ASLayoutElementPrivate.h +++ b/Source/Layout/ASLayoutElementPrivate.h @@ -51,11 +51,6 @@ NS_ASSUME_NONNULL_END return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];\ }\ \ -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize\ -{\ - return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];\ -}\ -\ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize\ {\ return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize];\ @@ -117,134 +112,3 @@ typedef struct ASLayoutElementStyleExtensions { return [self.style layoutOptionExtensionEdgeInsetsAtIndex:idx];\ }\ -#pragma mark ASLayoutElementStyleForwardingDeclaration (Deprecated) - -#define ASLayoutElementStyleForwardingDeclaration \ -@property (nonatomic, readwrite) CGFloat spacingBefore ASDISPLAYNODE_DEPRECATED_MSG("Use style.spacingBefore"); \ -@property (nonatomic, readwrite) CGFloat spacingAfter ASDISPLAYNODE_DEPRECATED_MSG("Use style.spacingAfter"); \ -@property (nonatomic, readwrite) CGFloat flexGrow ASDISPLAYNODE_DEPRECATED_MSG("Use style.flexGrow"); \ -@property (nonatomic, readwrite) CGFloat flexShrink ASDISPLAYNODE_DEPRECATED_MSG("Use style.flexShrink"); \ -@property (nonatomic, readwrite) ASDimension flexBasis ASDISPLAYNODE_DEPRECATED_MSG("Use style.flexBasis"); \ -@property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf ASDISPLAYNODE_DEPRECATED_MSG("Use style.alignSelf"); \ -@property (nonatomic, readwrite) CGFloat ascender ASDISPLAYNODE_DEPRECATED_MSG("Use style.ascender"); \ -@property (nonatomic, readwrite) CGFloat descender ASDISPLAYNODE_DEPRECATED_MSG("Use style.descender"); \ -@property (nonatomic, assign) ASRelativeSizeRange sizeRange ASDISPLAYNODE_DEPRECATED_MSG("Don't use sizeRange anymore instead set style.width or style.height"); \ -@property (nonatomic, assign) CGPoint layoutPosition ASDISPLAYNODE_DEPRECATED_MSG("Use style.layoutPosition"); \ - - -#pragma mark - ASLayoutElementStyleForwarding (Deprecated) - -// For the time beeing we are forwading all style related properties on ASDisplayNode and ASLayoutSpec. This define -// help us to not have duplicate code while moving from 1.x to 2.0s -#define ASLayoutElementStyleForwarding \ -\ -@dynamic spacingBefore, spacingAfter, flexGrow, flexShrink, flexBasis, alignSelf, ascender, descender, sizeRange, layoutPosition;\ -\ -_Pragma("mark - ASStackLayoutElement")\ -\ -- (void)setSpacingBefore:(CGFloat)spacingBefore\ -{\ - self.style.spacingBefore = spacingBefore;\ -}\ -\ -- (CGFloat)spacingBefore\ -{\ - return self.style.spacingBefore;\ -}\ -\ -- (void)setSpacingAfter:(CGFloat)spacingAfter\ -{\ - self.style.spacingAfter = spacingAfter;\ -}\ -\ -- (CGFloat)spacingAfter\ -{\ - return self.style.spacingAfter;\ -}\ -\ -- (void)setFlexGrow:(CGFloat)flexGrow\ -{\ - self.style.flexGrow = flexGrow;\ -}\ -\ -- (CGFloat)flexGrow\ -{\ - return self.style.flexGrow;\ -}\ -\ -- (void)setFlexShrink:(CGFloat)flexShrink\ -{\ - self.style.flexShrink = flexShrink;\ -}\ -\ -- (CGFloat)flexShrink\ -{\ - return self.style.flexShrink;\ -}\ -\ -- (void)setFlexBasis:(ASDimension)flexBasis\ -{\ - self.style.flexBasis = flexBasis;\ -}\ -\ -- (ASDimension)flexBasis\ -{\ - return self.style.flexBasis;\ -}\ -\ -- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf\ -{\ - self.style.alignSelf = alignSelf;\ -}\ -\ -- (ASStackLayoutAlignSelf)alignSelf\ -{\ - return self.style.alignSelf;\ -}\ -\ -- (void)setAscender:(CGFloat)ascender\ -{\ - self.style.ascender = ascender;\ -}\ -\ -- (CGFloat)ascender\ -{\ - return self.style.ascender;\ -}\ -\ -- (void)setDescender:(CGFloat)descender\ -{\ - self.style.descender = descender;\ -}\ -\ -- (CGFloat)descender\ -{\ - return self.style.descender;\ -}\ -\ -_Pragma("mark - ASAbsoluteLayoutElement")\ -\ -- (void)setLayoutPosition:(CGPoint)layoutPosition\ -{\ - self.style.layoutPosition = layoutPosition;\ -}\ -\ -- (CGPoint)layoutPosition\ -{\ - return self.style.layoutPosition;\ -}\ -\ -_Pragma("clang diagnostic push")\ -_Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")\ -\ -- (void)setSizeRange:(ASRelativeSizeRange)sizeRange\ -{\ - self.style.sizeRange = sizeRange;\ -}\ -\ -- (ASRelativeSizeRange)sizeRange\ -{\ - return self.style.sizeRange;\ -}\ -\ -_Pragma("clang diagnostic pop")\ diff --git a/Source/Layout/ASLayoutSpec.h b/Source/Layout/ASLayoutSpec.h index e4d9d42db1..ac02bacd01 100644 --- a/Source/Layout/ASLayoutSpec.h +++ b/Source/Layout/ASLayoutSpec.h @@ -104,10 +104,4 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface ASLayoutSpec (Deprecated) - -ASLayoutElementStyleForwardingDeclaration - -@end - NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASLayoutSpec.mm b/Source/Layout/ASLayoutSpec.mm index 6600f50867..76901d1ac7 100644 --- a/Source/Layout/ASLayoutSpec.mm +++ b/Source/Layout/ASLayoutSpec.mm @@ -35,17 +35,6 @@ @dynamic layoutElementType; @synthesize debugName = _debugName; -#pragma mark - Class - -+ (void)initialize -{ - [super initialize]; - if (self != [ASLayoutSpec class]) { - ASDisplayNodeAssert(!ASSubclassOverridesSelector([ASLayoutSpec class], self, @selector(measureWithSizeRange:)), @"Subclass %@ must not override measureWithSizeRange: method. Instead override calculateLayoutThatFits:", NSStringFromClass(self)); - } -} - - #pragma mark - Lifecycle - (instancetype)init @@ -167,7 +156,6 @@ ASLayoutElementLayoutCalculationDefaults } ASPrimitiveTraitCollectionDefaults -ASPrimitiveTraitCollectionDeprecatedImplementation #pragma mark - ASLayoutElementStyleExtensibility @@ -353,11 +341,3 @@ ASLayoutElementStyleExtensibilityForwarding } @end - -#pragma mark - ASLayoutSpec (Deprecated) - -@implementation ASLayoutSpec (Deprecated) - -ASLayoutElementStyleForwarding - -@end diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 36683954dd..0d1982a291 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -52,8 +52,6 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, - ASDisplayNodeMethodOverrideFetchData = 1 << 7, - ASDisplayNodeMethodOverrideClearFetchedData = 1 << 8 }; typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) diff --git a/Source/Private/ASTableView+Undeprecated.h b/Source/Private/ASTableView+Undeprecated.h index 70098940fe..bef11339d7 100644 --- a/Source/Private/ASTableView+Undeprecated.h +++ b/Source/Private/ASTableView+Undeprecated.h @@ -154,14 +154,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData; -/** - * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UITableView's version and will block the main thread while - * all the cells load. - */ -- (void)reloadDataImmediately; - /** * Triggers a relayout of all nodes. * diff --git a/Source/Private/Layout/ASStackUnpositionedLayout.mm b/Source/Private/Layout/ASStackUnpositionedLayout.mm index 54a17bb089..1dff933485 100644 --- a/Source/Private/Layout/ASStackUnpositionedLayout.mm +++ b/Source/Private/Layout/ASStackUnpositionedLayout.mm @@ -71,7 +71,7 @@ static ASLayout *crossChildLayout(const ASStackLayoutSpecChild &child, crossMax); const ASSizeRange childSizeRange = directionSizeRange(style.direction, stackMin, stackMax, childCrossMin, childCrossMax); ASLayout *layout = [child.element layoutThatFits:childSizeRange parentSize:parentSize]; - ASDisplayNodeCAssertNotNil(layout, @"ASLayout returned from measureWithSizeRange: must not be nil: %@", child.element); + ASDisplayNodeCAssertNotNil(layout, @"ASLayout returned from -layoutThatFits:parentSize: must not be nil: %@", child.element); return layout ? : [ASLayout layoutWithLayoutElement:child.element size:{0, 0}]; } diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.mm b/Source/Private/_ASCollectionGalleryLayoutItem.mm index 688ef44d48..32479d1ce3 100644 --- a/Source/Private/_ASCollectionGalleryLayoutItem.mm +++ b/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -38,7 +38,6 @@ ASLayoutElementStyleExtensibilityForwarding ASPrimitiveTraitCollectionDefaults -ASPrimitiveTraitCollectionDeprecatedImplementation - (ASTraitCollection *)asyncTraitCollection { diff --git a/Source/Private/_ASHierarchyChangeSet.mm b/Source/Private/_ASHierarchyChangeSet.mm index e2e70c7071..77ad5d959d 100644 --- a/Source/Private/_ASHierarchyChangeSet.mm +++ b/Source/Private/_ASHierarchyChangeSet.mm @@ -25,19 +25,11 @@ #import #import -// If assertions are enabled and they haven't forced us to suppress the exception, -// then throw, otherwise log. +// If assertions are enabled, throw. Otherwise log. #if ASDISPLAYNODE_ASSERTIONS_ENABLED #define ASFailUpdateValidation(...)\ - _Pragma("clang diagnostic push")\ - _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")\ - if ([ASDisplayNode suppressesInvalidCollectionUpdateExceptions]) {\ - NSLog(__VA_ARGS__);\ - } else {\ - NSLog(__VA_ARGS__);\ - [NSException raise:ASCollectionInvalidUpdateException format:__VA_ARGS__];\ - }\ - _Pragma("clang diagnostic pop") + NSLog(__VA_ARGS__);\ + [NSException raise:ASCollectionInvalidUpdateException format:__VA_ARGS__]; #else #define ASFailUpdateValidation(...) NSLog(__VA_ARGS__); #endif diff --git a/Tests/ASCollectionViewTests.mm b/Tests/ASCollectionViewTests.mm index f3567b4373..b223e246fe 100644 --- a/Tests/ASCollectionViewTests.mm +++ b/Tests/ASCollectionViewTests.mm @@ -620,7 +620,7 @@ [window makeKeyAndVisible]; for (NSInteger i = 0; i < 2; i++) { - // NOTE: waitUntilAllUpdatesAreProcessed or reloadDataImmediately is not sufficient here!! + // NOTE: reloadData and waitUntilAllUpdatesAreProcessed are not sufficient here!! XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]]; [cn reloadDataWithCompletion:^{ [done fulfill]; diff --git a/Tests/ASDisplayNodeLayoutTests.mm b/Tests/ASDisplayNodeLayoutTests.mm index a998e98285..00512e8012 100644 --- a/Tests/ASDisplayNodeLayoutTests.mm +++ b/Tests/ASDisplayNodeLayoutTests.mm @@ -44,7 +44,7 @@ ASXCTAssertEqualSizes(displayNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); ASXCTAssertEqualSizes(buttonNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); - // Trigger view creation and layout pass without a manual measure: call before so the automatic measurement + // Trigger view creation and layout pass without a manual -layoutThatFits: call before so the automatic measurement // pass will trigger in the layout pass [displayNode.view layoutIfNeeded]; diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 99882df06f..f1fb33d1b4 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -24,7 +24,6 @@ #import #import #import -#import #import #import "ASDisplayNodeTestsHelper.h" #import @@ -2128,27 +2127,6 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssertEqualObjects(calls, expected); } -- (void)testPreferredFrameSizeDeprecated -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - - ASDisplayNode *node = [ASDisplayNode new]; - - // Default auto preferred frame size will be CGSizeZero - XCTAssert(CGSizeEqualToSize(node.preferredFrameSize, CGSizeZero)); - - // Set a specific preferredFrameSize - node.preferredFrameSize = CGSizeMake(100, 100); - ASXCTAssertEqualSizes(node.preferredFrameSize, CGSizeMake(100, 100)); - - // CGSizeZero should be returned if width or height is not of unit type points - node.style.width = ASDimensionMakeWithFraction(0.5); - ASXCTAssertEqualSizes(node.preferredFrameSize, CGSizeZero); - -#pragma clang diagnostic pop -} - - (void)testSettingPropertiesViaStyllableProtocol { ASDisplayNode *node = [[ASDisplayNode alloc] init]; diff --git a/Tests/ASLayoutSpecTests.m b/Tests/ASLayoutSpecTests.m index 6300924648..5cfc3a66ce 100644 --- a/Tests/ASLayoutSpecTests.m +++ b/Tests/ASLayoutSpecTests.m @@ -2,8 +2,17 @@ // ASLayoutSpecTests.m // Texture // -// Created by Michael Schneider on 1/27/17. -// Copyright © 2017 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 @@ -39,7 +48,7 @@ ASDK_STYLE_PROP_OBJ(NSString *, extendedName, setExtendedName); @end /* - * As the ASLayoutableStyle conforms to the ASDKExtendedLayoutable protocol now, ASDKExtendedLayoutable properties + * As the ASLayoutElementStyle conforms to the ASDKExtendedLayoutElement protocol now, ASDKExtendedLayoutElement properties * can be accessed in ASDKExtendedLayoutSpec */ @interface ASDKExtendedLayoutSpec : ASLayoutSpec diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index 2bf7936418..f26d96b119 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -253,7 +253,8 @@ tableView.asyncDelegate = delegate; tableView.asyncDataSource = dataSource; - [tableView reloadDataImmediately]; + [tableView reloadData]; + [tableView waitUntilAllUpdatesAreCommitted]; [tableView setNeedsLayout]; [tableView layoutIfNeeded]; diff --git a/Tests/ASTableViewThrashTests.m b/Tests/ASTableViewThrashTests.m index e547fe39cb..fe2e2efd19 100644 --- a/Tests/ASTableViewThrashTests.m +++ b/Tests/ASTableViewThrashTests.m @@ -226,7 +226,8 @@ static atomic_uint ASThrashTestSectionNextID = 1; #else _tableView.asyncDelegate = self; _tableView.asyncDataSource = self; - [_tableView reloadDataImmediately]; + [_tableView reloadData]; + [_tableView waitUntilAllUpdatesAreCommitted]; #endif [_tableView layoutIfNeeded]; } diff --git a/examples/PagerNode/Sample/PageNode.m b/examples/PagerNode/Sample/PageNode.m index bda108a1bf..0efd831219 100644 --- a/examples/PagerNode/Sample/PageNode.m +++ b/examples/PagerNode/Sample/PageNode.m @@ -1,20 +1,18 @@ // // PageNode.m -// Sample -// -// Created by McCallum, Levi on 12/7/15. +// Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. +// 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. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// 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 "PageNode.h" @@ -26,10 +24,10 @@ return constrainedSize; } -- (void)fetchData +- (void)didEnterPreloadState { - [super fetchData]; - NSLog(@"Fetching data for node: %@", self); + [super didEnterPreloadState]; + NSLog(@"didEnterPreloadState for node: %@", self); } @end From 0bd18c852233d7b2b0b433e031aea1beb10da86b Mon Sep 17 00:00:00 2001 From: Eric Scheers Date: Thu, 7 Sep 2017 22:05:30 +0200 Subject: [PATCH 02/86] [ASDisplayNode] Notify rasterized subnodes that render pass has completed (#532) * Notify rasterized subsides that render pass has completed * Traverse entire subnode tree notifying all subnodes * Add entry in changelog * Retrieve rasterizesSubtree flag while holding instance lock * Balance display delegate calls for rasterized subnodes --- CHANGELOG.md | 1 + Source/Private/ASDisplayNode+AsyncDisplay.mm | 15 ++++++ Tests/ASDisplayNodeTests.mm | 49 ++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d6a3d332..f98a440dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - 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) - [Breaking] Remove APIs that have been deprecated since 2.0 and/or for at least 6 months [Huy Nguyen](https://github.com/nguyenhuy) [#529](https://github.com/TextureGroup/Texture/pull/529) +- [ASDisplayNode] Ensure `-displayWillStartAsynchronously:` and `-displayDidFinish` are invoked on rasterized subnodes. [Eric Scheers](https://github.com/smeis) [#532](https://github.com/TextureGroup/Texture/pull/532) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 00bb0ca66c..999c2552dd 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -23,6 +23,8 @@ #import #import #import +#import + @interface ASDisplayNode () <_ASDisplayLayerDelegate> @end @@ -302,6 +304,7 @@ } CALayer *layer = _layer; + BOOL rasterizesSubtree = _flags.rasterizesSubtree; __instanceLock__.unlock(); @@ -347,11 +350,23 @@ layer.contents = (id)image.CGImage; } [self didDisplayAsyncLayer:self.asyncLayer]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node didDisplayAsyncLayer:node.asyncLayer]; + }); + } } }; // Call willDisplay immediately in either case [self willDisplayAsyncLayer:self.asyncLayer asynchronously:asynchronously]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node willDisplayAsyncLayer:node.asyncLayer asynchronously:asynchronously]; + }); + } if (asynchronously) { // Async rendering operations are contained by a transaction, which allows them to proceed and concurrently diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index f1fb33d1b4..fac3f50929 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -110,6 +110,10 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (nonatomic) BOOL hasPreloaded; @property (nonatomic) BOOL preloadStateChangedToYES; @property (nonatomic) BOOL preloadStateChangedToNO; + +@property (nonatomic, assign) NSUInteger displayWillStartCount; +@property (nonatomic, assign) NSUInteger didDisplayCount; + @end @interface ASTestResponderNode : ASTestDisplayNode @@ -154,6 +158,18 @@ for (ASDisplayNode *n in @[ nodes ]) {\ } } +- (void)displayDidFinish +{ + [super displayDidFinish]; + _didDisplayCount++; +} + +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + [super displayWillStartAsynchronously:asynchronously]; + _displayWillStartCount++; +} + @end @interface UIDisplayNodeTestView : UIView @@ -2018,6 +2034,39 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssertThrows([rasterizedSupernode addSubnode:subnode]); } +- (void)testThatSubnodesGetDisplayUpdatesIfRasterized +{ + ASTestDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; + supernode.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [supernode enableSubtreeRasterization]; + + ASTestDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *subSubnode = [[ASTestDisplayNode alloc] init]; + + ASSetDebugNames(supernode, subnode); + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + [subnode addSubnode:subSubnode]; + [supernode addSubnode:subnode]; + [window addSubnode:supernode]; + [window makeKeyAndVisible]; + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subnode.didDisplayCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subSubnode.didDisplayCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subnode.displayWillStartCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subSubnode.displayWillStartCount == 1); + })); +} + // Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2011 - (void)testThatLayerBackedSubnodesAreMarkedInvisibleBeforeDeallocWhenSupernodesViewIsRemovedFromHierarchyWhileBeingRetained { From 53e99cc76273fe952ed790fc38c4026c7ea3d715 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 8 Sep 2017 07:17:35 -0700 Subject: [PATCH 03/86] Make ASWeakMapEntry Value Atomic (#555) * Make ASWeakMapEntry value atomic * Increment changelog * Go a little nuts * Update CHANGELOG.md --- CHANGELOG.md | 1 + Source/Private/ASWeakMap.h | 4 ++-- Source/Private/ASWeakMap.m | 16 ++++++---------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f98a440dbe..3f38cdc875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - [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) - [Breaking] Remove APIs that have been deprecated since 2.0 and/or for at least 6 months [Huy Nguyen](https://github.com/nguyenhuy) [#529](https://github.com/TextureGroup/Texture/pull/529) - [ASDisplayNode] Ensure `-displayWillStartAsynchronously:` and `-displayDidFinish` are invoked on rasterized subnodes. [Eric Scheers](https://github.com/smeis) [#532](https://github.com/TextureGroup/Texture/pull/532) +- Fixed a memory corruption issue in the ASImageNode display system. [Adlai Holler](https://github.com/Adlai-Holler) [#555](https://github.com/TextureGroup/Texture/pull/555) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/Private/ASWeakMap.h b/Source/Private/ASWeakMap.h index fb53de15a2..18bf07184f 100644 --- a/Source/Private/ASWeakMap.h +++ b/Source/Private/ASWeakMap.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASWeakMapEntry : NSObject -@property (nonatomic, retain, readonly) Value value; +@property (atomic, strong, readonly) Value value; @end @@ -49,7 +49,7 @@ AS_SUBCLASSING_RESTRICTED * The underlying storage is a hash table and the Key type should implement `hash` and `isEqual:`. */ AS_SUBCLASSING_RESTRICTED -@interface ASWeakMap<__covariant Key : NSObject *, Value> : NSObject +@interface ASWeakMap<__covariant Key, Value> : NSObject /** * Read from the cache. The Value object is accessible from the returned ASWeakMapEntry. diff --git a/Source/Private/ASWeakMap.m b/Source/Private/ASWeakMap.m index 2d9eb55230..5d5b3dc1ad 100644 --- a/Source/Private/ASWeakMap.m +++ b/Source/Private/ASWeakMap.m @@ -18,12 +18,13 @@ #import @interface ASWeakMapEntry () -@property (nonatomic, strong) NSObject *key; +@property (nonatomic, strong, readonly) id key; +@property (atomic, strong) id value; @end @implementation ASWeakMapEntry -- (instancetype)initWithKey:(NSObject *)key value:(NSObject *)value +- (instancetype)initWithKey:(id)key value:(id)value { self = [super init]; if (self) { @@ -33,16 +34,11 @@ return self; } -- (void)setValue:(NSObject *)value -{ - _value = value; -} - @end @interface ASWeakMap () -@property (nonatomic, strong) NSMapTable *hashTable; +@property (nonatomic, strong, readonly) NSMapTable *hashTable; @end /** @@ -69,12 +65,12 @@ return self; } -- (ASWeakMapEntry *)entryForKey:(NSObject *)key +- (ASWeakMapEntry *)entryForKey:(id)key { return [self.hashTable objectForKey:key]; } -- (ASWeakMapEntry *)setObject:(NSObject *)value forKey:(NSObject *)key +- (ASWeakMapEntry *)setObject:(id)value forKey:(id)key { ASWeakMapEntry *entry = [self.hashTable objectForKey:key]; if (entry != nil) { From d4b1f625aaa3bf85989f30aee809f116a92db57e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 8 Sep 2017 17:05:06 +0100 Subject: [PATCH 04/86] [Gallery layout] Include the caller in properties providing methods (#533) * Include the caller in ASCollectionGalleryLayoutPropertiesProviding's methods * Update CHANGELOG.md --- CHANGELOG.md | 1 + Source/ASPagerNode.m | 2 +- .../Details/ASCollectionGalleryLayoutDelegate.h | 17 +++++++++++++---- .../ASCollectionGalleryLayoutDelegate.mm | 14 +++++++------- .../ASCollectionView/Sample/ViewController.m | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f38cdc875..1553fc74cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - [Breaking] Remove APIs that have been deprecated since 2.0 and/or for at least 6 months [Huy Nguyen](https://github.com/nguyenhuy) [#529](https://github.com/TextureGroup/Texture/pull/529) - [ASDisplayNode] Ensure `-displayWillStartAsynchronously:` and `-displayDidFinish` are invoked on rasterized subnodes. [Eric Scheers](https://github.com/smeis) [#532](https://github.com/TextureGroup/Texture/pull/532) - Fixed a memory corruption issue in the ASImageNode display system. [Adlai Holler](https://github.com/Adlai-Holler) [#555](https://github.com/TextureGroup/Texture/pull/555) +- [Breaking] Rename ASCollectionGalleryLayoutSizeProviding to ASCollectionGalleryLayoutPropertiesProviding. Besides a fixed item size, it now can provide interitem and line spacings, as well as section inset [Huy Nguyen](https://github.com/nguyenhuy) [#496](https://github.com/TextureGroup/Texture/pull/496) [#533](https://github.com/TextureGroup/Texture/pull/533) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index 1ef893d9d6..4d8195a179 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -145,7 +145,7 @@ #pragma mark - ASCollectionGalleryLayoutPropertiesProviding -- (CGSize)sizeForElements:(ASElementMap *)elements +- (CGSize)galleryLayoutDelegate:(nonnull ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(nonnull ASElementMap *)elements { ASDisplayNodeAssertMainThread(); return [self pageSize]; diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h index dfd0f5c87d..99958d1c97 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.h +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -14,6 +14,7 @@ #import @class ASElementMap; +@class ASCollectionGalleryLayoutDelegate; NS_ASSUME_NONNULL_BEGIN @@ -24,11 +25,13 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion This method will only be called on main thread. * + * @param delegate The calling object. + * * @param elements All elements to be sized. * * @return The elements' size */ -- (CGSize)sizeForElements:(ASElementMap *)elements; +- (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements; @optional @@ -42,11 +45,13 @@ NS_ASSUME_NONNULL_BEGIN * It is not applied between the first line and the header, or between the last line and the footer. * This is the same behavior as UICollectionViewFlowLayout's minimumLineSpacing. * + * @param delegate The calling object. + * * @param elements All elements in the layout. * * @return The interitem spacing */ -- (CGFloat)minimumLineSpacingForElements:(ASElementMap *)elements; +- (CGFloat)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate minimumLineSpacingForElements:(ASElementMap *)elements; /** * Returns the minumum spacing to use between items in the same row or column, depending on the scroll directions. @@ -58,22 +63,26 @@ NS_ASSUME_NONNULL_BEGIN * It is considered while fitting items into lines, but the actual final spacing between some items might be larger. * This is the same behavior as UICollectionViewFlowLayout's minimumInteritemSpacing. * + * @param delegate The calling object. + * * @param elements All elements in the layout. * * @return The interitem spacing */ -- (CGFloat)minimumInteritemSpacingForElements:(ASElementMap *)elements; +- (CGFloat)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate minimumInteritemSpacingForElements:(ASElementMap *)elements; /** * Returns the margins of each section. * * @discussion This method will only be called on main thread. * + * @param delegate The calling object. + * * @param elements All elements in the layout. * * @return The margins used to layout content in a section */ -- (UIEdgeInsets)sectionInsetForElements:(ASElementMap *)elements; +- (UIEdgeInsets)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sectionInsetForElements:(ASElementMap *)elements; @end diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index a01fdf7f76..2734e9649a 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -66,9 +66,9 @@ _propertiesProviderFlags = {}; } else { _propertiesProvider = propertiesProvider; - _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumLineSpacingForElements:)]; - _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumInteritemSpacingForElements:)]; - _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(sectionInsetForElements:)]; + _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:minimumLineSpacingForElements:)]; + _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:minimumInteritemSpacingForElements:)]; + _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:sectionInsetForElements:)]; } } @@ -80,10 +80,10 @@ 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; + CGSize itemSize = [propertiesProvider galleryLayoutDelegate:self sizeForElements:elements]; + UIEdgeInsets sectionInset = _propertiesProviderFlags.sectionInsetForElements ? [propertiesProvider galleryLayoutDelegate:self sectionInsetForElements:elements] : UIEdgeInsetsZero; + CGFloat lineSpacing = _propertiesProviderFlags.minimumLineSpacingForElements ? [propertiesProvider galleryLayoutDelegate:self minimumLineSpacingForElements:elements] : 0.0; + CGFloat interitemSpacing = _propertiesProviderFlags.minimumInteritemSpacingForElements ? [propertiesProvider galleryLayoutDelegate:self minimumInteritemSpacingForElements:elements] : 0.0; return [[_ASCollectionGalleryLayoutInfo alloc] initWithItemSize:itemSize minimumLineSpacing:lineSpacing minimumInteritemSpacing:interitemSpacing diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index fa093f80ad..3755b01d69 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -112,7 +112,7 @@ #pragma mark - ASCollectionGalleryLayoutPropertiesProviding -- (CGSize)sizeForElements:(ASElementMap *)elements +- (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); return CGSizeMake(180, 90); From 786963c6a98514da9d227f2b8fed219e469d65d0 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 8 Sep 2017 18:04:43 +0100 Subject: [PATCH 05/86] [ASDisplayNode] Deprecate -displayWillStart in favor of -displayWillStartAsynchronously: (#536) * Deprecate -[ASDisplayNode displayWillStart] in favor of -displayWillStartAsynchronously: * Minor change * Fix CHANGELOG * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 3 ++- Source/ASDisplayNode+Subclasses.h | 13 ++++++++++++- Source/ASDisplayNode.mm | 5 ++++- Source/ASMultiplexImageNode.mm | 6 +++--- Source/ASNetworkImageNode.mm | 2 +- Source/Private/ASDisplayNode+AsyncDisplay.mm | 2 +- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1553fc74cd..0b69fcd8fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) -- [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) +- 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) @@ -19,6 +19,7 @@ - [ASDisplayNode] Ensure `-displayWillStartAsynchronously:` and `-displayDidFinish` are invoked on rasterized subnodes. [Eric Scheers](https://github.com/smeis) [#532](https://github.com/TextureGroup/Texture/pull/532) - Fixed a memory corruption issue in the ASImageNode display system. [Adlai Holler](https://github.com/Adlai-Holler) [#555](https://github.com/TextureGroup/Texture/pull/555) - [Breaking] Rename ASCollectionGalleryLayoutSizeProviding to ASCollectionGalleryLayoutPropertiesProviding. Besides a fixed item size, it now can provide interitem and line spacings, as well as section inset [Huy Nguyen](https://github.com/nguyenhuy) [#496](https://github.com/TextureGroup/Texture/pull/496) [#533](https://github.com/TextureGroup/Texture/pull/533) +- Deprecate `-[ASDisplayNode displayWillStart]` in favor of `-displayWillStartAsynchronously:` [Huy Nguyen](https://github.com/nguyenhuy) [536](https://github.com/TextureGroup/Texture/pull/536) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index 9cc98aab39..622acc2990 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -315,6 +315,18 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable id)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; +/** + * @abstract Indicates that the receiver is about to display. + * + * @discussion Deprecated in 2.5. + * + * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is + * about to begin. + * + * @note Called on the main thread only + */ +- (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use displayWillStartAsynchronously: instead."); + /** * @abstract Indicates that the receiver is about to display. * @@ -323,7 +335,6 @@ NS_ASSUME_NONNULL_BEGIN * * @note Called on the main thread only */ -- (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER; - (void)displayWillStartAsynchronously:(BOOL)asynchronously ASDISPLAYNODE_REQUIRES_SUPER; /** diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index f91b33849e..e1f1d143ce 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -1533,7 +1533,11 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, - (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer asynchronously:(BOOL)asynchronously { // Subclass hook. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self displayWillStart]; +#pragma clang diagnostic pop + [self displayWillStartAsynchronously:asynchronously]; } @@ -1546,7 +1550,6 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, - (void)displayWillStart {} - (void)displayWillStartAsynchronously:(BOOL)asynchronously { - [self displayWillStart]; // Subclass override ASDisplayNodeAssertMainThread(); ASDisplayNodeLogEvent(self, @"displayWillStart"); diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index 97652ec52b..81f3f43c13 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -261,11 +261,11 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent return (self.image == nil && self.animatedImage == nil && self.imageIdentifiers.count > 0); } -/* displayWillStart in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary +/* displayWillStartAsynchronously in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary in ASNetworkImageNode as well. */ -- (void)displayWillStart +- (void)displayWillStartAsynchronously:(BOOL)asynchronously { - [super displayWillStart]; + [super displayWillStartAsynchronously:asynchronously]; [self didEnterPreloadState]; diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index c68cb792e6..25f31ae077 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -313,7 +313,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return (self.image == nil && self.animatedImage == nil && _URL != nil); } -/* displayWillStart in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary +/* displayWillStartAsynchronously: in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary in ASMultiplexImageNode as well. */ - (void)displayWillStartAsynchronously:(BOOL)asynchronously { diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 999c2552dd..e22f461497 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -335,7 +335,7 @@ return; } - ASDisplayNodeAssert(_layer, @"Expect _layer to be not nil"); + ASDisplayNodeAssert(layer, @"Expect _layer to be not nil"); // This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ From 3c77d4a5da44c46c7b80b2a627c95389b7d6352d Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Mon, 11 Sep 2017 11:12:45 -0700 Subject: [PATCH 06/86] Adds support for specifying a quality indexed array of URLs (#557) * Add support for downloading a set of URLs on ASNetworkImageNode * Should be building now; * Remove old unused code * Add a changelog message * Bump PINRemoteImage * Huy's comments --- CHANGELOG.md | 1 + Cartfile | 2 +- Source/ASNetworkImageNode.h | 9 + Source/ASNetworkImageNode.mm | 182 +++++++++++++------- Source/Details/ASImageProtocols.h | 30 +++- Source/Details/ASPINRemoteImageDownloader.m | 101 +++++++---- Texture.podspec | 2 +- 7 files changed, 228 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b69fcd8fd..1f086bd71f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Fixed a memory corruption issue in the ASImageNode display system. [Adlai Holler](https://github.com/Adlai-Holler) [#555](https://github.com/TextureGroup/Texture/pull/555) - [Breaking] Rename ASCollectionGalleryLayoutSizeProviding to ASCollectionGalleryLayoutPropertiesProviding. Besides a fixed item size, it now can provide interitem and line spacings, as well as section inset [Huy Nguyen](https://github.com/nguyenhuy) [#496](https://github.com/TextureGroup/Texture/pull/496) [#533](https://github.com/TextureGroup/Texture/pull/533) - Deprecate `-[ASDisplayNode displayWillStart]` in favor of `-displayWillStartAsynchronously:` [Huy Nguyen](https://github.com/nguyenhuy) [536](https://github.com/TextureGroup/Texture/pull/536) +- Add support for URLs on ASNetworkImageNode. [Garrett Moon](https://github.com/garrettmoon) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Cartfile b/Cartfile index 93ba674369..bdcfff60c9 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "pinterest/PINRemoteImage" "3.0.0-beta.11" +github "pinterest/PINRemoteImage" "3.0.0-beta.12" github "pinterest/PINCache" "3.0.1-beta.5" diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index a438b422a3..dc911bede6 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -82,6 +82,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, strong, readwrite) NSURL *URL; +/** + * An array of URLs of increasing cost to download. + * + * @discussion By setting an array of URLs, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. + */ +@property (nullable, nonatomic, strong, readwrite) NSArray *URLs; + /** * Download and display a new image. * diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 25f31ae077..0e9c1fb1f9 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -39,7 +39,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; // Only access any of these with __instanceLock__. __weak id _delegate; - NSURL *_URL; + NSArray *_URLs; UIImage *_defaultImage; NSUUID *_cacheUUID; @@ -68,6 +68,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; unsigned int downloaderImplementsSetPriority:1; unsigned int downloaderImplementsAnimatedImage:1; unsigned int downloaderImplementsCancelWithResume:1; + unsigned int downloaderImplementsDownloadURLs:1; } _downloaderFlags; // Immutable and set on init only. We don't need to lock in this case. @@ -75,6 +76,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; struct { unsigned int cacheSupportsClearing:1; unsigned int cacheSupportsSynchronousFetch:1; + unsigned int cacheSupportsCachedURLs:1; } _cacheFlags; } @@ -96,9 +98,11 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _downloaderFlags.downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)]; _downloaderFlags.downloaderImplementsAnimatedImage = [downloader respondsToSelector:@selector(animatedImageWithData:)]; _downloaderFlags.downloaderImplementsCancelWithResume = [downloader respondsToSelector:@selector(cancelImageDownloadWithResumePossibilityForIdentifier:)]; + _downloaderFlags.downloaderImplementsDownloadURLs = [downloader respondsToSelector:@selector(downloadImageWithURLs:callbackQueue:downloadProgress:completion:)]; _cacheFlags.cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; _cacheFlags.cacheSupportsSynchronousFetch = [cache respondsToSelector:@selector(synchronouslyFetchedCachedImageWithURL:)]; + _cacheFlags.cacheSupportsCachedURLs = [cache respondsToSelector:@selector(cachedImageWithURLs:callbackQueue:completion:)]; _shouldCacheImage = YES; _shouldRenderProgressImages = YES; @@ -136,8 +140,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL shouldCancelAndClear = imageWasSetExternally && (imageWasSetExternally != _imageWasSetExternally); _imageWasSetExternally = imageWasSetExternally; if (shouldCancelAndClear) { - ASDisplayNodeAssertNil(_URL, @"Directly setting an image on an ASNetworkImageNode causes it to behave like an ASImageNode instead of an ASNetworkImageNode. If this is what you want, set the URL to nil first."); - _URL = nil; + ASDisplayNodeAssert(_URLs == nil || _URLs.count == 0, @"Directly setting an image on an ASNetworkImageNode causes it to behave like an ASImageNode instead of an ASNetworkImageNode. If this is what you want, set the URL to nil first."); + _URLs = nil; [self _locked_cancelDownloadAndClearImageWithResumePossibility:NO]; } @@ -158,15 +162,38 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - (void)setURL:(NSURL *)URL { - [self setURL:URL resetToDefault:YES]; + if (URL) { + [self setURLs:@[URL]]; + } else { + [self setURLs:nil]; + } } - (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset +{ + if (URL) { + [self setURLs:@[URL] resetToDefault:reset]; + } else { + [self setURLs:nil resetToDefault:reset]; + } +} + +- (NSURL *)URL +{ + return [self.URLs lastObject]; +} + +- (void)setURLs:(NSArray *)URLs +{ + [self setURLs:URLs resetToDefault:YES]; +} + +- (void)setURLs:(NSArray *)URLs resetToDefault:(BOOL)reset { { ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(URL, _URL)) { + if (ASObjectIsEqual(URLs, _URLs)) { return; } @@ -175,25 +202,25 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _imageWasSetExternally = NO; [self _locked_cancelImageDownloadWithResumePossibility:NO]; - + _imageLoaded = NO; - _URL = URL; + _URLs = URLs; - BOOL hasURL = (_URL == nil); + BOOL hasURL = (_URLs.count == 0); if (reset || hasURL) { [self _locked_setCurrentImageQuality:(hasURL ? 0.0 : 1.0)]; [self _locked__setImage:_defaultImage]; } } - + [self setNeedsPreload]; } -- (NSURL *)URL +- (NSArray *)URLs { ASDN::MutexLocker l(__instanceLock__); - return _URL; + return _URLs; } - (void)setDefaultImage:(UIImage *)defaultImage @@ -212,7 +239,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _defaultImage = defaultImage; if (!_imageLoaded) { - [self _locked_setCurrentImageQuality:((_URL == nil) ? 0.0 : 1.0)]; + [self _locked_setCurrentImageQuality:((_URLs.count == 0) ? 0.0 : 1.0)]; [self _locked__setImage:defaultImage]; } @@ -310,7 +337,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - (BOOL)placeholderShouldPersist { ASDN::MutexLocker l(__instanceLock__); - return (self.image == nil && self.animatedImage == nil && _URL != nil); + return (self.image == nil && self.animatedImage == nil && _URLs.count != 0); } /* displayWillStartAsynchronously: in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary @@ -322,13 +349,16 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (asynchronously == NO && _cacheFlags.cacheSupportsSynchronousFetch) { ASDN::MutexLocker l(__instanceLock__); - if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { - UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; - if (result) { - [self _locked_setCurrentImageQuality:1.0]; - [self _locked__setImage:result]; - - _imageLoaded = YES; + if (_imageLoaded == NO && _URLs.count > 0 && _downloadIdentifier == nil) { + for (NSURL *url in [_URLs reverseObjectEnumerator]) { + UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:url] asdk_image]; + if (result) { + [self _locked_setCurrentImageQuality:1.0]; + [self _locked__setImage:result]; + + _imageLoaded = YES; + break; + } } } } @@ -510,9 +540,11 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _imageLoaded = NO; if (_cacheFlags.cacheSupportsClearing) { - if (_URL != nil) { - as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URL); - [_cache clearFetchedImageFromCacheWithURL:_URL]; + if (_URLs.count != 0) { + as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URLs); + for (NSURL *url in _URLs) { + [_cache clearFetchedImageFromCacheWithURL:url]; + } } } } @@ -546,7 +578,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier))finished { ASPerformBlockOnBackgroundThread(^{ - NSURL *url; + NSArray *urls; id downloadIdentifier; BOOL cancelAndReattempt = NO; @@ -555,23 +587,34 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; // it and try again. { ASDN::MutexLocker l(__instanceLock__); - url = _URL; + urls = _URLs; } - - downloadIdentifier = [_downloader downloadImageWithURL:url - callbackQueue:dispatch_get_main_queue() - downloadProgress:NULL - completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { - if (finished != NULL) { - finished(imageContainer, error, downloadIdentifier); - } - }]; + if (_downloaderFlags.downloaderImplementsDownloadURLs) { + downloadIdentifier = [_downloader downloadImageWithURLs:urls + callbackQueue:dispatch_get_main_queue() + downloadProgress:NULL + completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { + if (finished != NULL) { + finished(imageContainer, error, downloadIdentifier); + } + }]; + } else { + downloadIdentifier = [_downloader downloadImageWithURL:[urls lastObject] + callbackQueue:dispatch_get_main_queue() + downloadProgress:NULL + completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { + if (finished != NULL) { + finished(imageContainer, error, downloadIdentifier); + } + }]; + } + as_log_verbose(ASImageLoadingLog(), "Downloading image for %@ url: %@", self, url); { ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(_URL, url)) { + if (ASObjectIsEqual(_URLs, urls)) { // The download we kicked off is correct, no need to do any more work. _downloadIdentifier = downloadIdentifier; } else { @@ -600,34 +643,36 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; __weak id delegate = _delegate; BOOL delegateDidStartFetchingData = _delegateFlags.delegateDidStartFetchingData; BOOL isImageLoaded = _imageLoaded; - NSURL *URL = _URL; + NSArray *URLs = _URLs; id currentDownloadIdentifier = _downloadIdentifier; __instanceLock__.unlock(); - if (!isImageLoaded && URL != nil && currentDownloadIdentifier == nil) { + if (!isImageLoaded && URLs.count > 0 && currentDownloadIdentifier == nil) { if (delegateDidStartFetchingData) { [delegate imageNodeDidStartFetchingData:self]; } - if (URL.isFileURL) { + // We only support file URLs if there is one URL currently + if (URLs.count == 1 && [URLs lastObject].isFileURL) { dispatch_async(dispatch_get_main_queue(), ^{ ASDN::MutexLocker l(__instanceLock__); // Bail out if not the same URL anymore - if (!ASObjectIsEqual(URL, _URL)) { + if (!ASObjectIsEqual(URLs, _URLs)) { return; } + NSURL *URL = [URLs lastObject]; if (_shouldCacheImage) { - [self _locked__setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; + [self _locked__setImage:[UIImage imageNamed:URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. - UIImage *nonAnimatedImage = [UIImage imageWithContentsOfFile:_URL.path]; + UIImage *nonAnimatedImage = [UIImage imageWithContentsOfFile:URL.path]; if (nonAnimatedImage == nil) { // If we couldn't find it, execute an -imageNamed:-like search so we can find resources even if the // extension is not provided in the path. This allows the same path to work regardless of shouldCacheImage. - NSString *filename = [[NSBundle mainBundle] pathForResource:_URL.path.lastPathComponent ofType:nil]; + NSString *filename = [[NSBundle mainBundle] pathForResource:URL.path.lastPathComponent ofType:nil]; if (filename != nil) { nonAnimatedImage = [UIImage imageWithContentsOfFile:filename]; } @@ -636,7 +681,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; // If the file may be an animated gif and then created an animated image. id animatedImage = nil; if (_downloaderFlags.downloaderImplementsAnimatedImage) { - NSData *data = [NSData dataWithContentsOfURL:_URL]; + NSData *data = [NSData dataWithContentsOfURL:URL]; if (data != nil) { animatedImage = [_downloader animatedImageWithData:data]; @@ -671,7 +716,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return; } - as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); // Grab the lock for the rest of the block ASDN::MutexLocker l(strongSelf->__instanceLock__); @@ -714,26 +759,35 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _cacheUUID = cacheUUID; __instanceLock__.unlock(); - as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ url: %@", self, URL); - [_cache cachedImageWithURL:URL - callbackQueue:dispatch_get_main_queue() - completion:^(id imageContainer) { - // If the cache UUID changed, that means this request was cancelled. - __instanceLock__.lock(); - NSUUID *currentCacheUUID = _cacheUUID; - __instanceLock__.unlock(); - - if (!ASObjectIsEqual(currentCacheUUID, cacheUUID)) { - return; - } - - if ([imageContainer asdk_image] == nil && _downloader != nil) { - [self _downloadImageWithCompletion:finished]; - } else { - as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); - finished(imageContainer, nil, nil); - } - }]; + as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ urls: %@", self, URLs); + + ASImageCacherCompletion completion = ^(id imageContainer) { + // If the cache UUID changed, that means this request was cancelled. + __instanceLock__.lock(); + NSUUID *currentCacheUUID = _cacheUUID; + __instanceLock__.unlock(); + + if (!ASObjectIsEqual(currentCacheUUID, cacheUUID)) { + return; + } + + if ([imageContainer asdk_image] == nil && _downloader != nil) { + [self _downloadImageWithCompletion:finished]; + } else { + as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); + finished(imageContainer, nil, nil); + } + }; + + if (_cacheFlags.cacheSupportsCachedURLs) { + [_cache cachedImageWithURLs:URLs + callbackQueue:dispatch_get_main_queue() + completion:completion]; + } else { + [_cache cachedImageWithURL:[URLs lastObject] + callbackQueue:dispatch_get_main_queue() + completion:completion]; + } } else { [self _downloadImageWithCompletion:finished]; } diff --git a/Source/Details/ASImageProtocols.h b/Source/Details/ASImageProtocols.h index 3fdf321e45..bc671866ac 100644 --- a/Source/Details/ASImageProtocols.h +++ b/Source/Details/ASImageProtocols.h @@ -37,7 +37,7 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i @param URL The URL of the image to retrieve from the cache. @param callbackQueue The queue to call `completion` on. @param completion The block to be called when the cache has either hit or missed. - @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image. This method should not block + @discussion If `URL` is nil, `completion` should be invoked immediately with a nil image. This method should not block the calling thread as it is likely to be called from the main thread. */ - (void)cachedImageWithURL:(NSURL *)URL @@ -66,6 +66,19 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i */ - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL; +/** + @abstract Attempts to fetch an image with the given URLs from the cache in reverse order. + @param URLs The URLs of the image to retrieve from the cache. + @param callbackQueue The queue to call `completion` on. + @param completion The block to be called when the cache has either hit or missed. + @discussion If `URLs` is nil or empty, `completion` should be invoked immediately with a nil image. This method should not block + the calling thread as it is likely to be called from the main thread. + @see downloadImageWithURLs:callbackQueue:downloadProgress:completion: + */ +- (void)cachedImageWithURLs:(NSArray *)URLs + callbackQueue:(dispatch_queue_t)callbackQueue + completion:(ASImageCacherCompletion)completion; + @end /** @@ -154,6 +167,21 @@ typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { - (void)setPriority:(ASImageDownloaderPriority)priority withDownloadIdentifier:(id)downloadIdentifier; +/** + @abstract Downloads an image from a list of URLs depending on previously observed network speed conditions. + @param URLs An array of URLs ordered by the cost of downloading them, the URL at index 0 being the lowest cost. + @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. + @param downloadProgress The block to be invoked when the download of `URL` progresses. + @param completion The block to be invoked when the download has completed, or has failed. + @discussion This method is likely to be called on the main thread, so any custom implementations should make sure to background any expensive download operations. + @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must + retain the identifier if you wish to use it later. + */ +- (nullable id)downloadImageWithURLs:(NSArray *)URLs + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion; + @end @protocol ASAnimatedImageProtocol diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 3fd3c86dc0..0e6c6adff4 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -196,6 +196,23 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; } } +- (void)cachedImageWithURLs:(NSArray *)URLs + callbackQueue:(dispatch_queue_t)callbackQueue + completion:(ASImageCacherCompletion)completion +{ + [self cachedImageWithURL:[URLs lastObject] + callbackQueue:callbackQueue + completion:^(id _Nullable imageFromCache) { + if (imageFromCache.asdk_image == nil && URLs.count > 1) { + [self cachedImageWithURLs:[URLs subarrayWithRange:NSMakeRange(0, URLs.count - 1)] + callbackQueue:callbackQueue + completion:completion]; + } else { + completion(imageFromCache); + } + }]; +} + - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL { if ([self sharedImageManagerSupportsMemoryRemoval]) { @@ -210,43 +227,63 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion; { - return [[self sharedPINRemoteImageManager] downloadImageWithURL:URL options:PINRemoteImageManagerDownloadOptionsSkipDecode progressDownload:^(int64_t completedBytes, int64_t totalBytes) { - if (downloadProgress == nil) { return; } + NSArray *URLs = nil; + if (URL) { + URLs = @[URL]; + } + return [self downloadImageWithURLs:URLs callbackQueue:callbackQueue downloadProgress:downloadProgress completion:completion]; +} - /// If we're targeting the main queue and we're on the main thread, call immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { - downloadProgress(completedBytes / (CGFloat)totalBytes); - } else { - dispatch_async(callbackQueue, ^{ - downloadProgress(completedBytes / (CGFloat)totalBytes); - }); - } - } completion:^(PINRemoteImageManagerResult * _Nonnull result) { - /// If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { -#if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } -#else - completion(result.image, result.error, result.UUID); -#endif - } else { - dispatch_async(callbackQueue, ^{ -#if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); +- (nullable id)downloadImageWithURLs:(NSArray *)URLs + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion +{ + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { + if (downloadProgress == nil) { return; } + + /// If we're targeting the main queue and we're on the main thread, call immediately. + if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { + downloadProgress(completedBytes / (CGFloat)totalBytes); } else { - completion(result.image, result.error, result.UUID); + dispatch_async(callbackQueue, ^{ + downloadProgress(completedBytes / (CGFloat)totalBytes); + }); } + }; + + PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { + /// If we're targeting the main queue and we're on the main thread, complete immediately. + if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { +#if PIN_ANIMATED_AVAILABLE + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation, result.error, result.UUID); + } else { + completion(result.image, result.error, result.UUID); + } #else - completion(result.image, result.error, result.UUID); + completion(result.image, result.error, result.UUID); #endif - }); - } - }]; + } else { + dispatch_async(callbackQueue, ^{ +#if PIN_ANIMATED_AVAILABLE + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation, result.error, result.UUID); + } else { + completion(result.image, result.error, result.UUID); + } +#else + completion(result.image, result.error, result.UUID); +#endif + }); + } + }; + + return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs + options:PINRemoteImageManagerDownloadOptionsSkipDecode + progressImage:nil + progressDownload:progressDownload + completion:imageCompletion]; } - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier diff --git a/Texture.podspec b/Texture.podspec index 9c53d9f4c1..1fcc6cf070 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -45,7 +45,7 @@ Pod::Spec.new do |spec| end spec.subspec 'PINRemoteImage' do |pin| - pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.11' + pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.12' pin.dependency 'PINRemoteImage/PINCache' pin.dependency 'Texture/Core' end From fcee108af536571a4abc8b41da6472e75cef1b9e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 11 Sep 2017 19:20:32 +0100 Subject: [PATCH 07/86] [ASCollectionNode][ASTableNode] Add content inset bridging property (#560) * Add content inset bridging property to table and collection nodes * Fix CHANGELOG * Fix typo * Minor fixes --- CHANGELOG.md | 2 +- Source/ASCollectionNode.h | 5 +++ Source/ASCollectionNode.mm | 23 +++++++++++ Source/ASCollectionView.h | 5 +++ Source/ASPagerNode.m | 2 +- Source/ASTableNode.h | 5 +++ Source/ASTableNode.mm | 40 +++++++++++++++---- Source/ASTableView.h | 5 +++ .../Private/ASCollectionView+Undeprecated.h | 2 + Source/Private/ASTableView+Undeprecated.h | 1 + Tests/ASPagerNodeTests.m | 4 +- 11 files changed, 83 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f086bd71f..a8a963685c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) -- 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) +- Add content inset and offset bridging properties to ASTableNode and ASCollectionNode. Deprecate related properties and methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) [#560](https://github.com/TextureGroup/Texture/pull/560) - 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) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 68e5d752f3..6913f87206 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -128,6 +128,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, weak) id layoutInspector; +/** + * The distance that the content view is inset from the collection node edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset; + /** * The offset of the content view's origin from the collection node's origin. Defaults to CGPointZero. */ diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index e5cde4c8e6..0be5cda59f 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -49,6 +49,7 @@ @property (nonatomic, assign) BOOL usesSynchronousDataLoading; @property (nonatomic, assign) CGFloat leadingScreensForBatching; @property (weak, nonatomic) id layoutInspector; +@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL animatesContentOffset; @end @@ -63,6 +64,7 @@ _allowsSelection = YES; _allowsMultipleSelection = NO; _inverted = NO; + _contentInset = UIEdgeInsetsZero; _contentOffset = CGPointZero; _animatesContentOffset = NO; } @@ -188,6 +190,7 @@ view.allowsMultipleSelection = pendingState.allowsMultipleSelection; view.usesSynchronousDataLoading = pendingState.usesSynchronousDataLoading; view.layoutInspector = pendingState.layoutInspector; + view.contentInset = pendingState.contentInset; self.pendingState = nil; if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { @@ -440,6 +443,25 @@ } } +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + if ([self pendingState]) { + _pendingState.contentInset = contentInset; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.contentInset = contentInset; + } +} + +- (UIEdgeInsets)contentInset +{ + if ([self pendingState]) { + return _pendingState.contentInset; + } else { + return self.view.contentInset; + } +} + - (void)setContentOffset:(CGPoint)contentOffset { [self setContentOffset:contentOffset animated:NO]; @@ -451,6 +473,7 @@ _pendingState.contentOffset = contentOffset; _pendingState.animatesContentOffset = animated; } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); [self.view setContentOffset:contentOffset animated:animated]; } } diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index 2c1f87984c..ba9dda2964 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -141,6 +141,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) BOOL zeroContentInsets ASDISPLAYNODE_DEPRECATED_MSG("Set automaticallyAdjustsScrollViewInsets=NO on your view controller instead."); +/** + * The distance that the content view is inset from the collection view edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead"); + /** * The point at which the origin of the content view is offset from the origin of the collection view. */ diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index 4d8195a179..719a45687a 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -115,7 +115,7 @@ - (CGSize)pageSize { - UIEdgeInsets contentInset = self.view.contentInset; + UIEdgeInsets contentInset = self.contentInset; CGSize pageSize = self.bounds.size; pageSize.height -= (contentInset.top + contentInset.bottom); return pageSize; diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 08b26a6fd7..8322faaf45 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -55,6 +55,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL inverted; +/** + * The distance that the content view is inset from the table node edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset; + /** * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. */ diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index b4a3e06db9..d68e4a9d72 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -43,6 +43,7 @@ @property (nonatomic, assign) BOOL allowsMultipleSelectionDuringEditing; @property (nonatomic, assign) BOOL inverted; @property (nonatomic, assign) CGFloat leadingScreensForBatching; +@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL animatesContentOffset; @property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; @@ -60,6 +61,7 @@ _allowsMultipleSelectionDuringEditing = NO; _inverted = NO; _leadingScreensForBatching = 2; + _contentInset = UIEdgeInsetsZero; _contentOffset = CGPointZero; _animatesContentOffset = NO; _automaticallyAdjustsContentOffset = NO; @@ -113,17 +115,20 @@ if (_pendingState) { _ASTablePendingState *pendingState = _pendingState; - self.pendingState = nil; - view.asyncDelegate = pendingState.delegate; - view.asyncDataSource = pendingState.dataSource; - view.inverted = pendingState.inverted; - view.allowsSelection = pendingState.allowsSelection; - view.allowsSelectionDuringEditing = pendingState.allowsSelectionDuringEditing; - view.allowsMultipleSelection = pendingState.allowsMultipleSelection; + view.asyncDelegate = pendingState.delegate; + view.asyncDataSource = pendingState.dataSource; + view.inverted = pendingState.inverted; + view.allowsSelection = pendingState.allowsSelection; + view.allowsSelectionDuringEditing = pendingState.allowsSelectionDuringEditing; + view.allowsMultipleSelection = pendingState.allowsMultipleSelection; view.allowsMultipleSelectionDuringEditing = pendingState.allowsMultipleSelectionDuringEditing; + view.contentInset = pendingState.contentInset; + self.pendingState = nil; + if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } + [view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset]; } } @@ -237,6 +242,27 @@ } } +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.contentInset = contentInset; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.contentInset = contentInset; + } +} + +- (UIEdgeInsets)contentInset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.contentInset; + } else { + return self.view.contentInset; + } +} + - (void)setContentOffset:(CGPoint)contentOffset { [self setContentOffset:contentOffset animated:NO]; diff --git a/Source/ASTableView.h b/Source/ASTableView.h index 04e1dbd128..ba6736eba2 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -67,6 +67,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGFloat leadingScreensForBatching ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); +/** + * The distance that the content view is inset from the table view edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead"); + /** * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. */ diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index 811ba7bafa..591f49efae 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -75,6 +75,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id layoutInspector; +@property (nonatomic, assign) UIEdgeInsets contentInset; + @property (nonatomic, assign) CGPoint contentOffset; /** diff --git a/Source/Private/ASTableView+Undeprecated.h b/Source/Private/ASTableView+Undeprecated.h index bef11339d7..bab43adf06 100644 --- a/Source/Private/ASTableView+Undeprecated.h +++ b/Source/Private/ASTableView+Undeprecated.h @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id asyncDelegate; @property (nonatomic, weak) id asyncDataSource; +@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; @property (nonatomic, assign) BOOL inverted; diff --git a/Tests/ASPagerNodeTests.m b/Tests/ASPagerNodeTests.m index 4b45a8c1ee..05396387b9 100644 --- a/Tests/ASPagerNodeTests.m +++ b/Tests/ASPagerNodeTests.m @@ -138,7 +138,7 @@ XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); XCTAssertEqual(pagerNode.contentOffset.y, 0); - XCTAssertEqual(pagerNode.view.contentInset.top, 0); + XCTAssertEqual(pagerNode.contentInset.top, 0); e = [self expectationWithDescription:@"Transition completed"]; // Push another view controller @@ -168,7 +168,7 @@ XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); XCTAssertEqual(pagerNode.contentOffset.y, 0); - XCTAssertEqual(pagerNode.view.contentInset.top, 0); + XCTAssertEqual(pagerNode.contentInset.top, 0); } @end From 7c7a4acf0e36d5472ae3d74e5be3e8da61224876 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 11 Sep 2017 21:32:17 +0100 Subject: [PATCH 08/86] ASCollectionLayout to exclude content inset on scrollable directions from viewport size (#562) --- CHANGELOG.md | 2 +- Source/Private/ASCollectionLayout.mm | 61 ++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8a963685c..fda12670ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) - Add content inset and offset bridging properties to ASTableNode and ASCollectionNode. Deprecate related properties and methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) [#560](https://github.com/TextureGroup/Texture/pull/560) - 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) +- 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) [#562]((https://github.com/TextureGroup/Texture/pull/562) - 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) diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 97451c32a6..59273c975b 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -69,18 +69,35 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh - (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - CGSize viewportSize = [self _viewportSize]; - CGPoint contentOffset = _collectionNode.contentOffset; + + Class layoutDelegateClass = [_layoutDelegate class]; + ASCollectionLayoutCache *layoutCache = _layoutCache; + ASCollectionNode *collectionNode = _collectionNode; + if (collectionNode == nil) { + return [[ASCollectionLayoutContext alloc] initWithViewportSize:CGSizeZero + initialContentOffset:CGPointZero + scrollableDirections:ASScrollDirectionNone + elements:[[ASElementMap alloc] init] + layoutDelegateClass:layoutDelegateClass + layoutCache:layoutCache + additionalInfo:nil]; + } + + ASScrollDirection scrollableDirections = [_layoutDelegate scrollableDirections]; + CGSize viewportSize = [ASCollectionLayout _viewportSizeForCollectionNode:collectionNode scrollableDirections:scrollableDirections]; + CGPoint contentOffset = collectionNode.contentOffset; + id additionalInfo = nil; if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } + return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize initialContentOffset:contentOffset - scrollableDirections:[_layoutDelegate scrollableDirections] + scrollableDirections:scrollableDirections elements:elements - layoutDelegateClass:[_layoutDelegate class] - layoutCache:_layoutCache + layoutDelegateClass:layoutDelegateClass + layoutCache:layoutCache additionalInfo:additionalInfo]; } @@ -208,21 +225,41 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { - return (! CGSizeEqualToSize([self _viewportSize], newBounds.size)); + return (! CGSizeEqualToSize([ASCollectionLayout _boundsForCollectionNode:_collectionNode], newBounds.size)); } #pragma mark - Private methods -- (CGSize)_viewportSize ++ (CGSize)_boundsForCollectionNode:(nonnull ASCollectionNode *)collectionNode { - ASCollectionNode *collectionNode = _collectionNode; - if (collectionNode != nil && !collectionNode.isNodeLoaded) { + if (collectionNode == nil) { + return CGSizeZero; + } + + if (!collectionNode.isNodeLoaded) { // TODO consider calculatedSize as well return collectionNode.threadSafeBounds.size; - } else { - ASDisplayNodeAssertMainThread(); - return self.collectionView.bounds.size; } + + ASDisplayNodeAssertMainThread(); + return collectionNode.view.bounds.size; +} + ++ (CGSize)_viewportSizeForCollectionNode:(nonnull ASCollectionNode *)collectionNode scrollableDirections:(ASScrollDirection)scrollableDirections +{ + if (collectionNode == nil) { + return CGSizeZero; + } + + CGSize result = [ASCollectionLayout _boundsForCollectionNode:collectionNode]; + // TODO: Consider using adjustedContentInset on iOS 11 and later, to include the safe area of the scroll view + UIEdgeInsets contentInset = collectionNode.contentInset; + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { + result.height -= (contentInset.top + contentInset.bottom); + } else { + result.width -= (contentInset.left + contentInset.right); + } + return result; } /** From 002c2d6978efab95207077811fbccd2d9cccda88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Est=C3=A9banez=20Tasc=C3=B3n?= Date: Mon, 11 Sep 2017 22:32:25 +0200 Subject: [PATCH 09/86] Mark ASRunLoopQueue as drained if it contains only NULLs (#558) * Mark ASRunLoopQueue as drained if it contains only NULLs * Update CHANGELOG.md * Cover ASRunLoopQueue with tests * Include PR link in CHANGELOG.md * Replace license header of ASRunLoopQueueTests.m with correct one * Insert a nil in _internalQueue to ensure compaction, instead of maintaining a state for _isQueueDrained --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + CHANGELOG.md | 1 + Source/ASRunLoopQueue.h | 2 + Source/ASRunLoopQueue.mm | 20 ++- Tests/ASRunLoopQueueTests.m | 160 ++++++++++++++++++++++ 5 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 Tests/ASRunLoopQueueTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 41389f7219..068938a4d2 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.m */; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */; }; + 4E9127691F64157600499623 /* ASRunLoopQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */; }; 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E111B371BD7007741D0 /* ASScrollDirection.m */; }; 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */; }; 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E181B37339C007741D0 /* ASAbstractLayoutController.mm */; }; @@ -624,6 +625,7 @@ 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableLayoutController.m; sourceTree = ""; }; 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; + 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRunLoopQueueTests.m; sourceTree = ""; }; 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPINRemoteImageDownloader.m; sourceTree = ""; }; 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = ""; }; @@ -1219,6 +1221,7 @@ 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.m */, 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.m */, + 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */, ); path = Tests; sourceTree = ""; @@ -2140,6 +2143,7 @@ 696FCB311D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm in Sources */, CC583AD81EF9BDC300134156 /* OCMockObject+ASAdditions.m in Sources */, 69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.m in Sources */, + 4E9127691F64157600499623 /* ASRunLoopQueueTests.m in Sources */, CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */, CC54A81E1D7008B300296A24 /* ASDispatchTests.m in Sources */, CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index fda12670ac..16e6571690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - [Breaking] Rename ASCollectionGalleryLayoutSizeProviding to ASCollectionGalleryLayoutPropertiesProviding. Besides a fixed item size, it now can provide interitem and line spacings, as well as section inset [Huy Nguyen](https://github.com/nguyenhuy) [#496](https://github.com/TextureGroup/Texture/pull/496) [#533](https://github.com/TextureGroup/Texture/pull/533) - Deprecate `-[ASDisplayNode displayWillStart]` in favor of `-displayWillStartAsynchronously:` [Huy Nguyen](https://github.com/nguyenhuy) [536](https://github.com/TextureGroup/Texture/pull/536) - Add support for URLs on ASNetworkImageNode. [Garrett Moon](https://github.com/garrettmoon) +- Mark ASRunLoopQueue as drained if it contains only NULLs [Cesar Estebanez](https://github.com/cesteban) [#558](https://github.com/TextureGroup/Texture/pull/558) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index 7ff563f409..6b08540ccd 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -41,6 +41,8 @@ AS_SUBCLASSING_RESTRICTED - (void)enqueue:(ObjectType)object; +@property (nonatomic, readonly) BOOL isEmpty; + @property (nonatomic, assign) NSUInteger batchSize; // Default == 1. @property (nonatomic, assign) BOOL ensureExclusiveMembership; // Default == YES. Set-like behavior. diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 004d4abb97..1902ad23d0 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -348,12 +348,12 @@ typedef enum { { ASDN::MutexLocker l(_internalQueueLock); - // Early-exit if the queue is empty. NSInteger internalQueueCount = _internalQueue.count; + // Early-exit if the queue is empty. if (internalQueueCount == 0) { return; } - + ASSignpostStart(ASSignpostRunLoopQueueBatch); // Snatch the next batch of items. @@ -382,6 +382,14 @@ typedef enum { } } + if (foundItemCount == 0) { + // If _internalQueue holds weak references, and all of them just become NULL, then the array + // is never marked as needsCompletion, and compact will return early, not removing the NULL's. + // Inserting a NULL here ensures the compaction will take place. + // See http://www.openradar.me/15396578 and https://stackoverflow.com/a/40274426/1136669 + [_internalQueue addPointer:NULL]; + } + [_internalQueue compact]; if (_internalQueue.count == 0) { isQueueDrained = YES; @@ -434,10 +442,16 @@ typedef enum { if (!foundObject) { [_internalQueue addPointer:(__bridge void *)object]; - + CFRunLoopSourceSignal(_runLoopSource); CFRunLoopWakeUp(_runLoop); } } +- (BOOL)isEmpty +{ + ASDN::MutexLocker l(_internalQueueLock); + return _internalQueue.count == 0; +} + @end diff --git a/Tests/ASRunLoopQueueTests.m b/Tests/ASRunLoopQueueTests.m new file mode 100644 index 0000000000..ccf15ae7b7 --- /dev/null +++ b/Tests/ASRunLoopQueueTests.m @@ -0,0 +1,160 @@ +// +// ASRunLoopQueueTests.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 + +static NSTimeInterval const kRunLoopRunTime = 0.001; // Allow the RunLoop to run for one millisecond each time. + +@interface ASRunLoopQueueTests : XCTestCase + +@end + +@implementation ASRunLoopQueueTests + +#pragma mark enqueue tests + +- (void)testEnqueueNilObjectsToQueue +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; + id object = nil; + [queue enqueue:object]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testEnqueueSameObjectTwiceToDefaultQueue +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block NSUInteger dequeuedCount = 0; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + dequeuedCount++; + } + }]; + [queue enqueue:object]; + [queue enqueue:object]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssert(dequeuedCount == 1); +} + +- (void)testEnqueueSameObjectTwiceToNonExclusiveMembershipQueue +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block NSUInteger dequeuedCount = 0; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + dequeuedCount++; + } + }]; + queue.ensureExclusiveMembership = NO; + [queue enqueue:object]; + [queue enqueue:object]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssert(dequeuedCount == 2); +} + +#pragma mark processQueue tests + +- (void)testDefaultQueueProcessObjectsOneAtATime +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + [NSThread sleepForTimeInterval:kRunLoopRunTime * 2]; // So each element takes more time than the available + }]; + [queue enqueue:[[NSObject alloc] init]]; + [queue enqueue:[[NSObject alloc] init]]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(queue.isEmpty); +} + +- (void)testQueueProcessObjectsInBatchesOfSpecifiedSize +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + [NSThread sleepForTimeInterval:kRunLoopRunTime * 2]; // So each element takes more time than the available + }]; + queue.batchSize = 2; + [queue enqueue:[[NSObject alloc] init]]; + [queue enqueue:[[NSObject alloc] init]]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testQueueOnlySendsIsDrainedForLastObjectInBatch +{ + id objectA = [[NSObject alloc] init]; + id objectB = [[NSObject alloc] init]; + __unsafe_unretained id weakObjectA = objectA; + __unsafe_unretained id weakObjectB = objectB; + __block BOOL isQueueDrainedWhenProcessingA = NO; + __block BOOL isQueueDrainedWhenProcessingB = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObjectA) { + isQueueDrainedWhenProcessingA = isQueueDrained; + } else if (dequeuedItem == weakObjectB) { + isQueueDrainedWhenProcessingB = isQueueDrained; + } + }]; + queue.batchSize = 2; + [queue enqueue:objectA]; + [queue enqueue:objectB]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(isQueueDrainedWhenProcessingA); + XCTAssertTrue(isQueueDrainedWhenProcessingB); +} + +#pragma mark strong/weak tests + +- (void)testStrongQueueRetainsObjects +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block BOOL didProcessObject = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + didProcessObject = YES; + } + }]; + [queue enqueue:object]; + object = nil; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(didProcessObject); +} + +- (void)testWeakQueueDoesNotRetainsObjects +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block BOOL didProcessObject = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:NO handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + didProcessObject = YES; + } + }]; + [queue enqueue:object]; + object = nil; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(didProcessObject); +} + +- (void)testWeakQueueWithAllDeallocatedObjectsIsDrained +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:NO handler:nil]; + id object = [[NSObject alloc] init]; + [queue enqueue:object]; + object = nil; + XCTAssertFalse(queue.isEmpty); + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(queue.isEmpty); +} + +@end From 9df6909d71f28100e32cc846ff12a1d2be05512a Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 11 Sep 2017 21:33:30 +0100 Subject: [PATCH 10/86] [ASImageNode] Always dealloc images in a background queue (#561) * ASImageNode to always dealloc its images in a background queue * Update CHANGELOG --- CHANGELOG.md | 4 +++- Source/ASImageNode.mm | 17 ++++++++++++++--- Source/ASNetworkImageNode.mm | 16 +--------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e6571690..648ec0b0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,10 @@ - [ASDisplayNode] Ensure `-displayWillStartAsynchronously:` and `-displayDidFinish` are invoked on rasterized subnodes. [Eric Scheers](https://github.com/smeis) [#532](https://github.com/TextureGroup/Texture/pull/532) - Fixed a memory corruption issue in the ASImageNode display system. [Adlai Holler](https://github.com/Adlai-Holler) [#555](https://github.com/TextureGroup/Texture/pull/555) - [Breaking] Rename ASCollectionGalleryLayoutSizeProviding to ASCollectionGalleryLayoutPropertiesProviding. Besides a fixed item size, it now can provide interitem and line spacings, as well as section inset [Huy Nguyen](https://github.com/nguyenhuy) [#496](https://github.com/TextureGroup/Texture/pull/496) [#533](https://github.com/TextureGroup/Texture/pull/533) -- Deprecate `-[ASDisplayNode displayWillStart]` in favor of `-displayWillStartAsynchronously:` [Huy Nguyen](https://github.com/nguyenhuy) [536](https://github.com/TextureGroup/Texture/pull/536) +- Deprecate `-[ASDisplayNode displayWillStart]` in favor of `-displayWillStartAsynchronously:` [Huy Nguyen](https://github.com/nguyenhuy) [#536](https:/ +/github.com/TextureGroup/Texture/pull/536) - Add support for URLs on ASNetworkImageNode. [Garrett Moon](https://github.com/garrettmoon) +- [ASImageNode] Always dealloc images in a background queue [Huy Nguyen](https://github.com/nguyenhuy) [#561](https://github.com/TextureGroup/Texture/pull/561) - Mark ASRunLoopQueue as drained if it contains only NULLs [Cesar Estebanez](https://github.com/cesteban) [#558](https://github.com/TextureGroup/Texture/pull/558) ##2.4 diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index dce952d2c2..165d745352 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -41,6 +41,8 @@ #include +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); @interface ASImageNodeDrawParameters : NSObject { @@ -248,11 +250,11 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); if (ASObjectIsEqual(_image, image)) { return; } - + + UIImage *oldImage = _image; _image = image; if (image != nil) { - // We explicitly call setNeedsDisplay in this case, although we know setNeedsDisplay will be called with lock held. // Therefore we have to be careful in methods that are involved with setNeedsDisplay to not run into a deadlock [self setNeedsDisplay]; @@ -265,10 +267,19 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); [self addSubnode:_debugLabelNode]; }); } - } else { self.contents = nil; } + + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + CGSize oldImageSize = oldImage.size; + BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width + || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(oldImage); + } } - (UIImage *)image diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 0e9c1fb1f9..d969322763 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -32,8 +32,6 @@ #import #endif -static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - @interface ASNetworkImageNode () { // Only access any of these with __instanceLock__. @@ -521,21 +519,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { [self _locked_cancelImageDownloadWithResumePossibility:storeResume]; - // Destruction of bigger images on the main thread can be expensive - // and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - UIImage *image = [self _locked_Image]; - UIImage *defaultImage = _defaultImage; - CGSize imageSize = image.size; - BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || - imageSize.height > kMinReleaseImageOnBackgroundSize.height; - if (shouldReleaseImageOnBackgroundThread) { - ASPerformBackgroundDeallocation(image); - } - [self _locked_setAnimatedImage:nil]; [self _locked_setCurrentImageQuality:0.0]; - [self _locked__setImage:defaultImage]; + [self _locked__setImage:_defaultImage]; _imageLoaded = NO; From 008b847a7a66495d9a63556b106be5e1285cadaf Mon Sep 17 00:00:00 2001 From: Samuel Hsiung Date: Wed, 13 Sep 2017 09:12:46 -0700 Subject: [PATCH 11/86] Fix -[ASPagerNode view] triggering pendingState + nodeLoaded assert (#564) --- Source/ASCollectionNode.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 0be5cda59f..3ff691c1ef 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -183,6 +183,7 @@ if (_pendingState) { _ASCollectionPendingState *pendingState = _pendingState; + self.pendingState = nil; view.asyncDelegate = pendingState.delegate; view.asyncDataSource = pendingState.dataSource; view.inverted = pendingState.inverted; @@ -191,7 +192,6 @@ view.usesSynchronousDataLoading = pendingState.usesSynchronousDataLoading; view.layoutInspector = pendingState.layoutInspector; view.contentInset = pendingState.contentInset; - self.pendingState = nil; if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; From b9b2d7e3cac608ec8b0b71342244206b41ade494 Mon Sep 17 00:00:00 2001 From: Max Wang Date: Sat, 16 Sep 2017 10:32:07 -0700 Subject: [PATCH 12/86] Update yoga version (#569) * fix SIMULATE_WEB_RESPONSE not imported #449 * update yoga version --- Texture.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Texture.podspec b/Texture.podspec index 1fcc6cf070..5731208e8a 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -57,7 +57,7 @@ Pod::Spec.new do |spec| spec.subspec 'Yoga' do |yoga| yoga.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) YOGA=1' } - yoga.dependency 'Yoga', '1.5.0' + yoga.dependency 'Yoga', '1.6.0' yoga.dependency 'Texture/Core' end From dcaca529b46bf8804c8d34195ddee886f0d52a3a Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Sat, 16 Sep 2017 10:36:58 -0700 Subject: [PATCH 13/86] [ASDKgram Example] fix crash on startup (#566) * fix crash on ASDKgram startup * quicker fix --- examples/ASDKgram/Sample/PhotoFeedBaseController.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/ASDKgram/Sample/PhotoFeedBaseController.m b/examples/ASDKgram/Sample/PhotoFeedBaseController.m index c901d737e2..1d883ba7b1 100644 --- a/examples/ASDKgram/Sample/PhotoFeedBaseController.m +++ b/examples/ASDKgram/Sample/PhotoFeedBaseController.m @@ -59,7 +59,7 @@ [_activityIndicatorView stopAnimating]; - [self insertNewRows:newPhotos]; + [self.tableView reloadData]; [self requestCommentsForPhotos:newPhotos]; // immediately start second larger fetch @@ -74,7 +74,8 @@ NSMutableArray *indexPaths = [NSMutableArray array]; NSInteger newTotalNumberOfPhotos = [_photoFeed numberOfItemsInFeed]; - for (NSInteger row = newTotalNumberOfPhotos - newPhotos.count; row < newTotalNumberOfPhotos; row++) { + NSInteger existingNumberOfPhotos = newTotalNumberOfPhotos - newPhotos.count; + for (NSInteger row = existingNumberOfPhotos; row < newTotalNumberOfPhotos; row++) { NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:section]; [indexPaths addObject:path]; } From cfc48679bacfd649037847136dc809d339c1ffbd Mon Sep 17 00:00:00 2001 From: appleguy Date: Sun, 17 Sep 2017 22:45:39 -0700 Subject: [PATCH 14/86] [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. (#469) * [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. * Add new "version" parameter to Yoga initialization of ASDisplayNodeLayout C++ struct. --- CHANGELOG.md | 1 + Source/ASDisplayNode+Beta.h | 5 +- Source/ASDisplayNode+Yoga.mm | 89 +++++++++++++++++++++++++-------- Source/ASDisplayNode.mm | 27 ++++++---- Source/Layout/ASLayout.mm | 21 ++++++++ Source/Layout/ASYogaUtilities.h | 4 +- 6 files changed, 115 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 648ec0b0bc..c1a53764fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) - [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) diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index e8b08a9a57..ea241bd164 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -175,12 +175,15 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable - (void)addYogaChild:(ASDisplayNode *)child; - (void)removeYogaChild:(ASDisplayNode *)child; +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index; - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; @property (nonatomic, assign) BOOL yogaLayoutInProgress; @property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout; -// These methods should not normally be called directly. + +// These methods are intended to be used internally to Texture, and should not be called directly. +- (BOOL)shouldHaveYogaMeasureFunc; - (void)invalidateCalculatedYogaLayout; - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 281bf1e781..6ef80ccf14 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -59,20 +59,7 @@ - (void)addYogaChild:(ASDisplayNode *)child { - if (child == nil) { - return; - } - if (_yogaChildren == nil) { - _yogaChildren = [NSMutableArray array]; - } - - // Clean up state in case this child had another parent. - [self removeYogaChild:child]; - - [_yogaChildren addObject:child]; - - // YGNodeRef insertion is done in setParent: - child.yogaParent = self; + [self insertYogaChild:child atIndex:_yogaChildren.count]; } - (void)removeYogaChild:(ASDisplayNode *)child @@ -87,6 +74,24 @@ child.yogaParent = nil; } +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index +{ + if (child == nil) { + return; + } + if (_yogaChildren == nil) { + _yogaChildren = [NSMutableArray array]; + } + + // Clean up state in case this child had another parent. + [self removeYogaChild:child]; + + [_yogaChildren insertObject:child atIndex:index]; + + // YGNodeRef insertion is done in setParent: + child.yogaParent = self; +} + - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute { if (AS_AT_LEAST_IOS9) { @@ -168,28 +173,72 @@ CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; - self.yogaCalculatedLayout = layout; +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // Assert that the sublayout is already flattened. + for (ASLayout *sublayout in layout.sublayouts) { + if (sublayout.sublayouts.count > 0 || ASDynamicCast(sublayout.layoutElement, ASDisplayNode) == nil) { + ASDisplayNodeAssert(NO, @"Yoga sublayout is not flattened! %@, %@", self, sublayout); + } + } +#endif + + // Because this layout won't go through the rest of the logic in calculateLayoutThatFits:, flatten it now. + layout = [layout filteredNodeLayoutTree]; + + if ([self.yogaCalculatedLayout isEqual:layout] == NO) { + self.yogaCalculatedLayout = layout; + } else { + layout = self.yogaCalculatedLayout; + ASYogaLog("-setupYogaCalculatedLayout: applying identical ASLayout: %@", layout); + } + + // Setup _pendingDisplayNodeLayout to reference the Yoga-calculated ASLayout, *unless* we are a leaf node. + // Leaf yoga nodes may have their own .sublayouts, if they use a layout spec (such as ASButtonNode). + // Their _pending variable is set after passing the Yoga checks at the start of -calculateLayoutThatFits: + + // For other Yoga nodes, there is no code that will set _pending unless we do it here. Why does it need to be set? + // When CALayer triggers the -[ASDisplayNode __layout] call, we will check if our current _pending layout + // has a size which matches our current bounds size. If it does, that layout will be used without recomputing it. + + // NOTE: Yoga does not make the constrainedSize available to intermediate nodes in the tree (e.g. not root or leaves). + // Although the size range provided here is not accurate, this will only affect caching of calls to layoutThatFits: + // These calls will behave as if they are not cached, starting a new Yoga layout pass, but this will tap into Yoga's + // own internal cache. + + if ([self shouldHaveYogaMeasureFunc] == NO) { + YGNodeRef parentNode = YGNodeGetParent(yogaNode); + CGSize parentSize = CGSizeZero; + if (parentNode) { + parentSize.width = YGNodeLayoutGetWidth(parentNode); + parentSize.height = YGNodeLayoutGetHeight(parentNode); + } + _pendingDisplayNodeLayout = std::make_shared(layout, ASSizeRangeUnconstrained, parentSize, 0); + } } -- (void)updateYogaMeasureFuncIfNeeded +- (BOOL)shouldHaveYogaMeasureFunc { // Size calculation via calculateSizeThatFits: or layoutSpecThatFits: // This will be used for ASTextNode, as well as any other node that has no Yoga children BOOL isLeafNode = (self.yogaChildren.count == 0); BOOL definesCustomLayout = [self implementsLayoutMethod]; + return (isLeafNode && definesCustomLayout); +} +- (void)updateYogaMeasureFuncIfNeeded +{ // We set the measure func only during layout. Otherwise, a cycle is created: // The YGNodeRef Context will retain the ASDisplayNode, which retains the style, which owns the YGNodeRef. - BOOL shouldHaveMeasureFunc = (isLeafNode && definesCustomLayout && checkFlag(YogaLayoutInProgress)); + BOOL shouldHaveMeasureFunc = ([self shouldHaveYogaMeasureFunc] && checkFlag(YogaLayoutInProgress)); ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, shouldHaveMeasureFunc ? self : nil); } - (void)invalidateCalculatedYogaLayout { - // Yoga internally asserts that this method may only be called on nodes with a measurement function. YGNodeRef yogaNode = self.style.yogaNode; if (yogaNode && YGNodeGetMeasureFunc(yogaNode)) { + // Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function. YGNodeMarkDirty(yogaNode); } self.yogaCalculatedLayout = nil; @@ -200,7 +249,7 @@ ASDisplayNode *yogaParent = self.yogaParent; if (yogaParent) { - ASYogaLog(@"ESCALATING to Yoga root: %@", self); + ASYogaLog("ESCALATING to Yoga root: %@", self); // TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually. [yogaParent calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained]; return; @@ -217,7 +266,7 @@ rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; } - ASYogaLog(@"CALCULATING at Yoga root with constraint = {%@, %@}: %@", + ASYogaLog("CALCULATING at Yoga root with constraint = {%@, %@}: %@", NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self); YGNodeRef rootYogaNode = self.style.yogaNode; diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index e1f1d143ce..2b2041e2a4 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -967,23 +967,30 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. - // If we're a leaf node, we are probably being called by a measure function and proceed as normal. - // If we're a yoga root or tree node, initiate a new Yoga calculation pass from root. YGNodeRef yogaNode = _style.yogaNode; BOOL hasYogaParent = (_yogaParent != nil); BOOL hasYogaChildren = (_yogaChildren.count > 0); BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); - if (usesYoga && (_yogaParent == nil || _yogaChildren.count > 0)) { + if (usesYoga) { // This node has some connection to a Yoga tree. - ASDN::MutexUnlocker ul(__instanceLock__); - - if (self.yogaLayoutInProgress == NO) { - [self calculateLayoutFromYogaRoot:constrainedSize]; + if ([self shouldHaveYogaMeasureFunc] == NO) { + // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then + // initiate a new Yoga calculation pass from root. + ASDN::MutexUnlocker ul(__instanceLock__); + as_activity_create_for_scope("Yoga layout calculation"); + if (self.yogaLayoutInProgress == NO) { + ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize)); + [self calculateLayoutFromYogaRoot:constrainedSize]; + } else { + ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); + } + ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; + } else { + // If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode). + ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); } - ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); - return _yogaCalculatedLayout; } - ASYogaLog(@"PROCEEDING past Yoga check to calculate ASLayout for: %@", self); #endif /* YOGA */ // Manual size calculation via calculateSizeThatFits: diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 031120100c..4e8d2f5026 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -271,6 +271,27 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( return layout; } +#pragma mark - Equality Checking + +- (BOOL)isEqual:(id)object +{ + ASLayout *layout = ASDynamicCast(object, ASLayout); + if (layout == nil) { + return NO; + } + + if (!CGSizeEqualToSize(_size, layout.size)) return NO; + if (!CGPointEqualToPoint(_position, layout.position)) return NO; + if (_layoutElement != layout.layoutElement) return NO; + + NSArray *sublayouts = layout.sublayouts; + if (sublayouts != _sublayouts && (sublayouts == nil || _sublayouts == nil || ![_sublayouts isEqual:sublayouts])) { + return NO; + } + + return YES; +} + #pragma mark - Accessors - (ASLayoutElementType)type diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h index 2986c0195f..b229b34358 100644 --- a/Source/Layout/ASYogaUtilities.h +++ b/Source/Layout/ASYogaUtilities.h @@ -15,9 +15,11 @@ #if YOGA /* YOGA */ #import +#import #import -#define ASYogaLog(...) //NSLog(__VA_ARGS__) +// Should pass a string literal, not an NSString as the first argument to ASYogaLog +#define ASYogaLog(x, ...) as_log_verbose(ASLayoutLog(), x, ##__VA_ARGS__); @interface ASDisplayNode (YogaHelpers) From e330e57668e62128af9aaaad370c9c6dce371721 Mon Sep 17 00:00:00 2001 From: appleguy Date: Mon, 18 Sep 2017 05:02:51 -0700 Subject: [PATCH 15/86] [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. (#465) * [ASCornerRounding] Initial (untested) implementation of ASCornerRounding features. * [ASCornerRounding] Working version of both clip corners and precomposited corners. * [ASCornerRounding] Improve factoring and documentation of corner rounding features. * [ASCornerRounding] Some final fixups. * Add entry to changelog for .cornerRoundingType --- CHANGELOG.md | 1 + Source/ASDisplayNode.h | 33 ++++- Source/ASDisplayNode.mm | 144 ++++++++++++++++++- Source/Private/ASDisplayNode+AsyncDisplay.mm | 112 ++++++++++++--- Source/Private/ASDisplayNode+UIViewBridge.mm | 39 ++++- Source/Private/ASDisplayNodeInternal.h | 13 ++ 6 files changed, 317 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a53764fe..93a3a8b09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) - [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) diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index c221e3c6ec..1100764f14 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -100,6 +100,12 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState) ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible, }; +typedef NS_ENUM(NSInteger, ASCornerRoundingType) { + ASCornerRoundingTypeDefaultSlowCALayer, + ASCornerRoundingTypePrecomposited, + ASCornerRoundingTypeClipping +}; + /** * Default drawing priority for display node */ @@ -375,7 +381,6 @@ extern NSInteger const ASDefaultDrawingPriority; /** @name Drawing and Updating the View */ - /** * @abstract Whether this node's view performs asynchronous rendering. * @@ -631,6 +636,31 @@ extern NSInteger const ASDefaultDrawingPriority; @property (nonatomic, assign) CGPoint position; // default=CGPointZero @property (nonatomic, assign) CGFloat alpha; // default=1.0f +/* @abstract Sets the corner rounding method to use on the ASDisplayNode. + * There are three types of corner rounding provided by Texture: CALayer, Precomposited, and Clipping. + * + * - ASCornerRoundingTypeDefaultSlowCALayer: uses CALayer's inefficient .cornerRadius property. Use + * this type of corner in situations in which there is both movement through and movement underneath + * the corner (very rare). This uses only .cornerRadius. + * + * - ASCornerRoundingTypePrecomposited: corners are drawn using bezier paths to clip the content in a + * CGContext / UIGraphicsContext. This requires .backgroundColor and .cornerRadius to be set. Use opaque + * background colors when possible for optimal efficiency, but transparent colors are supported and much + * more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and + * thus works best for ASImageNodes or containers showing a background around their children. + * + * - ASCornerRoundingTypeClipping: overlays 4 seperate opaque corners on top of the content that needs + * corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations + * in which is movement through the corner, with an opaque background (no movement underneath the corner). + * Clipped corners are ideal for animating / resizing views, and still outperform CALayer. + * + * For more information and examples, see http://texturegroup.org/docs/corner-rounding.html + * + * @default ASCornerRoundingTypeDefaultSlowCALayer + */ +@property (nonatomic, assign) ASCornerRoundingType cornerRoundingType; // default=Slow CALayer .cornerRadius (offscreen rendering) +@property (nonatomic, assign) CGFloat cornerRadius; // default=0.0 + @property (nonatomic, assign) BOOL clipsToBounds; // default==NO @property (nonatomic, getter=isHidden) BOOL hidden; // default==NO @property (nonatomic, getter=isOpaque) BOOL opaque; // default==YES @@ -643,7 +673,6 @@ extern NSInteger const ASDefaultDrawingPriority; @property (nonatomic, assign) CGPoint anchorPoint; // default={0.5, 0.5} @property (nonatomic, assign) CGFloat zPosition; // default=0.0 -@property (nonatomic, assign) CGFloat cornerRadius; // default=0.0 @property (nonatomic, assign) CATransform3D transform; // default=CATransform3DIdentity @property (nonatomic, assign) CATransform3D subnodeTransform; // default=CATransform3DIdentity diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 2b2041e2a4..03c1a2f049 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -62,7 +62,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; // We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 @protocol CALayerDelegate; -@interface ASDisplayNode () +@interface ASDisplayNode () /** * See ASDisplayNodeInternal.h for ivars @@ -912,6 +912,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (loaded) { ASPerformBlockOnMainThread(^{ [self layout]; + [self _layoutClipCornersIfNeeded]; [self layoutDidFinish]; }); } @@ -1465,6 +1466,147 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) }); } +- (void)_layoutClipCornersIfNeeded +{ + ASDisplayNodeAssertMainThread(); + if (_clipCornerLayers[0] == nil) { + return; + } + + CGSize boundsSize = self.bounds.size; + for (int idx = 0; idx < 4; idx++) { + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + if (_clipCornerLayers[idx]) { + // Note the Core Animation coordinates are reversed for y; 0 is at the bottom. + _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0); + [_layer addSublayer:_clipCornerLayers[idx]]; + } + } +} + +- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor +{ + ASPerformBlockOnMainThread(^{ + for (int idx = 0; idx < 4; idx++) { + // Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left. + // anchorPoint is Bottom Left at 0,0 and Top Right at 1,1. + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + + CGSize size = CGSizeMake(radius + 1, radius + 1); + UIGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + if (isRight == YES) { + CGContextTranslateCTM(ctx, -radius + 1, 0); + } + if (isTop == YES) { + CGContextTranslateCTM(ctx, 0, -radius + 1); + } + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; + [roundedRect setUsesEvenOddFillRule:YES]; + [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; + [backgroundColor setFill]; + [roundedRect fill]; + + // No lock needed, as _clipCornerLayers is only modified on the main thread. + CALayer *clipCornerLayer = _clipCornerLayers[idx]; + clipCornerLayer.contents = (id)(UIGraphicsGetImageFromCurrentImageContext().CGImage); + clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height); + clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + + UIGraphicsEndImageContext(); + } + [self _layoutClipCornersIfNeeded]; + }); +} + +- (void)_setClipCornerLayersVisible:(BOOL)visible +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + if (visible) { + for (int idx = 0; idx < 4; idx++) { + if (_clipCornerLayers[idx] == nil) { + _clipCornerLayers[idx] = [[CALayer alloc] init]; + _clipCornerLayers[idx].zPosition = 99999; + _clipCornerLayers[idx].delegate = self; + } + } + [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; + } else { + for (int idx = 0; idx < 4; idx++) { + [_clipCornerLayers[idx] removeFromSuperlayer]; + _clipCornerLayers[idx] = nil; + } + } + }); +} + +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius +{ + __instanceLock__.lock(); + CGFloat oldCornerRadius = _cornerRadius; + ASCornerRoundingType oldRoundingType = _cornerRoundingType; + + _cornerRadius = newCornerRadius; + _cornerRoundingType = newRoundingType; + __instanceLock__.unlock(); + + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + if (newRoundingType == ASCornerRoundingTypePrecomposited) { + self.layerCornerRadius = 0.0; + if (oldCornerRadius > 0.0) { + [self displayImmediately]; + } else { + [self setNeedsDisplay]; // Async display is OK if we aren't replacing an existing .cornerRadius. + } + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + self.layerCornerRadius = 0.0; + [self _setClipCornerLayersVisible:YES]; + } else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + } + } + else if (oldRoundingType == ASCornerRoundingTypePrecomposited) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + // Corners are already precomposited, but the radius has changed. + // Default to async re-display. The user may force a synchronous display if desired. + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + [self _setClipCornerLayersVisible:YES]; + [self setNeedsDisplay]; + } + } + else if (oldRoundingType == ASCornerRoundingTypeClipping) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self _setClipCornerLayersVisible:NO]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + [self _setClipCornerLayersVisible:NO]; + [self displayImmediately]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + // Clip corners already exist, but the radius has changed. + [self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor]; + } + } + } + }); +} + - (void)recursivelySetDisplaySuspended:(BOOL)flag { _recursivelySetDisplaySuspended(self, nil, flag); diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index e22f461497..986f3acf99 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -163,6 +163,8 @@ isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + ASDisplayNodeAssertMainThread(); + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; ASDisplayNodeFlags flags; @@ -184,6 +186,9 @@ BOOL opaque = self.opaque; CGRect bounds = self.bounds; + UIColor *backgroundColor = self.backgroundColor; + CGColorRef borderColor = self.borderColor; + CGFloat borderWidth = self.borderWidth; CGFloat contentsScaleForDisplay = _contentsScaleForDisplay; __instanceLock__.unlock(); @@ -208,7 +213,7 @@ // If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing. // Unlike CALayer drawing, we include the backgroundColor as a base during rasterization. - opaque = opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f; + opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f; displayBlock = ^id{ CHECK_CANCELLED_AND_RETURN_NIL(); @@ -237,22 +242,10 @@ CGContextRef currentContext = UIGraphicsGetCurrentContext(); UIImage *image = nil; - - ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = nil; - ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = nil; - if (currentContext) { - __instanceLock__.lock(); - willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; - didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; - __instanceLock__.unlock(); - } - - + // For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. - if (willDisplayNodeContentWithRenderingContext != nil) { - willDisplayNodeContentWithRenderingContext(currentContext, drawParameters); - } + [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly. image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock]; @@ -260,9 +253,7 @@ [self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; } - if (didDisplayNodeContentWithRenderingContext != nil) { - didDisplayNodeContentWithRenderingContext(currentContext, drawParameters); - } + [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; if (shouldCreateGraphicsContext) { CHECK_CANCELLED_AND_RETURN_NIL( UIGraphicsEndImageContext(); ); @@ -292,6 +283,91 @@ return displayBlock; } +- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters +{ + if (context) { + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) { + ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + // TODO: This clip path should be removed if we are rasterizing. + CGRect boundingBox = CGContextGetClipBoundingBox(context); + [[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip]; + } + + if (willDisplayNodeContentWithRenderingContext) { + willDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + +} +- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor +{ + if (context == NULL && *image == NULL) { + return; + } + + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + CGFloat contentsScale = _contentsScaleForDisplay; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (context != NULL) { + if (didDisplayNodeContentWithRenderingContext) { + didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) { + CGRect bounds = CGRectZero; + if (context == NULL) { + bounds = self.threadSafeBounds; + bounds.size.width *= contentsScale; + bounds.size.height *= contentsScale; + CGFloat white = 0.0f, alpha = 0.0f; + [backgroundColor getWhite:&white alpha:&alpha]; + UIGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); + [*image drawInRect:bounds]; + } else { + bounds = CGContextGetClipBoundingBox(context); + } + + ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + + UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds]; + [roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]]; + roundedHole.usesEvenOddFillRule = YES; + + UIBezierPath *roundedPath = nil; + if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0 + CGFloat strokeThickness = borderWidth * contentsScale; + CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f; + roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset) + cornerRadius:_cornerRadius * contentsScale]; + roundedPath.lineWidth = strokeThickness; + [[UIColor colorWithCGColor:borderColor] setStroke]; + } + + // Punch out the corners by copying the backgroundColor over them. + // This works for everything from clearColor to opaque colors. + [backgroundColor setFill]; + [roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f]; + + [roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil. + + if (*image) { + *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + } +} + - (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { ASDisplayNodeAssertMainThread(); diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index cd3705921c..ebb50600a8 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -185,14 +185,30 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo - (CGFloat)cornerRadius { - _bridge_prologue_read; - return _getFromLayer(cornerRadius); + ASDN::MutexLocker l(__instanceLock__); + if (_cornerRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + return self.layerCornerRadius; + } else { + return _cornerRadius; + } } - (void)setCornerRadius:(CGFloat)newCornerRadius { - _bridge_prologue_write; - _setToLayer(cornerRadius, newCornerRadius); + ASDN::MutexLocker l(__instanceLock__); + [self updateCornerRoundingWithType:_cornerRoundingType cornerRadius:newCornerRadius]; +} + +- (ASCornerRoundingType)cornerRoundingType +{ + ASDN::MutexLocker l(__instanceLock__); + return _cornerRoundingType; +} + +- (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType +{ + ASDN::MutexLocker l(__instanceLock__); + [self updateCornerRoundingWithType:newRoundingType cornerRadius:_cornerRadius]; } - (NSString *)contentsGravity @@ -857,6 +873,21 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo @end +@implementation ASDisplayNode (InternalPropertyBridge) + +- (CGFloat)layerCornerRadius +{ + _bridge_prologue_read; + return _getFromLayer(cornerRadius); +} + +- (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius +{ + _bridge_prologue_write; + _setToLayer(cornerRadius, newLayerCornerRadius); +} + +@end #pragma mark - UIViewBridgeAccessibility diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 0d1982a291..4e380c4eb8 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -173,6 +173,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo // keeps track of nodes/subnodes that have not finished display, used with placeholders ASWeakSet *_pendingDisplayNodes; + + CGFloat _cornerRadius; + ASCornerRoundingType _cornerRoundingType; + CALayer *_clipCornerLayers[4]; ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; @@ -274,6 +278,9 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo /// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. - (void)displayImmediately; +/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type. +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius; + /// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. - (instancetype)initWithViewClass:(Class)viewClass; @@ -318,4 +325,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo @end +@interface ASDisplayNode (InternalPropertyBridge) + +@property (nonatomic, assign) CGFloat layerCornerRadius; + +@end + NS_ASSUME_NONNULL_END From d4846dc292236ed3a23b54933b840b6637c7214a Mon Sep 17 00:00:00 2001 From: Sev Date: Mon, 18 Sep 2017 23:18:14 +0800 Subject: [PATCH 16/86] [ASElementMap] Fix indexPath's section or item is actually negative #trivial (#457) * [ASElementMap] Fix indexPath's section or item is actually negative. * Small code style changes --- Source/Details/ASElementMap.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Details/ASElementMap.m b/Source/Details/ASElementMap.m index 716e40301c..ddcd18c99e 100644 --- a/Source/Details/ASElementMap.m +++ b/Source/Details/ASElementMap.m @@ -218,7 +218,7 @@ - (BOOL)sectionIndexIsValid:(NSInteger)section assert:(BOOL)assert { NSInteger sectionCount = _sectionsOfItems.count; - if (section >= sectionCount) { + if (section >= sectionCount || section < 0) { if (assert) { ASDisplayNodeFailAssert(@"Invalid section index %zd when there are only %zd sections!", section, sectionCount); } @@ -246,7 +246,7 @@ NSInteger itemCount = _sectionsOfItems[section].count; NSInteger item = indexPath.item; - if (item >= itemCount) { + if (item >= itemCount || item < 0) { if (assert) { ASDisplayNodeFailAssert(@"Invalid item index %zd in section %zd which only has %zd items!", item, section, itemCount); } From 76eccbf26921936798d1a8a2fd961d9e0ee499f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20H=C3=BCllmandel?= Date: Mon, 18 Sep 2017 19:07:01 +0200 Subject: [PATCH 17/86] Added attributed versions of accessibilityLabel, accessibilityHint, accessibilityValue (#554) * Added attributed versions of accessibilityLabel, accessibilityHint and accessibilityValue * Follow conventions for property types * Use curly braces * Update changelog * Follow conventions for property types in UIView+ASConvenience.h * Add compatibility for Xcode 8 * Use isEqualToString instead of pointer comparison * Only allocate attributed strings once. Use _setAttributedAccessibilityToViewAndProperty only for attributed properties. --- CHANGELOG.md | 1 + Source/ASDisplayNode.h | 3 + Source/Details/UIView+ASConvenience.h | 11 ++- Source/Details/_ASDisplayViewAccessiblity.mm | 23 ++++- Source/Private/ASDisplayNode+UIViewBridge.mm | 61 +++++++++++++ Source/Private/ASDisplayNodeInternal.h | 3 + Source/Private/_ASPendingState.mm | 93 ++++++++++++++++++-- Tests/ASDisplayNodeTests.mm | 13 +++ 8 files changed, 196 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a3a8b09b..9139129598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) - [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. [#522](https://github.com/TextureGroup/Texture/pull/522) [Scott Goodson](https://github.com/appleguy) diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 1100764f14..84da628b54 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -745,8 +745,11 @@ extern NSInteger const ASDefaultDrawingPriority; // Accessibility support @property (nonatomic, assign) BOOL isAccessibilityElement; @property (nonatomic, copy, nullable) NSString *accessibilityLabel; +@property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, copy, nullable) NSString *accessibilityHint; +@property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, copy, nullable) NSString *accessibilityValue; +@property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, assign) UIAccessibilityTraits accessibilityTraits; @property (nonatomic, assign) CGRect accessibilityFrame; @property (nonatomic, copy, nullable) UIBezierPath *accessibilityPath; diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 537a4ae9e7..109bf78128 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -82,12 +82,15 @@ NS_ASSUME_NONNULL_BEGIN as they are already on NSObject @property (nonatomic, assign) BOOL isAccessibilityElement; - @property (nonatomic, copy) NSString *accessibilityLabel; - @property (nonatomic, copy) NSString *accessibilityHint; - @property (nonatomic, copy) NSString *accessibilityValue; + @property (nonatomic, copy, nullable) NSString *accessibilityLabel; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic, copy, nullable) NSString *accessibilityHint; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic, copy, nullable) NSString *accessibilityValue; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, assign) UIAccessibilityTraits accessibilityTraits; @property (nonatomic, assign) CGRect accessibilityFrame; - @property (nonatomic, strong) NSString *accessibilityLanguage; + @property (nonatomic, strong, nullable) NSString *accessibilityLanguage; @property (nonatomic, assign) BOOL accessibilityElementsHidden; @property (nonatomic, assign) BOOL accessibilityViewIsModal; @property (nonatomic, assign) BOOL shouldGroupAccessibilityChildren; diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 80ae2a9223..abac3f7cc8 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -18,6 +18,7 @@ #ifndef ASDK_ACCESSIBILITY_DISABLE #import +#import #import #import #import @@ -83,6 +84,11 @@ static void SortAccessibilityElements(NSMutableArray *elements) accessibilityElement.accessibilityHint = node.accessibilityHint; accessibilityElement.accessibilityValue = node.accessibilityValue; accessibilityElement.accessibilityTraits = node.accessibilityTraits; + if (AS_AT_LEAST_IOS11) { + [accessibilityElement setValue:node.accessibilityAttributedLabel forKey:@"accessibilityAttributedLabel"]; + [accessibilityElement setValue:node.accessibilityAttributedHint forKey:@"accessibilityAttributedHint"]; + [accessibilityElement setValue:node.accessibilityAttributedValue forKey:@"accessibilityAttributedValue"]; + } return accessibilityElement; } @@ -169,8 +175,21 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, _ } SortAccessibilityElements(labeledNodes); - NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; - accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + + if (AS_AT_LEAST_IOS11) { + NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; + NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; + [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (idx != 0) { + [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; + } + [attributedLabel appendAttributedString:(NSAttributedString *)obj]; + }]; + [accessiblityElement setValue:attributedLabel forKey:@"accessibilityAttributedLabel"]; + } else { + NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; + accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + } SortAccessibilityElements(actions); accessiblityElement.accessibilityCustomActions = actions; diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index ebb50600a8..a28bad10a0 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -904,10 +904,20 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo (_view ? _view.viewAndPendingViewStateProperty : nodeProperty )\ : ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty +// Attributed version of `_getAccessibilityFromViewOrProperty` macro +#define _getAttributedAccessibilityFromViewOrProperty(nodeProperty, viewAndPendingViewStatePropertyKey) __loaded(self) ? \ +(_view ? (NSAttributedString *)[_view valueForKey: viewAndPendingViewStatePropertyKey] : nodeProperty )\ +: (NSAttributedString *)[ASDisplayNodeGetPendingState(self) valueForKey: viewAndPendingViewStatePropertyKey] + // Helper function to set property values on pending state or view and property if loaded #define _setAccessibilityToViewAndProperty(nodeProperty, nodeValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) \ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) +// Attributed version of `_setAccessibilityToViewAndProperty` macro +#define _setAttributedAccessibilityToViewAndProperty(nodeProperty, nodeValueExpr, viewAndPendingViewStatePropertyKey, viewAndPendingViewStateExpr) \ +nodeProperty = nodeValueExpr; BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ +if (shouldApply) { [_view setValue:(viewAndPendingViewStateExpr) forKey: viewAndPendingViewStatePropertyKey]; } else { [ASDisplayNodeGetPendingState(self) setValue:(viewAndPendingViewStateExpr) forKey:viewAndPendingViewStatePropertyKey]; } + @implementation ASDisplayNode (UIViewBridgeAccessibility) - (BOOL)isAccessibilityElement @@ -932,6 +942,23 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityLabel, accessibilityLabel, accessibilityLabel); + if (AS_AT_LEAST_IOS11) { + NSAttributedString *accessibilityAttributedLabel = [[NSAttributedString alloc] initWithString:accessibilityLabel]; + _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, @"accessibilityAttributedLabel", accessibilityAttributedLabel); + } +} + +- (NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_read; + return _getAttributedAccessibilityFromViewOrProperty(_accessibilityAttributedLabel, @"accessibilityAttributedLabel"); +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_write; + { _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, @"accessibilityAttributedLabel", accessibilityAttributedLabel); } + { _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityAttributedLabel.string, accessibilityLabel, accessibilityAttributedLabel.string); } } - (NSString *)accessibilityHint @@ -944,6 +971,23 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityHint, accessibilityHint, accessibilityHint); + if (AS_AT_LEAST_IOS11) { + NSAttributedString *accessibilityAttributedHint = [[NSAttributedString alloc] initWithString:accessibilityHint]; + _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, @"accessibilityAttributedHint", accessibilityAttributedHint); + } +} + +- (NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_read; + return _getAttributedAccessibilityFromViewOrProperty(_accessibilityAttributedHint, @"accessibilityAttributedHint"); +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_write; + { _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, @"accessibilityAttributedHint", accessibilityAttributedHint); } + { _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityAttributedHint.string, accessibilityHint, accessibilityAttributedHint.string); } } - (NSString *)accessibilityValue @@ -956,6 +1000,23 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityValue, accessibilityValue, accessibilityValue); + if (AS_AT_LEAST_IOS11) { + NSAttributedString *accessibilityAttributedValue = [[NSAttributedString alloc] initWithString:accessibilityValue]; + _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, @"accessibilityAttributedValue", accessibilityAttributedValue); + } +} + +- (NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_read; + return _getAttributedAccessibilityFromViewOrProperty(_accessibilityAttributedValue, @"accessibilityAttributedValue"); +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_write; + { _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, @"accessibilityAttributedValue", accessibilityAttributedValue); } + { _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityAttributedValue.string, accessibilityValue, accessibilityAttributedValue.string); } } - (UIAccessibilityTraits)accessibilityTraits diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 4e380c4eb8..1be9e8c94c 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -184,8 +184,11 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo // Accessibility support BOOL _isAccessibilityElement; NSString *_accessibilityLabel; + NSAttributedString *_accessibilityAttributedLabel; NSString *_accessibilityHint; + NSAttributedString *_accessibilityAttributedHint; NSString *_accessibilityValue; + NSAttributedString *_accessibilityAttributedValue; UIAccessibilityTraits _accessibilityTraits; CGRect _accessibilityFrame; NSString *_accessibilityLanguage; diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 411a1f9f8d..cabeab0386 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -73,8 +73,11 @@ typedef struct { int setEdgeAntialiasingMask:1; int setIsAccessibilityElement:1; int setAccessibilityLabel:1; + int setAccessibilityAttributedLabel:1; int setAccessibilityHint:1; + int setAccessibilityAttributedHint:1; int setAccessibilityValue:1; + int setAccessibilityAttributedValue:1; int setAccessibilityTraits:1; int setAccessibilityFrame:1; int setAccessibilityLanguage:1; @@ -121,8 +124,11 @@ typedef struct { BOOL asyncTransactionContainer; BOOL isAccessibilityElement; NSString *accessibilityLabel; + NSAttributedString *accessibilityAttributedLabel; NSString *accessibilityHint; + NSAttributedString *accessibilityAttributedHint; NSString *accessibilityValue; + NSAttributedString *accessibilityAttributedValue; UIAccessibilityTraits accessibilityTraits; CGRect accessibilityFrame; NSString *accessibilityLanguage; @@ -271,8 +277,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; borderColor = blackColorRef; isAccessibilityElement = NO; accessibilityLabel = nil; + accessibilityAttributedLabel = nil; accessibilityHint = nil; + accessibilityAttributedHint = nil; accessibilityValue = nil; + accessibilityAttributedValue = nil; accessibilityTraits = UIAccessibilityTraitNone; accessibilityFrame = CGRectZero; accessibilityLanguage = nil; @@ -586,9 +595,26 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel { - _flags.setAccessibilityLabel = YES; - if (accessibilityLabel != newAccessibilityLabel) { + if (![accessibilityLabel isEqualToString:newAccessibilityLabel]) { + _flags.setAccessibilityLabel = YES; + _flags.setAccessibilityAttributedLabel = YES; accessibilityLabel = [newAccessibilityLabel copy]; + accessibilityAttributedLabel = [[NSAttributedString alloc] initWithString:newAccessibilityLabel]; + } +} + +- (NSAttributedString *)accessibilityAttributedLabel +{ + return accessibilityAttributedLabel; +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)newAccessibilityAttributedLabel +{ + if (![accessibilityAttributedLabel isEqualToAttributedString: newAccessibilityAttributedLabel]) { + _flags.setAccessibilityAttributedLabel = YES; + _flags.setAccessibilityLabel = YES; + accessibilityAttributedLabel = [newAccessibilityAttributedLabel copy]; + accessibilityLabel = [newAccessibilityAttributedLabel.string copy]; } } @@ -599,8 +625,27 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityHint:(NSString *)newAccessibilityHint { - _flags.setAccessibilityHint = YES; - accessibilityHint = [newAccessibilityHint copy]; + if (![accessibilityHint isEqualToString:newAccessibilityHint]) { + _flags.setAccessibilityHint = YES; + _flags.setAccessibilityAttributedHint = YES; + accessibilityHint = [newAccessibilityHint copy]; + accessibilityAttributedHint = [[NSAttributedString alloc] initWithString:newAccessibilityHint]; + } +} + +- (NSAttributedString *)accessibilityAttributedHint +{ + return accessibilityAttributedHint; +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)newAccessibilityAttributedHint +{ + if (![accessibilityAttributedHint isEqual:newAccessibilityAttributedHint]) { + _flags.setAccessibilityAttributedHint = YES; + _flags.setAccessibilityHint = YES; + accessibilityAttributedHint = [newAccessibilityAttributedHint copy]; + accessibilityHint = [newAccessibilityAttributedHint.string copy]; + } } - (NSString *)accessibilityValue @@ -610,8 +655,27 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityValue:(NSString *)newAccessibilityValue { - _flags.setAccessibilityValue = YES; - accessibilityValue = [newAccessibilityValue copy]; + if (![accessibilityValue isEqualToString:newAccessibilityValue]) { + _flags.setAccessibilityValue = YES; + _flags.setAccessibilityAttributedValue = YES; + accessibilityValue = [newAccessibilityValue copy]; + accessibilityAttributedValue = [[NSAttributedString alloc] initWithString:newAccessibilityValue]; + } +} + +- (NSAttributedString *)accessibilityAttributedValue +{ + return accessibilityAttributedValue; +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)newAccessibilityAttributedValue +{ + if (![accessibilityAttributedValue isEqualToAttributedString:newAccessibilityAttributedValue]) { + _flags.setAccessibilityAttributedValue = YES; + _flags.setAccessibilityValue = YES; + accessibilityAttributedValue = [newAccessibilityAttributedValue copy]; + accessibilityValue = [newAccessibilityAttributedValue.string copy]; + } } - (UIAccessibilityTraits)accessibilityTraits @@ -994,12 +1058,21 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; if (flags.setAccessibilityLabel) view.accessibilityLabel = accessibilityLabel; + if (AS_AT_LEAST_IOS11 && flags.setAccessibilityAttributedLabel) + [view setValue:accessibilityAttributedLabel forKey:@"accessibilityAttributedLabel"]; + if (flags.setAccessibilityHint) view.accessibilityHint = accessibilityHint; + if (AS_AT_LEAST_IOS11 && flags.setAccessibilityAttributedHint) + [view setValue:accessibilityAttributedHint forKey:@"accessibilityAttributedHint"]; + if (flags.setAccessibilityValue) view.accessibilityValue = accessibilityValue; + if (AS_AT_LEAST_IOS11 && flags.setAccessibilityAttributedValue) + [view setValue:accessibilityAttributedValue forKey:@"accessibilityAttributedValue"]; + if (flags.setAccessibilityTraits) view.accessibilityTraits = accessibilityTraits; @@ -1142,6 +1215,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; pendingState.accessibilityLabel = view.accessibilityLabel; pendingState.accessibilityHint = view.accessibilityHint; pendingState.accessibilityValue = view.accessibilityValue; + if (AS_AT_LEAST_IOS11) { + pendingState.accessibilityAttributedLabel = [view valueForKey: @"accessibilityAttributedLabel"]; + pendingState.accessibilityAttributedHint = [view valueForKey: @"accessibilityAttributedHint"]; + pendingState.accessibilityAttributedValue = [view valueForKey: @"accessibilityAttributedValue"]; + } pendingState.accessibilityTraits = view.accessibilityTraits; pendingState.accessibilityFrame = view.accessibilityFrame; pendingState.accessibilityLanguage = view.accessibilityLanguage; @@ -1219,8 +1297,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; || flags.setSemanticContentAttribute || flags.setIsAccessibilityElement || flags.setAccessibilityLabel + || flags.setAccessibilityAttributedLabel || flags.setAccessibilityHint + || flags.setAccessibilityAttributedHint || flags.setAccessibilityValue + || flags.setAccessibilityAttributedValue || flags.setAccessibilityTraits || flags.setAccessibilityFrame || flags.setAccessibilityLanguage diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index fac3f50929..f7eb1d6bc1 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -22,6 +22,7 @@ #import #import +#import #import #import #import @@ -352,6 +353,11 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqual((id)nil, node.accessibilityLabel, @"default accessibilityLabel is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityHint, @"default accessibilityHint is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityValue, @"default accessibilityValue is broken %@", hasLoadedView); + if (AS_AT_LEAST_IOS11) { + XCTAssertEqual((id)nil, node.accessibilityAttributedLabel, @"default accessibilityAttributedLabel is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityAttributedHint, @"default accessibilityAttributedHint is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityAttributedValue, @"default accessibilityAttributedValue is broken %@", hasLoadedView); + } XCTAssertEqual(UIAccessibilityTraitNone, node.accessibilityTraits, @"default accessibilityTraits is broken %@", hasLoadedView); XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.accessibilityFrame), @"default accessibilityFrame is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityLanguage, @"default accessibilityLanguage is broken %@", hasLoadedView); @@ -450,6 +456,13 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqualObjects(@"Ship love", node.accessibilityLabel, @"accessibilityLabel broken %@", hasLoadedView); XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityHint, @"accessibilityHint broken %@", hasLoadedView); XCTAssertEqualObjects(@"1 of 2", node.accessibilityValue, @"accessibilityValue broken %@", hasLoadedView); + + // setting the accessibilityLabel, accessibilityHint and accessibilityValue is supposed to be bridged to the attributed versions + if (AS_AT_LEAST_IOS11) { + XCTAssertEqualObjects(@"Ship love", node.accessibilityAttributedLabel.string, @"accessibilityAttributedLabel is broken %@", hasLoadedView); + XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityAttributedHint.string, @"accessibilityAttributedHint is broken %@", hasLoadedView); + XCTAssertEqualObjects(@"1 of 2", node.accessibilityAttributedValue.string, @"accessibilityAttributedValue is broken %@", hasLoadedView); + } XCTAssertEqual(UIAccessibilityTraitSelected | UIAccessibilityTraitButton, node.accessibilityTraits, @"accessibilityTraits broken %@", hasLoadedView); XCTAssertTrue(CGRectEqualToRect(CGRectMake(1, 2, 3, 4), node.accessibilityFrame), @"accessibilityFrame broken %@", hasLoadedView); XCTAssertEqualObjects(@"mas", node.accessibilityLanguage, @"accessibilityLanguage broken %@", hasLoadedView); From 8e0aa1ea73be002aeddd5b64fbfd6ef205c333e5 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 22 Sep 2017 11:53:41 +0100 Subject: [PATCH 18/86] Fix crashes caused by failing to unlock or destroy a static mutex while the app is being terminated (#577) * Fix crashes caused by failing to unlock or destroy a static mutex while the app is being terminated * Allocate static mutexes on the heap memory to avoid destruction at app exit * ASThread to use ASDisplayNodeCAssert() instead of assert() --- CHANGELOG.md | 1 + Source/ASImageNode.mm | 7 +++-- Source/ASTextNode.mm | 7 +++-- Source/ASTextNode2.mm | 5 ++-- Source/Details/ASBasicImageDownloader.mm | 7 +++-- Source/Details/ASThread.h | 35 ++++++++++-------------- Source/TextKit/ASTextKitContext.mm | 5 ++-- 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9139129598..58d653bf94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Add support for URLs on ASNetworkImageNode. [Garrett Moon](https://github.com/garrettmoon) - [ASImageNode] Always dealloc images in a background queue [Huy Nguyen](https://github.com/nguyenhuy) [#561](https://github.com/TextureGroup/Texture/pull/561) - Mark ASRunLoopQueue as drained if it contains only NULLs [Cesar Estebanez](https://github.com/cesteban) [#558](https://github.com/TextureGroup/Texture/pull/558) +- Fix crashes caused by failing to unlock or destroy a static mutex while the app is being terminated [Huy Nguyen](https://github.com/nguyenhuy) ##2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index 165d745352..df39741b00 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -442,12 +442,13 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); } static ASWeakMap *cache = nil; -static ASDN::Mutex cacheLock; +// Allocate cacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& cacheLock = *new ASDN::StaticMutex; + (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled { { - ASDN::MutexLocker l(cacheLock); + ASDN::StaticMutexLocker l(cacheLock); if (!cache) { cache = [[ASWeakMap alloc] init]; } @@ -464,7 +465,7 @@ static ASDN::Mutex cacheLock; } { - ASDN::MutexLocker l(cacheLock); + ASDN::StaticMutexLocker l(cacheLock); return [cache setObject:contents forKey:key]; } } diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 18f0198efc..6caaec8cd9 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -1384,7 +1384,8 @@ static NSAttributedString *DefaultTruncationAttributedString() } #endif -static ASDN::Mutex _experimentLock; +// Allocate _experimentLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& _experimentLock = *new ASDN::StaticMutex; static ASTextNodeExperimentOptions _experimentOptions; static BOOL _hasAllocatedNode; @@ -1392,7 +1393,7 @@ static BOOL _hasAllocatedNode; { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ASDN::MutexLocker lock(_experimentLock); + ASDN::StaticMutexLocker lock(_experimentLock); // They must call this before allocating any text nodes. ASDisplayNodeAssertFalse(_hasAllocatedNode); @@ -1427,7 +1428,7 @@ static BOOL _hasAllocatedNode; { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ASDN::MutexLocker lock(_experimentLock); + ASDN::StaticMutexLocker lock(_experimentLock); _hasAllocatedNode = YES; }); diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 48ff3c4bb9..ecd9496c51 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -376,7 +376,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; text:(NSAttributedString *)text { - static ASDN::Mutex layoutCacheLock; + // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) + static ASDN::StaticMutex& layoutCacheLock = *new ASDN::StaticMutex; static NSCache *textLayoutCache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -384,7 +385,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; }); ASTextCacheValue *cacheValue = ({ - ASDN::MutexLocker lock(layoutCacheLock); + ASDN::StaticMutexLocker lock(layoutCacheLock); cacheValue = [textLayoutCache objectForKey:text]; if (cacheValue == nil) { cacheValue = [[ASTextCacheValue alloc] init]; diff --git a/Source/Details/ASBasicImageDownloader.mm b/Source/Details/ASBasicImageDownloader.mm index 57fb412144..0d80842654 100644 --- a/Source/Details/ASBasicImageDownloader.mm +++ b/Source/Details/ASBasicImageDownloader.mm @@ -46,11 +46,12 @@ NSString * const kASBasicImageDownloaderContextCompletionBlock = @"kASBasicImage @implementation ASBasicImageDownloaderContext static NSMutableDictionary *currentRequests = nil; -static ASDN::RecursiveMutex currentRequestsLock; +// Allocate currentRequestsLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& currentRequestsLock = *new ASDN::StaticMutex; + (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL { - ASDN::MutexLocker l(currentRequestsLock); + ASDN::StaticMutexLocker l(currentRequestsLock); if (!currentRequests) { currentRequests = [[NSMutableDictionary alloc] init]; } @@ -64,7 +65,7 @@ static ASDN::RecursiveMutex currentRequestsLock; + (void)cancelContextWithURL:(NSURL *)URL { - ASDN::MutexLocker l(currentRequestsLock); + ASDN::StaticMutexLocker l(currentRequestsLock); if (currentRequests) { [currentRequests removeObjectForKey:URL]; } diff --git a/Source/Details/ASThread.h b/Source/Details/ASThread.h index ca049d72a4..989db9a26c 100644 --- a/Source/Details/ASThread.h +++ b/Source/Details/ASThread.h @@ -52,12 +52,6 @@ static inline BOOL ASDisplayNodeThreadIsMain() #include -/** - For use with ASDN::StaticMutex only. - */ -#define ASDISPLAYNODE_MUTEX_INITIALIZER {PTHREAD_MUTEX_INITIALIZER} -#define ASDISPLAYNODE_MUTEX_RECURSIVE_INITIALIZER {PTHREAD_RECURSIVE_MUTEX_INITIALIZER} - // This MUST always execute, even when assertions are disabled. Otherwise all lock operations become no-ops! // (To be explicit, do not turn this into an NSAssert, assert(), or any other kind of statement where the // evaluation of x_ can be compiled out.) @@ -65,7 +59,7 @@ static inline BOOL ASDisplayNodeThreadIsMain() _Pragma("clang diagnostic push"); \ _Pragma("clang diagnostic ignored \"-Wunused-variable\""); \ volatile int res = (x_); \ - assert(res == 0); \ + ASDisplayNodeCAssert(res == 0, @"Expected %@ to return 0, got %d instead", @#x_, res); \ _Pragma("clang diagnostic pop"); \ } while (0) @@ -142,7 +136,7 @@ namespace ASDN { #if !TIME_LOCKER SharedLocker (std::shared_ptr const& l) ASDISPLAYNODE_NOTHROW : _l (l) { - assert(_l != nullptr); + ASDisplayNodeCAssertTrue(_l != nullptr); _l->lock (); } @@ -217,12 +211,12 @@ namespace ASDN { mach_port_t thread_id = pthread_mach_thread_np(pthread_self()); if (thread_id != _owner) { // New owner. Since this mutex can't be acquired by another thread if there is an existing owner, _owner and _count must be 0. - assert(0 == _owner); - assert(0 == _count); + ASDisplayNodeCAssertTrue(0 == _owner); + ASDisplayNodeCAssertTrue(0 == _count); _owner = thread_id; } else { // Existing owner tries to reacquire this (recursive) mutex. _count must already be positive. - assert(_count > 0); + ASDisplayNodeCAssertTrue(_count > 0); } ++_count; #endif @@ -232,9 +226,9 @@ namespace ASDN { #if CHECK_LOCKING_SAFETY mach_port_t thread_id = pthread_mach_thread_np(pthread_self()); // Unlocking a mutex on an unowning thread causes undefined behaviour. Assert and fail early. - assert(thread_id == _owner); + ASDisplayNodeCAssertTrue(thread_id == _owner); // Current thread owns this mutex. _count must be positive. - assert(_count > 0); + ASDisplayNodeCAssertTrue(_count > 0); --_count; if (0 == _count) { // Current thread is no longer the owner. @@ -297,18 +291,19 @@ namespace ASDN { typedef SharedUnlocker MutexSharedUnlocker; /** - If you are creating a static mutex, use StaticMutex and specify its default value as one of ASDISPLAYNODE_MUTEX_INITIALIZER - or ASDISPLAYNODE_MUTEX_RECURSIVE_INITIALIZER. This avoids expensive constructor overhead at startup (or worse, ordering + If you are creating a static mutex, use StaticMutex. This avoids expensive constructor overhead at startup (or worse, ordering issues between different static objects). It also avoids running a destructor on app exit time (needless expense). Note that you can, but should not, use StaticMutex for non-static objects. It will leak its mutex on destruction, so avoid that! - - If you fail to specify a default value (like ASDISPLAYNODE_MUTEX_INITIALIZER) an assert will be thrown when you attempt to lock. */ struct StaticMutex { - pthread_mutex_t _m; // public so it can be provided by ASDISPLAYNODE_MUTEX_INITIALIZER and friends + StaticMutex () : _m (PTHREAD_MUTEX_INITIALIZER) {} + + // non-copyable. + StaticMutex(const StaticMutex&) = delete; + StaticMutex &operator=(const StaticMutex&) = delete; void lock () { ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_lock (this->mutex())); @@ -320,8 +315,8 @@ namespace ASDN { pthread_mutex_t *mutex () { return &_m; } - StaticMutex(const StaticMutex&) = delete; - StaticMutex &operator=(const StaticMutex&) = delete; + private: + pthread_mutex_t _m; }; typedef Locker StaticMutexLocker; diff --git a/Source/TextKit/ASTextKitContext.mm b/Source/TextKit/ASTextKitContext.mm index 64fcf65092..f8b3d15655 100755 --- a/Source/TextKit/ASTextKitContext.mm +++ b/Source/TextKit/ASTextKitContext.mm @@ -40,8 +40,9 @@ { if (self = [super init]) { // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. - static ASDN::Mutex __staticMutex; - ASDN::MutexLocker l(__staticMutex); + // Allocate __staticMutex on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) + static ASDN::StaticMutex& __staticMutex = *new ASDN::StaticMutex; + ASDN::StaticMutexLocker l(__staticMutex); __instanceLock__ = std::make_shared(); From 1c9738ab7e86967bfb1e1de836dc877c602feaaa Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 26 Sep 2017 12:12:34 -0700 Subject: [PATCH 19/86] Update podspec and changelog for v2.5 --- CHANGELOG.md | 7 +++++-- Texture.podspec | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d653bf94..3bd11f3285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) + +## 2.5 + - [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) @@ -29,7 +32,7 @@ - Mark ASRunLoopQueue as drained if it contains only NULLs [Cesar Estebanez](https://github.com/cesteban) [#558](https://github.com/TextureGroup/Texture/pull/558) - Fix crashes caused by failing to unlock or destroy a static mutex while the app is being terminated [Huy Nguyen](https://github.com/nguyenhuy) -##2.4 +## 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/) @@ -43,7 +46,7 @@ - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) - 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 +## 2.3.4 - [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy) - [ASTraitCollection] Convert ASPrimitiveTraitCollection from lock to atomic. [Scott Goodson](https://github.com/appleguy) - Add a synchronous mode to ASCollectionNode, for colletion view data source debugging. [Hannah Troisi](https://github.com/hannahmbanana) diff --git a/Texture.podspec b/Texture.podspec index 5731208e8a..5c353ed258 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '2.4' + spec.version = '2.5' spec.license = { :type => 'BSD and Apache 2', } spec.homepage = 'http://texturegroup.org' spec.authors = { 'Huy Nguyen' => 'huy@pinterest.com', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'schneider@pinterest.com', 'Adlai Holler' => 'adlai@pinterest.com' } From e295ba870592db41616af252beb3fd99a5f2df0f Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Tue, 26 Sep 2017 16:18:50 -0700 Subject: [PATCH 20/86] Update showcase.md (#587) * Update showcase.md * Update showcase.md --- docs/showcase.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/showcase.md b/docs/showcase.md index b90d9957a3..3a55315099 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -189,6 +189,28 @@ permalink: /showcase.html HakkerJobs + + +
+ TraceMe + + + + + + + + +
+ Pairs + + + + +
+ Sorted: Master Your Day + + From 40551c746bbcb82b5c1411bafb9968147b2066b0 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Wed, 27 Sep 2017 01:23:30 -0700 Subject: [PATCH 21/86] Rolling back CI to known version for now (#585) * Rolling back CI to known version for now * Fix availability in tests * Commenting out for now to see if it builds * Fix up the Swift framework test * Fix availability --- Source/Details/_ASDisplayViewAccessiblity.mm | 2 +- Source/Private/_ASPendingState.mm | 2 +- Tests/ASDisplayNodeTests.mm | 20 ++++++------- build.sh | 4 +-- .../Sample.xcodeproj/project.pbxproj | 29 ++++--------------- .../Framework/Sample/AppDelegate.swift | 6 ++-- .../Framework/Sample/ViewController.swift | 8 ++--- 7 files changed, 26 insertions(+), 45 deletions(-) diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index abac3f7cc8..419901131e 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -84,7 +84,7 @@ static void SortAccessibilityElements(NSMutableArray *elements) accessibilityElement.accessibilityHint = node.accessibilityHint; accessibilityElement.accessibilityValue = node.accessibilityValue; accessibilityElement.accessibilityTraits = node.accessibilityTraits; - if (AS_AT_LEAST_IOS11) { + if (@available(iOS 11, *)) { [accessibilityElement setValue:node.accessibilityAttributedLabel forKey:@"accessibilityAttributedLabel"]; [accessibilityElement setValue:node.accessibilityAttributedHint forKey:@"accessibilityAttributedHint"]; [accessibilityElement setValue:node.accessibilityAttributedValue forKey:@"accessibilityAttributedValue"]; diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index cabeab0386..6ac8063345 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -1215,7 +1215,7 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; pendingState.accessibilityLabel = view.accessibilityLabel; pendingState.accessibilityHint = view.accessibilityHint; pendingState.accessibilityValue = view.accessibilityValue; - if (AS_AT_LEAST_IOS11) { + if (@available(iOS 11, *)) { pendingState.accessibilityAttributedLabel = [view valueForKey: @"accessibilityAttributedLabel"]; pendingState.accessibilityAttributedHint = [view valueForKey: @"accessibilityAttributedHint"]; pendingState.accessibilityAttributedValue = [view valueForKey: @"accessibilityAttributedValue"]; diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index f7eb1d6bc1..b3fc3e15cd 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -353,11 +353,11 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqual((id)nil, node.accessibilityLabel, @"default accessibilityLabel is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityHint, @"default accessibilityHint is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityValue, @"default accessibilityValue is broken %@", hasLoadedView); - if (AS_AT_LEAST_IOS11) { - XCTAssertEqual((id)nil, node.accessibilityAttributedLabel, @"default accessibilityAttributedLabel is broken %@", hasLoadedView); - XCTAssertEqual((id)nil, node.accessibilityAttributedHint, @"default accessibilityAttributedHint is broken %@", hasLoadedView); - XCTAssertEqual((id)nil, node.accessibilityAttributedValue, @"default accessibilityAttributedValue is broken %@", hasLoadedView); - } +// if (AS_AT_LEAST_IOS11) { +// XCTAssertEqual((id)nil, node.accessibilityAttributedLabel, @"default accessibilityAttributedLabel is broken %@", hasLoadedView); +// XCTAssertEqual((id)nil, node.accessibilityAttributedHint, @"default accessibilityAttributedHint is broken %@", hasLoadedView); +// XCTAssertEqual((id)nil, node.accessibilityAttributedValue, @"default accessibilityAttributedValue is broken %@", hasLoadedView); +// } XCTAssertEqual(UIAccessibilityTraitNone, node.accessibilityTraits, @"default accessibilityTraits is broken %@", hasLoadedView); XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.accessibilityFrame), @"default accessibilityFrame is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityLanguage, @"default accessibilityLanguage is broken %@", hasLoadedView); @@ -458,11 +458,11 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqualObjects(@"1 of 2", node.accessibilityValue, @"accessibilityValue broken %@", hasLoadedView); // setting the accessibilityLabel, accessibilityHint and accessibilityValue is supposed to be bridged to the attributed versions - if (AS_AT_LEAST_IOS11) { - XCTAssertEqualObjects(@"Ship love", node.accessibilityAttributedLabel.string, @"accessibilityAttributedLabel is broken %@", hasLoadedView); - XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityAttributedHint.string, @"accessibilityAttributedHint is broken %@", hasLoadedView); - XCTAssertEqualObjects(@"1 of 2", node.accessibilityAttributedValue.string, @"accessibilityAttributedValue is broken %@", hasLoadedView); - } +// if (AS_AT_LEAST_IOS11) { +// XCTAssertEqualObjects(@"Ship love", node.accessibilityAttributedLabel.string, @"accessibilityAttributedLabel is broken %@", hasLoadedView); +// XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityAttributedHint.string, @"accessibilityAttributedHint is broken %@", hasLoadedView); +// XCTAssertEqualObjects(@"1 of 2", node.accessibilityAttributedValue.string, @"accessibilityAttributedValue is broken %@", hasLoadedView); +// } XCTAssertEqual(UIAccessibilityTraitSelected | UIAccessibilityTraitButton, node.accessibilityTraits, @"accessibilityTraits broken %@", hasLoadedView); XCTAssertTrue(CGRectEqualToRect(CGRectMake(1, 2, 3, 4), node.accessibilityFrame), @"accessibilityFrame broken %@", hasLoadedView); XCTAssertEqualObjects(@"mas", node.accessibilityLanguage, @"accessibilityLanguage broken %@", hasLoadedView); diff --git a/build.sh b/build.sh index 1684c2497d..bffaad4cc4 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/bin/bash -PLATFORM="platform=iOS Simulator,name=iPhone 7" -SDK="iphonesimulator" +PLATFORM="platform=iOS Simulator,OS=10.2,name=iPhone 7" +SDK="iphonesimulator11.0" DERIVED_DATA_PATH="~/ASDKDerivedData" diff --git a/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj b/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj index f6094669cc..b4eeb8cc2c 100644 --- a/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj +++ b/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7319D22E19004363C2 /* AppDelegate.swift */; }; 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7519D22E19004363C2 /* ViewController.swift */; }; 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */; }; - 34566CAA1BC1204100715E6B /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */; }; - 34566CAB1BC1204100715E6B /* AsyncDisplayKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053D919EE266A00E385DE /* Default-667h@2x.png */; }; 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */; }; /* End PBXBuildFile section */ @@ -38,13 +36,6 @@ remoteGlobalIDString = 057D02BF1AC0A66700C7AC3C; remoteInfo = AsyncDisplayKitTestHost; }; - 34566CA81BC1202A00715E6B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = B35061DA1B010EDF0018CF92; - remoteInfo = "AsyncDisplayKit-iOS"; - }; 34566CAC1BC1204100715E6B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; @@ -61,7 +52,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 34566CAB1BC1204100715E6B /* AsyncDisplayKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -86,7 +76,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 34566CAA1BC1204100715E6B /* AsyncDisplayKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,7 +139,6 @@ 34566CA31BC1202A00715E6B /* libAsyncDisplayKit.a */, 34566CA51BC1202A00715E6B /* AsyncDisplayKitTests.xctest */, 34566CA71BC1202A00715E6B /* AsyncDisplayKitTestHost.app */, - 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */, ); name = Products; sourceTree = ""; @@ -189,7 +177,7 @@ TargetAttributes = { 050E7C6D19D22E19004363C2 = { CreatedOnToolsVersion = 6.0.1; - LastSwiftMigration = 0800; + LastSwiftMigration = 0830; }; }; }; @@ -220,8 +208,8 @@ /* Begin PBXReferenceProxy section */ 34566CA31BC1202A00715E6B /* libAsyncDisplayKit.a */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libAsyncDisplayKit.a; + fileType = wrapper.framework; + path = AsyncDisplayKit.framework; remoteRef = 34566CA21BC1202A00715E6B /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -239,13 +227,6 @@ remoteRef = 34566CA61BC1202A00715E6B /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = AsyncDisplayKit.framework; - remoteRef = 34566CA81BC1202A00715E6B /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -369,7 +350,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -382,7 +363,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/smoke-tests/Framework/Sample/AppDelegate.swift b/smoke-tests/Framework/Sample/AppDelegate.swift index 5c30133002..843523a436 100644 --- a/smoke-tests/Framework/Sample/AppDelegate.swift +++ b/smoke-tests/Framework/Sample/AppDelegate.swift @@ -22,9 +22,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - let window = UIWindow(frame: UIScreen.mainScreen().bounds) - window.backgroundColor = UIColor.whiteColor() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = UIColor.white window.rootViewController = ViewController(nibName: nil, bundle: nil) window.makeKeyAndVisible() self.window = window diff --git a/smoke-tests/Framework/Sample/ViewController.swift b/smoke-tests/Framework/Sample/ViewController.swift index 24fa79de75..544850d42d 100644 --- a/smoke-tests/Framework/Sample/ViewController.swift +++ b/smoke-tests/Framework/Sample/ViewController.swift @@ -25,7 +25,7 @@ class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { // MARK: UIViewController. - override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { self.tableNode = ASTableNode() super.init(nibName: nil, bundle: nil) @@ -50,7 +50,7 @@ class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { // MARK: ASTableView data source and delegate. - func tableNode(tableNode: ASTableNode, nodeForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNode { + func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { let patter = NSString(format: "[%ld.%ld] says hello!", indexPath.section, indexPath.row) let node = ASTextCellNode() node.text = patter as String @@ -58,11 +58,11 @@ class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { return node } - func numberOfSectionsInTableNode(tableNode: ASTableNode) -> Int { + func numberOfSections(in tableNode: ASTableNode) -> Int { return 1 } - func tableNode(tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { + func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { return 20 } From 9e178dc0a6af492890c80a2b0716ca4cef4b6cdc Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 27 Sep 2017 11:33:42 +0100 Subject: [PATCH 22/86] [_ASPendingState] Make sure accessibility strings are not nil before allocating attributed strings for them #trivial (#581) * Make sure accessibility strings are not nil before allocating attributed strings for them - Fix crashes caused by https://github.com/TextureGroup/Texture/pull/554 * Update tests --- Source/Private/ASDisplayNode+UIViewBridge.mm | 6 +-- Source/Private/_ASPendingState.mm | 39 ++++++++++---------- Tests/ASDisplayNodeTests.mm | 16 ++++++-- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index a28bad10a0..25c0efdf6c 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -943,7 +943,7 @@ if (shouldApply) { [_view setValue:(viewAndPendingViewStateExpr) forKey: viewAnd _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityLabel, accessibilityLabel, accessibilityLabel); if (AS_AT_LEAST_IOS11) { - NSAttributedString *accessibilityAttributedLabel = [[NSAttributedString alloc] initWithString:accessibilityLabel]; + NSAttributedString *accessibilityAttributedLabel = accessibilityLabel ? [[NSAttributedString alloc] initWithString:accessibilityLabel] : nil; _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, @"accessibilityAttributedLabel", accessibilityAttributedLabel); } } @@ -972,7 +972,7 @@ if (shouldApply) { [_view setValue:(viewAndPendingViewStateExpr) forKey: viewAnd _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityHint, accessibilityHint, accessibilityHint); if (AS_AT_LEAST_IOS11) { - NSAttributedString *accessibilityAttributedHint = [[NSAttributedString alloc] initWithString:accessibilityHint]; + NSAttributedString *accessibilityAttributedHint = accessibilityHint ? [[NSAttributedString alloc] initWithString:accessibilityHint] : nil; _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, @"accessibilityAttributedHint", accessibilityAttributedHint); } } @@ -1001,7 +1001,7 @@ if (shouldApply) { [_view setValue:(viewAndPendingViewStateExpr) forKey: viewAnd _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityValue, accessibilityValue, accessibilityValue); if (AS_AT_LEAST_IOS11) { - NSAttributedString *accessibilityAttributedValue = [[NSAttributedString alloc] initWithString:accessibilityValue]; + NSAttributedString *accessibilityAttributedValue = accessibilityValue ? [[NSAttributedString alloc] initWithString:accessibilityValue] : nil; _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, @"accessibilityAttributedValue", accessibilityAttributedValue); } } diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 6ac8063345..daeb4d6e6d 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -20,8 +20,9 @@ #import #import #import -#import +#import #import +#import #define __shouldSetNeedsDisplay(layer) (flags.needsDisplay \ || (flags.setOpaque && opaque != (layer).opaque)\ @@ -595,11 +596,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel { - if (![accessibilityLabel isEqualToString:newAccessibilityLabel]) { + if (! ASObjectIsEqual(accessibilityLabel, newAccessibilityLabel)) { _flags.setAccessibilityLabel = YES; _flags.setAccessibilityAttributedLabel = YES; - accessibilityLabel = [newAccessibilityLabel copy]; - accessibilityAttributedLabel = [[NSAttributedString alloc] initWithString:newAccessibilityLabel]; + accessibilityLabel = newAccessibilityLabel ? [newAccessibilityLabel copy] : nil; + accessibilityAttributedLabel = newAccessibilityLabel ? [[NSAttributedString alloc] initWithString:newAccessibilityLabel] : nil; } } @@ -610,11 +611,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityAttributedLabel:(NSAttributedString *)newAccessibilityAttributedLabel { - if (![accessibilityAttributedLabel isEqualToAttributedString: newAccessibilityAttributedLabel]) { + if (! ASObjectIsEqual(accessibilityAttributedLabel, newAccessibilityAttributedLabel)) { _flags.setAccessibilityAttributedLabel = YES; _flags.setAccessibilityLabel = YES; - accessibilityAttributedLabel = [newAccessibilityAttributedLabel copy]; - accessibilityLabel = [newAccessibilityAttributedLabel.string copy]; + accessibilityAttributedLabel = newAccessibilityAttributedLabel ? [newAccessibilityAttributedLabel copy] : nil; + accessibilityLabel = newAccessibilityAttributedLabel ? [newAccessibilityAttributedLabel.string copy] : nil; } } @@ -625,11 +626,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityHint:(NSString *)newAccessibilityHint { - if (![accessibilityHint isEqualToString:newAccessibilityHint]) { + if (! ASObjectIsEqual(accessibilityHint, newAccessibilityHint)) { _flags.setAccessibilityHint = YES; _flags.setAccessibilityAttributedHint = YES; - accessibilityHint = [newAccessibilityHint copy]; - accessibilityAttributedHint = [[NSAttributedString alloc] initWithString:newAccessibilityHint]; + accessibilityHint = newAccessibilityHint ? [newAccessibilityHint copy] : nil; + accessibilityAttributedHint = newAccessibilityHint ? [[NSAttributedString alloc] initWithString:newAccessibilityHint] : nil; } } @@ -640,11 +641,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityAttributedHint:(NSAttributedString *)newAccessibilityAttributedHint { - if (![accessibilityAttributedHint isEqual:newAccessibilityAttributedHint]) { + if (! ASObjectIsEqual(accessibilityAttributedHint, newAccessibilityAttributedHint)) { _flags.setAccessibilityAttributedHint = YES; _flags.setAccessibilityHint = YES; - accessibilityAttributedHint = [newAccessibilityAttributedHint copy]; - accessibilityHint = [newAccessibilityAttributedHint.string copy]; + accessibilityAttributedHint = newAccessibilityAttributedHint ? [newAccessibilityAttributedHint copy] : nil; + accessibilityHint = newAccessibilityAttributedHint ? [newAccessibilityAttributedHint.string copy] : nil; } } @@ -655,11 +656,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityValue:(NSString *)newAccessibilityValue { - if (![accessibilityValue isEqualToString:newAccessibilityValue]) { + if (! ASObjectIsEqual(accessibilityValue, newAccessibilityValue)) { _flags.setAccessibilityValue = YES; _flags.setAccessibilityAttributedValue = YES; - accessibilityValue = [newAccessibilityValue copy]; - accessibilityAttributedValue = [[NSAttributedString alloc] initWithString:newAccessibilityValue]; + accessibilityValue = newAccessibilityValue ? [newAccessibilityValue copy] : nil; + accessibilityAttributedValue = newAccessibilityValue ? [[NSAttributedString alloc] initWithString:newAccessibilityValue] : nil; } } @@ -670,11 +671,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityAttributedValue:(NSAttributedString *)newAccessibilityAttributedValue { - if (![accessibilityAttributedValue isEqualToAttributedString:newAccessibilityAttributedValue]) { + if (! ASObjectIsEqual(accessibilityAttributedValue, newAccessibilityAttributedValue)) { _flags.setAccessibilityAttributedValue = YES; _flags.setAccessibilityValue = YES; - accessibilityAttributedValue = [newAccessibilityAttributedValue copy]; - accessibilityValue = [newAccessibilityAttributedValue.string copy]; + accessibilityAttributedValue = newAccessibilityAttributedValue? [newAccessibilityAttributedValue copy] : nil; + accessibilityValue = newAccessibilityAttributedValue ? [newAccessibilityAttributedValue.string copy] : nil; } } diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index b3fc3e15cd..75346ff857 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -518,9 +518,19 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.debugName = @"quack like a duck"; node.isAccessibilityElement = YES; - node.accessibilityLabel = @"Ship love"; - node.accessibilityHint = @"Awesome things will happen"; - node.accessibilityValue = @"1 of 2"; + + for (int i = 0; i < 4; i++) { + if (i % 2 == 0) { + XCTAssertNoThrow(node.accessibilityLabel = nil); + XCTAssertNoThrow(node.accessibilityHint = nil); + XCTAssertNoThrow(node.accessibilityValue = nil); + } else { + node.accessibilityLabel = @"Ship love"; + node.accessibilityHint = @"Awesome things will happen"; + node.accessibilityValue = @"1 of 2"; + } + } + node.accessibilityTraits = UIAccessibilityTraitSelected | UIAccessibilityTraitButton; node.accessibilityFrame = CGRectMake(1, 2, 3, 4); node.accessibilityLanguage = @"mas"; From 6c008974d69a1891687eb1fae55221a48c142607 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Wed, 27 Sep 2017 03:34:01 -0700 Subject: [PATCH 23/86] Use node lock instead of separate one to avoid deadlocks. (#582) * Use node lock instead of separate one to avoid deadlocks. * Add CHANGELOG entry --- CHANGELOG.md | 1 + Source/ASImageNode+AnimatedImage.mm | 19 ++++++++++--------- .../ASImageNode+AnimatedImagePrivate.h | 1 - 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd11f3285..bd484096ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASNetworkImageNode] Fix deadlock in GIF handling. [#582](https://github.com/TextureGroup/Texture/pull/582) [Garrett Moon](https://github.com/garrettmoon) - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 0d12351dd6..2ceed182f5 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -20,6 +20,7 @@ #import #import #import +#import #import #import #import @@ -43,7 +44,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setAnimatedImage:(id )animatedImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setAnimatedImage:animatedImage]; } @@ -85,13 +86,13 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (id )animatedImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); return _animatedImage; } - (void)setAnimatedImagePaused:(BOOL)animatedImagePaused { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); _animatedImagePaused = animatedImagePaused; @@ -100,13 +101,13 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (BOOL)animatedImagePaused { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); return _animatedImagePaused; } - (void)setCoverImageCompleted:(UIImage *)coverImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setCoverImageCompleted:coverImage]; } @@ -123,7 +124,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setCoverImage:(UIImage *)coverImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setCoverImage:coverImage]; } @@ -161,7 +162,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setShouldAnimate:(BOOL)shouldAnimate { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setShouldAnimate:shouldAnimate]; } @@ -194,7 +195,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_startAnimating]; } @@ -233,7 +234,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_stopAnimating]; } diff --git a/Source/Private/ASImageNode+AnimatedImagePrivate.h b/Source/Private/ASImageNode+AnimatedImagePrivate.h index 6c08ce617e..6e57d51dd1 100644 --- a/Source/Private/ASImageNode+AnimatedImagePrivate.h +++ b/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -21,7 +21,6 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode; @interface ASImageNode () { - ASDN::RecursiveMutex _animatedImageLock; ASDN::Mutex _displayLinkLock; id _animatedImage; BOOL _animatedImagePaused; From a103bab00af7a60aaa1b8fb56c1b88575af8a0b2 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 27 Sep 2017 13:42:02 -0700 Subject: [PATCH 24/86] Clear ivar after scheduling for main thread deallocation #trivial (#590) * Clear ivar after scheduling for main thread deallocation After scheduling the ivar for main thread deallocation we have clear out the ivar, otherwise we can run into a race condition where the main queue is drained earlier than this node is deallocated and the ivar is still deallocated on a background thread * First clear and than schedule --- Source/ASDisplayNode.mm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 03c1a2f049..cde2f0030b 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -428,8 +428,18 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) for (Ivar ivar : ivars) { id value = object_getIvar(self, ivar); + if (value == nil) { + continue; + } + if (ASClassRequiresMainThreadDeallocation(object_getClass(value))) { as_log_debug(ASMainThreadDeallocationLog(), "%@: Trampolining ivar '%s' value %@ for main deallocation.", self, ivar_getName(ivar), value); + + // Before scheduling the ivar for main thread deallocation we have clear out the ivar, otherwise we can run + // into a race condition where the main queue is drained earlier than this node is deallocated and the ivar + // is still deallocated on a background thread + object_setIvar(self, ivar, nil); + ASPerformMainThreadDeallocation(value); } else { as_log_debug(ASMainThreadDeallocationLog(), "%@: Not trampolining ivar '%s' value %@.", self, ivar_getName(ivar), value); From 5186a4317e2f4f011bdb15543e22c2d17adefef4 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 27 Sep 2017 20:12:18 -0700 Subject: [PATCH 25/86] Use Nil for class instead of nil (#589) --- Source/Private/ASInternalHelpers.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Private/ASInternalHelpers.m b/Source/Private/ASInternalHelpers.m index 088ddccea7..7727967c4f 100644 --- a/Source/Private/ASInternalHelpers.m +++ b/Source/Private/ASInternalHelpers.m @@ -113,14 +113,14 @@ Class _Nullable ASGetClassFromType(const char * _Nullable type) { // Class types all start with @" if (type == NULL || strncmp(type, "@\"", 2) != 0) { - return nil; + return Nil; } // Ensure length >= 3 size_t typeLength = strlen(type); if (typeLength < 3) { ASDisplayNodeCFailAssert(@"Got invalid type-encoding: %s", type); - return nil; + return Nil; } // Copy type[2..(end-1)]. So @"UIImage" -> UIImage From 5c6cd7c8d9c24d3c0a9053d0651e383f48c417ac Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sat, 30 Sep 2017 07:20:14 -0700 Subject: [PATCH 26/86] Move clearing out of ASTextKitComponents property delegates into ASTextKitComponents dealloc (#591) --- Source/ASEditableTextNode.mm | 7 ------- Source/TextKit/ASTextKitComponents.mm | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index d68182ed16..98351f0761 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -177,13 +177,6 @@ return self; } -- (void)dealloc -{ - _textKitComponents.textView.delegate = nil; - _textKitComponents.layoutManager.delegate = nil; - _placeholderTextKitComponents.layoutManager.delegate = nil; -} - #pragma mark - ASDisplayNode Overrides - (void)didLoad { diff --git a/Source/TextKit/ASTextKitComponents.mm b/Source/TextKit/ASTextKitComponents.mm index 2854ad6997..bbd37c229e 100644 --- a/Source/TextKit/ASTextKitComponents.mm +++ b/Source/TextKit/ASTextKitComponents.mm @@ -1,5 +1,5 @@ // -// ASTextKitComponents.m +// ASTextKitComponents.mm // Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. @@ -30,6 +30,8 @@ @implementation ASTextKitComponents +#pragma mark - Class + + (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString textContainerSize:(CGSize)textContainerSize { @@ -58,6 +60,17 @@ return components; } +#pragma mark - Lifecycle + +- (void)dealloc +{ + // Nil out all delegate to prevent crash + _textView.delegate = nil; + _layoutManager.delegate = nil; +} + +#pragma mark - Sizing + - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth { ASTextKitComponents *components = self; From 54f241e1dc35943eeb6b5e3e8aea262734df9aaf Mon Sep 17 00:00:00 2001 From: romankl Date: Mon, 2 Oct 2017 12:22:14 +0200 Subject: [PATCH 27/86] update faq toc links to match the generated html id (#597) Right now the faq- table of contents has links to the different headlines, but some of the generated id s don't match the referencing a tag. --- docs/_docs/faq.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/_docs/faq.md b/docs/_docs/faq.md index fa7a7826f1..18672cd9f3 100755 --- a/docs/_docs/faq.md +++ b/docs/_docs/faq.md @@ -1,5 +1,5 @@ --- -title: FAQ +title: FAQ layout: docs permalink: /docs/faq.html prevPage: subclassing.html @@ -9,7 +9,7 @@ nextPage: containers-asviewcontroller.html ### Common Developer Mistakes @@ -25,8 +25,8 @@ nextPage: containers-asviewcontroller.html ### Common Questions @@ -36,35 +36,35 @@ nextPage: containers-asviewcontroller.html
Node `-init` methods are often called off the main thread, therefore it is imperative that no UIKit objects are accessed. Examples of common errors include accessing the node's view or creating a gesture recognizer. Instead, these operations are ideal to perform in `-didLoad`. -Interacting with UIKit in `-init` can cause crashes and performance problems. +Interacting with UIKit in `-init` can cause crashes and performance problems.
### Make sure you access your data source outside the node block
-The `indexPath` parameter is only valid _outside_ the node block returned in `nodeBlockForItemAtIndexPath:` or `nodeBlockForRowAtIndexPath:`. Because these blocks are executed on a background thread, the `indexPath` may be invalid by execution time, due to additional changes in the data source. +The `indexPath` parameter is only valid _outside_ the node block returned in `nodeBlockForItemAtIndexPath:` or `nodeBlockForRowAtIndexPath:`. Because these blocks are executed on a background thread, the `indexPath` may be invalid by execution time, due to additional changes in the data source. -See an example of how to correctly code a node block in the ASTableNode page. Just as with UIKit, it will cause an exception if Nil is returned from the block for any `ASCellNode`. +See an example of how to correctly code a node block in the ASTableNode page. Just as with UIKit, it will cause an exception if Nil is returned from the block for any `ASCellNode`.
-### Take steps to avoid a retain cycle in viewBlocks +### Take steps to avoid a retain cycle in viewBlocks
-When using `initWithViewBlock:` it is important to prevent a retain cycle by capturing a strong reference to self. The two ways that a cycle can be created are by using any instance variable inside the block or directly referencing self without using a weak pointer. +When using `initWithViewBlock:` it is important to prevent a retain cycle by capturing a strong reference to self. The two ways that a cycle can be created are by using any instance variable inside the block or directly referencing self without using a weak pointer. -You can use properties instead of instance variables as long as they are accessed on a weak pointer to self. +You can use properties instead of instance variables as long as they are accessed on a weak pointer to self. -Because viewBlocks are always executed on the main thread, it is safe to preform UIKit operations (including gesture recognizer creation and addition). +Because viewBlocks are always executed on the main thread, it is safe to preform UIKit operations (including gesture recognizer creation and addition). -Although the block is destroyed after the view is created, in the event that the block is never run and the view is never created, then a cycle can persist preventing memory from being released. +Although the block is destroyed after the view is created, in the event that the block is never run and the view is never created, then a cycle can persist preventing memory from being released.
### ASCellNode Reusability
-Texture does not use cell reuse, for a number of specific reasons, one side effect of this is that it eliminates the large class of bugs associated with cell reuse. +Texture does not use cell reuse, for a number of specific reasons, one side effect of this is that it eliminates the large class of bugs associated with cell reuse.
### LayoutSpecs Are Regenerated
-A node's layoutSpec gets regenerated every time its `layoutThatFits:` method is called. +A node's layoutSpec gets regenerated every time its `layoutThatFits:` method is called.
### Layout API Sizing @@ -74,11 +74,11 @@ If you're confused by `ASRelativeDimension`, `ASRelativeSize`, `ASRelativeSizeRa ### CALayer's .cornerRadius Property Kills Performance
-CALayer's' .cornerRadius property is a disastrously expensive property that should only be used when there is no alternative. It is one of the least efficient, most render-intensive properties on CALayer (alongside shadowPath, masking, borders, etc). These properties trigger offscreen rendering to perform the clipping operation on every frame — 60FPS during scrolling! — even if the content in that area isn't changing. +CALayer's' .cornerRadius property is a disastrously expensive property that should only be used when there is no alternative. It is one of the least efficient, most render-intensive properties on CALayer (alongside shadowPath, masking, borders, etc). These properties trigger offscreen rendering to perform the clipping operation on every frame — 60FPS during scrolling! — even if the content in that area isn't changing. Using `.cornerRadius` will visually degraded performance on iPhone 4, 4S, and 5 / 5C (along with comparable iPads / iPods) and reduce head room and make frame drops more likely on 5S and newer devices. -For a longer discussion and easy alternative corner rounding solutions, please read our comprehensive corner rounding guide. +For a longer discussion and easy alternative corner rounding solutions, please read our comprehensive corner rounding guide.
### Texture does not support UIKit Auto Layout or InterfaceBuilder @@ -118,8 +118,8 @@ Good application design should not rely on this behavior, because a strong refer ### UICollectionViewCell Compatibility -Texture supports using UICollectionViewCells alongside native ASCellNodes. +Texture supports using UICollectionViewCells alongside native ASCellNodes. -Note that these UIKit cells will **not** have the performance benefits of `ASCellNodes` (like preloading, async layout, and async drawing), even when mixed within the same `ASCollectionNode`. +Note that these UIKit cells will **not** have the performance benefits of `ASCellNodes` (like preloading, async layout, and async drawing), even when mixed within the same `ASCollectionNode`. However, this interoperability allows developers the flexibility to test out the framework without needing to convert all of their cells at once. Read more here. From 7a07d9eb88273b9e7621b6e615bdc2f8f1d2cd59 Mon Sep 17 00:00:00 2001 From: appleguy Date: Tue, 3 Oct 2017 19:32:03 -0700 Subject: [PATCH 28/86] [PINCache] Set a default .byteLimit to reduce disk usage & startup time. (#595) * [PINCache] Set a default .byteLimit to reduce disk usage & startup time. This default is fairly low - only 20MB - but for most apps with images in the size range of 10-50KB, this is still 400-1000 images. Once some optimizations land to PINCache, we'll match the PINCache default of 50MB to ensure the default better serves users with larger objects in the cache. Apps should preferably set their own byteLimit to an optimal value. @garrettmoon - one interesting question for us is the best place to set .byteLimit as an app. Digging into the ASPINRemoteImageDownloader and doing this type cast is a bit complicated, so a passthrough API to get the PIN* objects directly might be worthwhile. * [PINCache] Declare necessary APIs to avoid a direct dependency. --- CHANGELOG.md | 1 + Source/Details/ASPINRemoteImageDownloader.m | 25 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd484096ed..835422a942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [PINCache] Set a default .byteLimit to reduce disk usage and startup time. [#595](https://github.com/TextureGroup/Texture/pull/595) [Scott Goodson](https://github.com/appleguy) - [ASNetworkImageNode] Fix deadlock in GIF handling. [#582](https://github.com/TextureGroup/Texture/pull/582) [Garrett Moon](https://github.com/garrettmoon) - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 0e6c6adff4..601d009450 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -76,6 +76,15 @@ @end #endif +// Declare two key methods on PINCache objects, avoiding a direct dependency on PINCache.h +@protocol ASPINCache +- (id)diskCache; +@end + +@protocol ASPINDiskCache +@property (assign) NSUInteger byteLimit; +@end + @interface ASPINRemoteImageManager : PINRemoteImageManager @end @@ -84,7 +93,21 @@ //Share image cache with sharedImageManager image cache. - (id )defaultImageCache { - return [[PINRemoteImageManager sharedImageManager] cache]; + static dispatch_once_t onceToken; + static id cache = nil; + dispatch_once(&onceToken, ^{ + cache = [[PINRemoteImageManager sharedImageManager] cache]; + if ([cache respondsToSelector:@selector(diskCache)]) { + id diskCache = [(id )cache diskCache]; + if ([diskCache respondsToSelector:@selector(setByteLimit:)]) { + // Set a default byteLimit. PINCache recently implemented a 50MB default (PR #201). + // Ensure that older versions of PINCache also have a byteLimit applied. + // NOTE: Using 20MB limit while large cache initialization is being optimized (Issue #144). + ((id )diskCache).byteLimit = 20 * 1024 * 1024; + } + } + }); + return cache; } @end From dd90978fb60cb8cc6cc51cdba115a84ffd720b5a Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 4 Oct 2017 06:52:49 -0700 Subject: [PATCH 29/86] ASTextKitComponents needs to be deallocated on main (#598) --- Source/Private/ASInternalHelpers.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Private/ASInternalHelpers.m b/Source/Private/ASInternalHelpers.m index 7727967c4f..e0d757101b 100644 --- a/Source/Private/ASInternalHelpers.m +++ b/Source/Private/ASInternalHelpers.m @@ -91,6 +91,7 @@ void ASPerformBackgroundDeallocation(id object) BOOL ASClassRequiresMainThreadDeallocation(Class c) { + // Specific classes if (c == [UIImage class] || c == [UIColor class]) { return NO; } @@ -101,10 +102,16 @@ BOOL ASClassRequiresMainThreadDeallocation(Class c) return YES; } + // Apple classes with prefix const char *name = class_getName(c); if (strncmp(name, "UI", 2) == 0 || strncmp(name, "AV", 2) == 0 || strncmp(name, "CA", 2) == 0) { return YES; } + + // Specific Texture classes + if (strncmp(name, "ASTextKitComponents", 19) == 0) { + return YES; + } return NO; } From 4379b31ac2ffb690845d183742875f312bcb5fdc Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 5 Oct 2017 09:06:50 -0700 Subject: [PATCH 30/86] Add assertion in dealloc that it is on main in ASTextKitComponents (#603) --- Source/TextKit/ASTextKitComponents.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/TextKit/ASTextKitComponents.mm b/Source/TextKit/ASTextKitComponents.mm index bbd37c229e..b733a4f4ee 100644 --- a/Source/TextKit/ASTextKitComponents.mm +++ b/Source/TextKit/ASTextKitComponents.mm @@ -16,6 +16,7 @@ // #import +#import #import @@ -64,6 +65,8 @@ - (void)dealloc { + ASDisplayNodeAssertMainThread(); + // Nil out all delegate to prevent crash _textView.delegate = nil; _layoutManager.delegate = nil; From 4a203db9fc15940b061e6ad49def1d519bcc2ac4 Mon Sep 17 00:00:00 2001 From: Flo Date: Fri, 6 Oct 2017 16:04:36 +0200 Subject: [PATCH 31/86] [ASVideoNode] Time observer fix (#604) * [ASVideoNode] Move time observer handling to player observers methods. * Update CHANGELOG.md. --- CHANGELOG.md | 1 + Source/ASVideoNode.mm | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 835422a942..33d7f29361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASVideoNode] Fix unreleased time observer. [Flo Vouin](https://github.com/flovouin) - [PINCache] Set a default .byteLimit to reduce disk usage and startup time. [#595](https://github.com/TextureGroup/Texture/pull/595) [Scott Goodson](https://github.com/appleguy) - [ASNetworkImageNode] Fix deadlock in GIF handling. [#582](https://github.com/TextureGroup/Texture/pull/582) [Garrett Moon](https://github.com/garrettmoon) - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) diff --git a/Source/ASVideoNode.mm b/Source/ASVideoNode.mm index 84486a831f..9274d6fa5a 100644 --- a/Source/ASVideoNode.mm +++ b/Source/ASVideoNode.mm @@ -181,12 +181,6 @@ static NSString * const kRate = @"rate"; if (self.image == nil && self.URL == nil) { [self generatePlaceholderImage]; } - - __weak __typeof(self) weakSelf = self; - _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); - _timeObserver = [_player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ - [weakSelf periodicTimeObserver:time]; - }]; } - (void)addPlayerItemObservers:(AVPlayerItem *)playerItem @@ -231,10 +225,21 @@ static NSString * const kRate = @"rate"; } [player addObserver:self forKeyPath:kRate options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + + __weak __typeof(self) weakSelf = self; + _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); + _timeObserver = [player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ + [weakSelf periodicTimeObserver:time]; + }]; } - (void) removePlayerObservers:(AVPlayer *)player { + if (_timeObserver != nil) { + [player removeTimeObserver:_timeObserver]; + _timeObserver = nil; + } + @try { [player removeObserver:self forKeyPath:kRate context:ASVideoNodeContext]; } @@ -836,8 +841,6 @@ static NSString * const kRate = @"rate"; - (void)dealloc { - [_player removeTimeObserver:_timeObserver]; - _timeObserver = nil; [self removePlayerItemObservers:_currentPlayerItem]; [self removePlayerObservers:_player]; From 550da242b7852563cbcbf409cb9e3373e626304a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olcay=20Erta=C5=9F?= Date: Mon, 9 Oct 2017 18:21:20 +0300 Subject: [PATCH 32/86] Update layout2-layoutspec-types.md (#608) Objective-C code sample was still showing old syntax: spacer.flexGrow = true; I have updated to new one: spacer.style.flexGrow = true; --- docs/_docs/layout2-layoutspec-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/layout2-layoutspec-types.md b/docs/_docs/layout2-layoutspec-types.md index e816ab3ca7..42fad5a9b0 100755 --- a/docs/_docs/layout2-layoutspec-types.md +++ b/docs/_docs/layout2-layoutspec-types.md @@ -452,7 +452,7 @@ Another use of `ASLayoutSpec` is to be used as a spacer in a `ASStackLayoutSpec` ... // ASLayoutSpec as spacer ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; - spacer.flexGrow = true; + spacer.style.flexGrow = true; stack.children = @[imageNode, spacer, textNode]; ... From c6e3dd7a5d8bb0dc4dd26b96f96bf96946ade806 Mon Sep 17 00:00:00 2001 From: appleguy Date: Mon, 9 Oct 2017 09:19:46 -0700 Subject: [PATCH 33/86] [ASCollectionView] Fix index space translation of Flow Layout Delegate methods. (#467) * [ASCollectionView] Fix index space translation of Flow Layout Delegate methods. This includes a few other cleanups, including overflow of signed integer indices. * [ASCollectionView] Improve code sharing of UIKit size method calls; ensure delegate invalidation re-fetches supplementary sizes too. * [ASCollectionView] Final method ordering and doc-comment for new _sizeForUIKitCellWithKind:atIndexPath: method. --- CHANGELOG.md | 1 + Source/ASCollectionNode+Beta.h | 2 + Source/ASCollectionNode.h | 4 + Source/ASCollectionNode.mm | 7 + Source/ASCollectionView.mm | 161 +++++++++++++----- Source/Base/ASAssert.h | 4 +- Source/Details/ASDataController.mm | 2 +- .../Private/ASCollectionView+Undeprecated.h | 5 + 8 files changed, 137 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d7f29361..4aa2cfd606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [ASVideoNode] Fix unreleased time observer. [Flo Vouin](https://github.com/flovouin) - [PINCache] Set a default .byteLimit to reduce disk usage and startup time. [#595](https://github.com/TextureGroup/Texture/pull/595) [Scott Goodson](https://github.com/appleguy) - [ASNetworkImageNode] Fix deadlock in GIF handling. [#582](https://github.com/TextureGroup/Texture/pull/582) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASCollectionNode+Beta.h b/Source/ASCollectionNode+Beta.h index 74625c75da..952227896f 100644 --- a/Source/ASCollectionNode+Beta.h +++ b/Source/ASCollectionNode+Beta.h @@ -67,6 +67,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion ASDISPLAYNODE_DEPRECATED_MSG("Use -performBatchUpdates:completion: instead."); +- (void)invalidateFlowLayoutDelegateMetrics; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 6913f87206..d60fd57ef0 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -876,6 +876,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 3ff691c1ef..2449377fd2 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -805,6 +805,13 @@ } } +- (void)invalidateFlowLayoutDelegateMetrics { + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view invalidateFlowLayoutDelegateMetrics]; + } +} + - (void)insertSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 906577fde2..0c706faa46 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -29,6 +29,7 @@ #import #import #import +#import #import #import #import @@ -63,6 +64,14 @@ return __val; \ } +#define ASIndexPathForSection(section) [NSIndexPath indexPathForItem:0 inSection:section] + +#define ASFlowLayoutDefault(layout, property, default) \ +({ \ + UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ + flowLayout ? flowLayout.property : default; \ +}) + /// What, if any, invalidation should we perform during the next -layoutSubviews. typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) { /// Perform no invalidation. @@ -192,6 +201,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; unsigned int interop:1; unsigned int interopWillDisplayCell:1; unsigned int interopDidEndDisplayingCell:1; + unsigned int interopWillDisplaySupplementaryView:1; + unsigned int interopdidEndDisplayingSupplementaryView:1; } _asyncDelegateFlags; struct { @@ -532,6 +543,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; id interopDelegate = (id)_asyncDelegate; _asyncDelegateFlags.interopWillDisplayCell = [interopDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]; _asyncDelegateFlags.interopDidEndDisplayingCell = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]; + _asyncDelegateFlags.interopWillDisplaySupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:)]; + _asyncDelegateFlags.interopdidEndDisplayingSupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)]; } } @@ -771,6 +784,25 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; self.dataController.usesSynchronousDataLoading = usesSynchronousDataLoading; } +- (void)invalidateFlowLayoutDelegateMetrics { + for (ASCollectionElement *element in self.dataController.pendingMap) { + // This may be either a Supplementary or Item type element. + // For UIKit passthrough cells of either type, re-fetch their sizes from the standard UIKit delegate methods. + ASCellNode *node = element.node; + if (node.shouldUseUIKitCell) { + NSIndexPath *indexPath = [self indexPathForNode:node]; + NSString *kind = [element supplementaryElementKind]; + CGSize previousSize = node.style.preferredSize; + CGSize size = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; + + if (!CGSizeEqualToSize(previousSize, size)) { + node.style.preferredSize = size; + [node invalidateCalculatedLayout]; + } + } + } +} + #pragma mark Internal - (void)_configureCollectionViewLayout:(nonnull UICollectionViewLayout *)layout @@ -781,6 +813,46 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; } } +/** + This method is called only for UIKit Passthrough cells - either regular Items or Supplementary elements. + It checks if the delegate implements the UICollectionViewFlowLayout methods that provide sizes, and if not, + uses the default values set on the flow layout. If a flow layout is not in use, UICollectionView Passthrough + cells must be sized by logic in the Layout object, and Texture does not participate in these paths. +*/ +- (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + CGSize size = CGSizeZero; + UICollectionViewLayout *l = self.collectionViewLayout; + + if (kind == nil) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interop, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); + if ([_asyncDelegate respondsToSelector:sizeForItem]) { + size = [(id)_asyncDelegate collectionView:self layout:l sizeForItemAtIndexPath:indexPath]; + } else { + size = ASFlowLayoutDefault(l, itemSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); + if ([_asyncDelegate respondsToSelector:sizeForHeader]) { + size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForHeaderInSection:indexPath.section]; + } else { + size = ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); + if ([_asyncDelegate respondsToSelector:sizeForFooter]) { + size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForFooterInSection:indexPath.section]; + } else { + size = ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); + } + } + + return size; +} + /** Performing nested batch updates with super (e.g. resizing a cell node & updating collection view during same frame) can cause super to throw data integrity exceptions because it checks the data source counts before @@ -961,15 +1033,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; return [_dataController.visibleMap numberOfItemsInSection:section]; } -#define ASIndexPathForSection(section) [NSIndexPath indexPathForItem:0 inSection:section] -#define ASFlowLayoutDefault(layout, property, default) \ -({ \ - UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ - flowLayout ? flowLayout.property : default; \ -}) - - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout - sizeForItemAtIndexPath:(NSIndexPath *)indexPath + sizeForItemAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; @@ -977,7 +1042,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; } - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -referenceSizeForHeaderInSection:(NSInteger)section + referenceSizeForHeaderInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); ASElementMap *map = _dataController.visibleMap; @@ -987,7 +1052,7 @@ referenceSizeForHeaderInSection:(NSInteger)section } - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -referenceSizeForFooterInSection:(NSInteger)section + referenceSizeForFooterInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); ASElementMap *map = _dataController.visibleMap; @@ -1010,7 +1075,7 @@ referenceSizeForFooterInSection:(NSInteger)section } - (UIEdgeInsets)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l - insetForSectionAtIndex:(NSInteger)section + insetForSectionAtIndex:(NSInteger)section { NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; if (indexPath) { @@ -1020,23 +1085,23 @@ referenceSizeForFooterInSection:(NSInteger)section } - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -minimumInteritemSpacingForSectionAtIndex:(NSInteger)section + minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; if (indexPath) { return [(id)_asyncDelegate collectionView:cv layout:l - minimumInteritemSpacingForSectionAtIndex:indexPath.section]; + minimumInteritemSpacingForSectionAtIndex:indexPath.section]; } return ASFlowLayoutDefault(l, minimumInteritemSpacing, 10.0); // Default is documented as 10.0 } - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -minimumLineSpacingForSectionAtIndex:(NSInteger)section + minimumLineSpacingForSectionAtIndex:(NSInteger)section { NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; if (indexPath) { return [(id)_asyncDelegate collectionView:cv layout:l - minimumLineSpacingForSectionAtIndex:indexPath.section]; + minimumLineSpacingForSectionAtIndex:indexPath.section]; } return ASFlowLayoutDefault(l, minimumLineSpacing, 10.0); // Default is documented as 10.0 } @@ -1046,7 +1111,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if ([_registeredSupplementaryKinds containsObject:kind] == NO) { [self registerSupplementaryNodeOfKind:kind]; } - + UICollectionReusableView *view = nil; ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath]; ASCellNode *node = element.node; @@ -1097,7 +1162,11 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopWillDisplayCell) { - [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + NSIndexPath *modelIndexPath = [self indexPathForNode:node]; + if (modelIndexPath && node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:modelIndexPath]; + } } _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); @@ -1154,7 +1223,11 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopDidEndDisplayingCell) { - [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + NSIndexPath *modelIndexPath = [self indexPathForNode:node]; + if (modelIndexPath && node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:modelIndexPath]; + } } _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); @@ -1195,6 +1268,14 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + if (_asyncDelegateFlags.interopWillDisplaySupplementaryView) { + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + NSIndexPath *modelIndexPath = [self indexPathForNode:node]; + if (modelIndexPath && node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:modelIndexPath]; + } + } + _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); if (view == nil) { return; @@ -1228,6 +1309,14 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)rawView forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + if (_asyncDelegateFlags.interopdidEndDisplayingSupplementaryView) { + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + NSIndexPath *modelIndexPath = [self indexPathForNode:node]; + if (modelIndexPath && node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:modelIndexPath]; + } + } + _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); if (view == nil) { return; @@ -1427,7 +1516,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section } for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { - // Only nodes that respond to the selector are added to _cellsForVisibilityUpdates + // _cellsForVisibilityUpdates only includes cells for ASCellNode subclasses with overrides of the visibility method. [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView]; } if (_asyncDelegateFlags.scrollViewDidScroll) { @@ -1680,6 +1769,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath { + ASDisplayNodeAssertMainThread(); ASCellNodeBlock block = nil; ASCellNode *cell = nil; @@ -1707,14 +1797,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if (block == nil) { if (_asyncDataSourceFlags.interop) { - UICollectionViewLayout *layout = self.collectionViewLayout; - CGSize preferredSize = CGSizeZero; - SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); - if ([_asyncDelegate respondsToSelector:sizeForItem]) { - preferredSize = [(id)_asyncDelegate collectionView:self layout:layout sizeForItemAtIndexPath:indexPath]; - } else { - preferredSize = ASFlowLayoutDefault(layout, itemSize, CGSizeZero); - } + CGSize preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath]; block = ^{ ASCellNode *node = [[ASCellNode alloc] init]; node.shouldUseUIKitCell = YES; @@ -1790,6 +1873,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + ASDisplayNodeAssertMainThread(); ASCellNodeBlock nodeBlock = nil; ASCellNode *node = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement) { @@ -1809,27 +1893,12 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if (node) { nodeBlock = ^{ return node; }; } else { - BOOL useUIKitCell = _asyncDataSourceFlags.interop; + // In this case, the app code returned nil for the node and the nodeBlock. + // If the UIKit method is implemented, then we should use it. Otherwise the CGSizeZero default will cause UIKit to not show it. CGSize preferredSize = CGSizeZero; + BOOL useUIKitCell = _asyncDataSourceFlags.interopViewForSupplementaryElement; if (useUIKitCell) { - UICollectionViewLayout *layout = self.collectionViewLayout; - if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { - SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); - if ([_asyncDelegate respondsToSelector:sizeForHeader]) { - preferredSize = [(id)_asyncDelegate collectionView:self layout:layout - referenceSizeForHeaderInSection:indexPath.section]; - } else { - preferredSize = ASFlowLayoutDefault(layout, headerReferenceSize, CGSizeZero); - } - } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { - SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); - if ([_asyncDelegate respondsToSelector:sizeForFooter]) { - preferredSize = [(id)_asyncDelegate collectionView:self layout:layout - referenceSizeForFooterInSection:indexPath.section]; - } else { - preferredSize = ASFlowLayoutDefault(layout, footerReferenceSize, CGSizeZero); - } - } + preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; } nodeBlock = ^{ ASCellNode *node = [[ASCellNode alloc] init]; diff --git a/Source/Base/ASAssert.h b/Source/Base/ASAssert.h index ddf87d882a..21bdcf2dae 100644 --- a/Source/Base/ASAssert.h +++ b/Source/Base/ASAssert.h @@ -64,8 +64,8 @@ #define ASDisplayNodeConditionalAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__) #define ASDisplayNodeConditionalCAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeCAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__) -#define ASDisplayNodeCAssertPositiveReal(description, num) ASDisplayNodeCAssert(num >= 0 && num <= CGFLOAT_MAX, @"%@ must be a real positive integer.", description) -#define ASDisplayNodeCAssertInfOrPositiveReal(description, num) ASDisplayNodeCAssert(isinf(num) || (num >= 0 && num <= CGFLOAT_MAX), @"%@ must be infinite or a real positive integer.", description) +#define ASDisplayNodeCAssertPositiveReal(description, num) ASDisplayNodeCAssert(num >= 0 && num <= CGFLOAT_MAX, @"%@ must be a real positive integer: %f.", description, (CGFloat)num) +#define ASDisplayNodeCAssertInfOrPositiveReal(description, num) ASDisplayNodeCAssert(isinf(num) || (num >= 0 && num <= CGFLOAT_MAX), @"%@ must be infinite or a real positive integer: %f.", description, (CGFloat)num) #define ASDisplayNodeErrorDomain @"ASDisplayNodeErrorDomain" #define ASDisplayNodeNonFatalErrorCode 1 diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 889599506a..f42d013069 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -791,7 +791,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; element.constrainedSize = newConstrainedSize; // Node may not be allocated yet (e.g node virtualization or same size optimization) - // Call context.nodeIfAllocated here to avoid immature node allocation and layout + // Call context.nodeIfAllocated here to avoid premature node allocation and layout ASCellNode *node = element.nodeIfAllocated; if (node) { [self _layoutNode:node withConstrainedSize:newConstrainedSize]; diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index 591f49efae..bc03fc6aa5 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -296,6 +296,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; +/** + * Invalidates and recalculates the cached sizes stored for pass-through cells used in interop mode. + */ +- (void)invalidateFlowLayoutDelegateMetrics; + - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; @end From 1705ec0140ebf8315c155c67217a4ddb39e3c8b9 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Mon, 9 Oct 2017 10:13:28 -0700 Subject: [PATCH 34/86] Don't set download results if no longer in preload range. (#606) Good catch by @djblake, if you scroll fast enough, you leave images set on the image node because didExitPreloadRange (which would have cleared it) was already called. --- Source/ASNetworkImageNode.mm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index d969322763..2b22ea5261 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -709,7 +709,12 @@ //Getting a result back for a different download identifier, download must not have been successfully canceled if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { - return; + return; + } + + //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) + if (ASInterfaceStateIncludesPreload(self->_interfaceState) == NO) { + return; } if (imageContainer != nil) { From 4dbb6441d7181beee482e34a0d7daeee5820cc82 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Mon, 9 Oct 2017 11:48:32 -0700 Subject: [PATCH 35/86] Animated WebP support (#605) * Updating to support animated WebP * Fix a deadlock with display link * Fix playhead issue. * Fix up timing on iOS 10 and above * Don't redraw the same frame over and over * Clear out layer contents if we're an animated GIF on exit range * Clear out cover image on exit of visible range * Don't set cover image if we're no longer in display range. * Don't clear out image if we're not an animated image * Only set image if we're not already animating * Get rid of changes to podfile * Add CHANGELOG entry * Update license * Update PINRemoteImage * Remove commented out lines in example --- CHANGELOG.md | 1 + Cartfile | 4 +- Podfile | 2 +- Source/ASImageNode+AnimatedImage.mm | 63 +++++++++++++++---- Source/Details/ASPINRemoteImageDownloader.m | 52 +++++++-------- .../ASImageNode+AnimatedImagePrivate.h | 1 + Texture.podspec | 2 +- .../ASAnimatedImage/ViewController.m | 22 +++---- examples/AnimatedGIF/Podfile | 1 + 9 files changed, 95 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa2cfd606..bbb407e1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) +- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) ## 2.5 diff --git a/Cartfile b/Cartfile index bdcfff60c9..aebf9308e8 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "pinterest/PINRemoteImage" "3.0.0-beta.12" -github "pinterest/PINCache" "3.0.1-beta.5" +github "pinterest/PINRemoteImage" "3.0.0-beta.13" +github "pinterest/PINCache" diff --git a/Podfile b/Podfile index 0d8cb0fa26..0ebd8c98ca 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ target :'AsyncDisplayKitTests' do pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' # Only for buck build - pod 'PINRemoteImage', '3.0.0-beta.10' + pod 'PINRemoteImage', '3.0.0-beta.13' end #TODO CocoaPods plugin instead? diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 2ceed182f5..75e5d5f653 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -66,14 +66,17 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; }; } + animatedImage.playbackReadyCallback = ^{ + // In this case the lock is already gone we have to call the unlocked version therefore + [weakSelf setShouldAnimate:YES]; + }; if (animatedImage.playbackReady) { [self _locked_setShouldAnimate:YES]; - } else { - animatedImage.playbackReadyCallback = ^{ - // In this case the lock is already gone we have to call the unlocked version therefore - [weakSelf setShouldAnimate:YES]; - }; } + } else { + // Clean up after ourselves. + self.contents = nil; + [self setCoverImage:nil]; } [self animatedImageSet:_animatedImage previousAnimatedImage:previousAnimatedImage]; @@ -107,8 +110,10 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setCoverImageCompleted:(UIImage *)coverImage { - ASDN::MutexLocker l(__instanceLock__); - [self _locked_setCoverImageCompleted:coverImage]; + if (ASInterfaceStateIncludesDisplay(self.interfaceState)) { + ASDN::MutexLocker l(__instanceLock__); + [self _locked_setCoverImageCompleted:coverImage]; + } } - (void)_locked_setCoverImageCompleted:(UIImage *)coverImage @@ -132,9 +137,12 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; { //If we're a network image node, we want to set the default image so //that it will correctly be restored if it exits the range. +#if ASAnimatedImageDebug + NSLog(@"setting cover image: %p", self); +#endif if ([self isKindOfClass:[ASNetworkImageNode class]]) { [(ASNetworkImageNode *)self _locked_setDefaultImage:coverImage]; - } else { + } else if (_displayLink == nil || _displayLink.paused == YES) { [self _locked_setImage:coverImage]; } } @@ -218,11 +226,14 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; NSLog(@"starting animation: %p", self); #endif + // Get frame interval before holding display link lock to avoid deadlock + NSUInteger frameInterval = self.animatedImage.frameInterval; ASDN::MutexLocker l(_displayLinkLock); if (_displayLink == nil) { _playHead = 0; _displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)]; - _displayLink.frameInterval = self.animatedImage.frameInterval; + _displayLink.frameInterval = frameInterval; + _lastSuccessfulFrameIndex = NSUIntegerMax; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; } else { @@ -263,7 +274,9 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; if (self.animatedImage.coverImageReady) { [self setCoverImage:self.animatedImage.coverImage]; } - [self startAnimating]; + if (self.animatedImage.playbackReady) { + [self startAnimating]; + } } - (void)didExitVisibleState @@ -274,6 +287,26 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; [self stopAnimating]; } +- (void)didExitDisplayState +{ + ASDisplayNodeAssertMainThread(); +#if ASAnimatedImageDebug + NSLog(@"exiting display state: %p", self); +#endif + + // Check to see if we're an animated image before calling super in case someone + // decides they want to clear out the animatedImage itself on exiting the display + // state + BOOL isAnimatedImage = self.animatedImage != nil; + [super didExitDisplayState]; + + // Also clear out the contents we've set to be good citizens, we'll put it back in when we become visible. + if (isAnimatedImage) { + self.contents = nil; + [self setCoverImage:nil]; + } +} + #pragma mark - Display Link Callbacks - (void)displayLinkFired:(CADisplayLink *)displayLink @@ -283,6 +316,8 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; CFTimeInterval timeBetweenLastFire; if (self.lastDisplayLinkFire == 0) { timeBetweenLastFire = 0; + } else if (AS_AT_LEAST_IOS10){ + timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp; } else { timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire; } @@ -291,7 +326,8 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; _playHead += timeBetweenLastFire; while (_playHead > self.animatedImage.totalDuration) { - _playHead -= self.animatedImage.totalDuration; + // Set playhead to zero to keep from showing different frames on different playthroughs + _playHead = 0; _playedLoops++; } @@ -301,15 +337,18 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead]; + if (frameIndex == _lastSuccessfulFrameIndex) { + return; + } CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex]; if (frameImage == nil) { - _playHead -= timeBetweenLastFire; //Pause the display link until we get a file ready notification displayLink.paused = YES; self.lastDisplayLinkFire = 0; } else { self.contents = (__bridge id)frameImage; + _lastSuccessfulFrameIndex = frameIndex; [self displayDidFinish]; } } diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 601d009450..05107f8e00 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -24,14 +24,20 @@ #import #import -#if __has_include () +#if __has_include () #define PIN_ANIMATED_AVAILABLE 1 -#import +#import #import #else #define PIN_ANIMATED_AVAILABLE 0 #endif +#if __has_include() +#define PIN_WEBP_AVAILABLE 1 +#else +#define PIN_WEBP_AVAILABLE 0 +#endif + #import #import #import @@ -42,35 +48,23 @@ @end -@interface PINAnimatedImage (ASPINRemoteImageDownloader) +@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) @end -@implementation PINAnimatedImage (ASPINRemoteImageDownloader) - -- (void)setCoverImageReadyCallback:(void (^)(UIImage * _Nonnull))coverImageReadyCallback -{ - self.infoCompletion = coverImageReadyCallback; -} - -- (void (^)(UIImage * _Nonnull))coverImageReadyCallback -{ - return self.infoCompletion; -} - -- (void)setPlaybackReadyCallback:(dispatch_block_t)playbackReadyCallback -{ - self.fileReady = playbackReadyCallback; -} - -- (dispatch_block_t)playbackReadyCallback -{ - return self.fileReady; -} +@implementation PINCachedAnimatedImage (ASPINRemoteImageDownloader) - (BOOL)isDataSupported:(NSData *)data { - return [data pin_isGIF]; + if ([data pin_isGIF]) { + return YES; + } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return YES; + } +#endif + return NO; } @end @@ -187,7 +181,7 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; #if PIN_ANIMATED_AVAILABLE - (nullable id )animatedImageWithData:(NSData *)animatedImageData { - return [[PINAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; + return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; } #endif @@ -365,6 +359,12 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; if ([data pin_isGIF]) { return data; } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return data; + } +#endif + #endif return nil; } diff --git a/Source/Private/ASImageNode+AnimatedImagePrivate.h b/Source/Private/ASImageNode+AnimatedImagePrivate.h index 6e57d51dd1..bd2a4d3099 100644 --- a/Source/Private/ASImageNode+AnimatedImagePrivate.h +++ b/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -26,6 +26,7 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode; BOOL _animatedImagePaused; NSString *_animatedImageRunLoopMode; CADisplayLink *_displayLink; + NSUInteger _lastSuccessfulFrameIndex; //accessed on main thread only CFTimeInterval _playHead; diff --git a/Texture.podspec b/Texture.podspec index 5c353ed258..6bc7c16212 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -45,7 +45,7 @@ Pod::Spec.new do |spec| end spec.subspec 'PINRemoteImage' do |pin| - pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.12' + pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.13' pin.dependency 'PINRemoteImage/PINCache' pin.dependency 'Texture/Core' end diff --git a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m index 90d88fbe27..b4286440f5 100644 --- a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m +++ b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m @@ -1,20 +1,18 @@ // // ViewController.m -// Sample -// -// Created by Garrett Moon on 3/22/16. +// Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. +// 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. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// 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 "ViewController.h" @@ -33,6 +31,8 @@ ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init]; imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; + // Uncomment to see animated webp support + // imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"]; imageNode.frame = self.view.bounds; imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; imageNode.contentMode = UIViewContentModeScaleAspectFit; diff --git a/examples/AnimatedGIF/Podfile b/examples/AnimatedGIF/Podfile index 922ff50ec1..e784c52d14 100644 --- a/examples/AnimatedGIF/Podfile +++ b/examples/AnimatedGIF/Podfile @@ -2,5 +2,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target 'Sample' do pod 'Texture', :path => '../..' + pod 'PINRemoteImage/WebP' end From 929bd4c60e909bd75434399b626abc5af7d498ab Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 10 Oct 2017 16:03:30 +0100 Subject: [PATCH 36/86] [ASTextKitComponents] Temporary components can be deallocated off main (#610) --- Source/TextKit/ASTextKitComponents.h | 2 +- Source/TextKit/ASTextKitComponents.mm | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Source/TextKit/ASTextKitComponents.h b/Source/TextKit/ASTextKitComponents.h index 9fe5201c82..223abf5c10 100644 --- a/Source/TextKit/ASTextKitComponents.h +++ b/Source/TextKit/ASTextKitComponents.h @@ -59,7 +59,7 @@ AS_SUBCLASSING_RESTRICTED @property (nonatomic, strong, readonly) NSTextStorage *textStorage; @property (nonatomic, strong, readonly) NSTextContainer *textContainer; @property (nonatomic, strong, readonly) NSLayoutManager *layoutManager; -@property (nullable, nonatomic, strong) UITextView *textView; +@property (nonatomic, strong, nullable) UITextView *textView; @end diff --git a/Source/TextKit/ASTextKitComponents.mm b/Source/TextKit/ASTextKitComponents.mm index b733a4f4ee..6b3955eb75 100644 --- a/Source/TextKit/ASTextKitComponents.mm +++ b/Source/TextKit/ASTextKitComponents.mm @@ -27,6 +27,9 @@ @property (nonatomic, strong, readwrite) NSTextContainer *textContainer; @property (nonatomic, strong, readwrite) NSLayoutManager *layoutManager; +// Indicates whether or not this object must be deallocated on main thread. Defaults to YES. +@property (nonatomic, assign) BOOL requiresMainThreadDeallocation; + @end @implementation ASTextKitComponents @@ -58,6 +61,8 @@ components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. [components.layoutManager addTextContainer:components.textContainer]; + components.requiresMainThreadDeallocation = YES; + return components; } @@ -65,10 +70,13 @@ - (void)dealloc { - ASDisplayNodeAssertMainThread(); - - // Nil out all delegate to prevent crash - _textView.delegate = nil; + if (_requiresMainThreadDeallocation) { + ASDisplayNodeAssertMainThread(); + } + // Nil out all delegates to prevent crash + if (_textView) { + _textView.delegate = nil; + } _layoutManager.delegate = nil; } @@ -82,6 +90,8 @@ // Otherwise, we create a temporary stack to size for `constrainedWidth`. if (CGRectGetWidth(components.textView.bounds) != constrainedWidth) { components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + // The temporary stack can be deallocated off main + components.requiresMainThreadDeallocation = NO; } // Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:). @@ -102,6 +112,8 @@ // Always use temporary stack in case of threading issues components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + // The temporary stack can be deallocated off main + components.requiresMainThreadDeallocation = NO; // Force glyph generation and layout, which may not have happened yet (and isn't triggered by - usedRectForTextContainer:). [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; From 72d33fc88e9fbe5a812a9126ceba2824960305c9 Mon Sep 17 00:00:00 2001 From: Derek Argueta Date: Thu, 12 Oct 2017 04:44:30 -0700 Subject: [PATCH 37/86] Update Pinterest CDN URL in example code (#613) --- examples/AnimatedGIF/ASAnimatedImage/ViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m index b4286440f5..fe9b30bb33 100644 --- a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m +++ b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m @@ -30,7 +30,7 @@ // Do any additional setup after loading the view, typically from a nib. ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init]; - imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; + imageNode.URL = [NSURL URLWithString:@"https://i.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; // Uncomment to see animated webp support // imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"]; imageNode.frame = self.view.bounds; From 1e7d46196fb82b5a2afeccd9adbb9d5a1ae7ddac Mon Sep 17 00:00:00 2001 From: Mustafa Besnili Date: Tue, 17 Oct 2017 16:18:23 +0300 Subject: [PATCH 38/86] Fix "This block and function declaration is not a prototype" warning. (#619) --- Source/ASBlockTypes.h | 2 +- Source/ASCollectionNode.h | 8 ++++---- Source/ASCollectionView.h | 8 ++++---- Source/ASDisplayNode+Beta.h | 4 ++-- Source/ASDisplayNode.h | 10 +++++----- Source/ASDisplayNodeExtras.h | 4 ++-- Source/ASTableNode.h | 8 ++++---- Source/ASTableView.h | 4 ++-- Source/Base/ASAssert.h | 6 +++--- Source/Base/ASLog.h | 14 +++++++------- Source/Details/ASDataController.h | 2 +- Source/Details/ASTraitCollection.h | 2 +- Source/Layout/ASLayoutElementPrivate.h | 4 ++-- Source/Private/ASCollectionView+Undeprecated.h | 4 ++-- Source/Private/ASInternalHelpers.h | 8 ++++---- Source/Private/ASInternalHelpers.m | 4 ++-- .../TextExperiment/Utility/ASTextUtilities.h | 4 ++-- 17 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Source/ASBlockTypes.h b/Source/ASBlockTypes.h index cf05a19377..f0e2875e12 100644 --- a/Source/ASBlockTypes.h +++ b/Source/ASBlockTypes.h @@ -22,7 +22,7 @@ /** * ASCellNode creation block. Used to lazily create the ASCellNode instance for a specified indexPath. */ -typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(); +typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(void); // Type for the cancellation checker block passed into the async display blocks. YES means the operation has been cancelled, NO means continue. typedef BOOL(^asdisplaynode_iscancelled_block_t)(void); diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index d60fd57ef0..5d77404582 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -244,7 +244,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. @@ -255,7 +255,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. @@ -284,7 +284,7 @@ NS_ASSUME_NONNULL_BEGIN * * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. */ -- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))didFinishProcessingUpdates; /** * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. @@ -382,7 +382,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UICollectionView's version. */ -- (void)reloadDataWithCompletion:(nullable void (^)())completion; +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion; /** diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index ba9dda2964..9f552e44c6 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -256,7 +256,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); /** * Perform a batch of updates asynchronously. This method must be called from the main thread. @@ -267,7 +267,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -276,7 +276,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UICollectionView's version. */ -- (void)reloadDataWithCompletion:(nullable void (^)())completion AS_UNAVAILABLE("Use ASCollectionNode method instead."); +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion AS_UNAVAILABLE("Use ASCollectionNode method instead."); /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -296,7 +296,7 @@ NS_ASSUME_NONNULL_BEGIN * See ASCollectionNode.h for full documentation of these methods. */ @property (nonatomic, readonly) BOOL isProcessingUpdates; -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion; - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASCollectionNode waitUntilAllUpdatesAreProcessed] instead."); /** diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index ea241bd164..fff4ee9752 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -28,8 +28,8 @@ NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN -void ASPerformBlockOnMainThread(void (^block)()); -void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT +void ASPerformBlockOnMainThread(void (^block)(void)); +void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT ASDISPLAYNODE_EXTERN_C_END #if ASEVENTLOG_ENABLE diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 84da628b54..bb81387b3f 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -36,17 +36,17 @@ NS_ASSUME_NONNULL_BEGIN /** * UIView creation block. Used to create the backing view of a new display node. */ -typedef UIView * _Nonnull(^ASDisplayNodeViewBlock)(); +typedef UIView * _Nonnull(^ASDisplayNodeViewBlock)(void); /** * UIView creation block. Used to create the backing view of a new display node. */ -typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(); +typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(void); /** * CALayer creation block. Used to create the backing layer of a new display node. */ -typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(); +typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(void); /** * ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread. @@ -880,7 +880,7 @@ extern NSInteger const ASDefaultDrawingPriority; - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(nullable void(^)())completion; + measurementCompletion:(nullable void(^)(void))completion; /** @@ -897,7 +897,7 @@ extern NSInteger const ASDefaultDrawingPriority; */ - (void)transitionLayoutWithAnimation:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(nullable void(^)())completion; + measurementCompletion:(nullable void(^)(void))completion; /** * @abstract Cancels all performing layout transitions. Can be called on any thread. diff --git a/Source/ASDisplayNodeExtras.h b/Source/ASDisplayNodeExtras.h index aefb445f9e..923f8b72fe 100644 --- a/Source/ASDisplayNodeExtras.h +++ b/Source/ASDisplayNodeExtras.h @@ -208,8 +208,8 @@ extern __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnode(ASDispla */ extern __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT; -extern UIColor *ASDisplayNodeDefaultPlaceholderColor() AS_WARN_UNUSED_RESULT; -extern UIColor *ASDisplayNodeDefaultTintColor() AS_WARN_UNUSED_RESULT; +extern UIColor *ASDisplayNodeDefaultPlaceholderColor(void) AS_WARN_UNUSED_RESULT; +extern UIColor *ASDisplayNodeDefaultTintColor(void) AS_WARN_UNUSED_RESULT; /** Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported. diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 8322faaf45..aae4f375d8 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -172,7 +172,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UITableView's version. */ -- (void)reloadDataWithCompletion:(nullable void (^)())completion; +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion; /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -198,7 +198,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Perform a batch of updates asynchronously with animations in the batch. This method must be called from the main thread. @@ -209,7 +209,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. @@ -238,7 +238,7 @@ NS_ASSUME_NONNULL_BEGIN * * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. */ -- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))didFinishProcessingUpdates; /** * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. diff --git a/Source/ASTableView.h b/Source/ASTableView.h index ba6736eba2..d99ae5b217 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -180,7 +180,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UITableView's version. */ --(void)reloadDataWithCompletion:(void (^ _Nullable)())completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); +-(void)reloadDataWithCompletion:(void (^ _Nullable)(void))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -219,7 +219,7 @@ NS_ASSUME_NONNULL_BEGIN * See ASTableNode.h for full documentation of these methods. */ @property (nonatomic, readonly) BOOL isProcessingUpdates; -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion; - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASTableNode waitUntilAllUpdatesAreProcessed] instead."); - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); diff --git a/Source/Base/ASAssert.h b/Source/Base/ASAssert.h index 21bdcf2dae..336ac3d0e9 100644 --- a/Source/Base/ASAssert.h +++ b/Source/Base/ASAssert.h @@ -78,11 +78,11 @@ #pragma mark - Main Thread Assertions Disabling ASDISPLAYNODE_EXTERN_C_BEGIN -BOOL ASMainThreadAssertionsAreDisabled(); +BOOL ASMainThreadAssertionsAreDisabled(void); -void ASPushMainThreadAssertionsDisabled(); +void ASPushMainThreadAssertionsDisabled(void); -void ASPopMainThreadAssertionsDisabled(); +void ASPopMainThreadAssertionsDisabled(void); ASDISPLAYNODE_EXTERN_C_END #pragma mark - Non-Fatal Assertions diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index e4f54fd812..5de8ab939e 100644 --- a/Source/Base/ASLog.h +++ b/Source/Base/ASLog.h @@ -40,31 +40,31 @@ ASDISPLAYNODE_EXTERN_C_BEGIN * are at the `debug` log level, which the system * disables in production. */ -void ASDisableLogging(); +void ASDisableLogging(void); /// Log for general node events e.g. interfaceState, didLoad. #define ASNodeLogEnabled 1 -os_log_t ASNodeLog(); +os_log_t ASNodeLog(void); /// Log for layout-specific events e.g. calculateLayout. #define ASLayoutLogEnabled 1 -os_log_t ASLayoutLog(); +os_log_t ASLayoutLog(void); /// Log for display-specific events e.g. display queue batches. #define ASDisplayLogEnabled 1 -os_log_t ASDisplayLog(); +os_log_t ASDisplayLog(void); /// Log for collection events e.g. reloadData, performBatchUpdates. #define ASCollectionLogEnabled 1 -os_log_t ASCollectionLog(); +os_log_t ASCollectionLog(void); /// Log for ASNetworkImageNode and ASMultiplexImageNode events. #define ASImageLoadingLogEnabled 1 -os_log_t ASImageLoadingLog(); +os_log_t ASImageLoadingLog(void); /// Specialized log for our main thread deallocation trampoline. #define ASMainThreadDeallocationLogEnabled 0 -os_log_t ASMainThreadDeallocationLog(); +os_log_t ASMainThreadDeallocationLog(void); ASDISPLAYNODE_EXTERN_C_END diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index b2d2f3ab8a..7590700e48 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -258,7 +258,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * See ASCollectionNode.h for full documentation of these methods. */ @property (nonatomic, readonly) BOOL isProcessingUpdates; -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion; - (void)waitUntilAllUpdatesAreProcessed; /** diff --git a/Source/Details/ASTraitCollection.h b/Source/Details/ASTraitCollection.h index fdff5c0b17..26714aa649 100644 --- a/Source/Details/ASTraitCollection.h +++ b/Source/Details/ASTraitCollection.h @@ -42,7 +42,7 @@ typedef struct ASPrimitiveTraitCollection { /** * Creates ASPrimitiveTraitCollection with default values. */ -extern ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(); +extern ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(void); /** * Creates a ASPrimitiveTraitCollection from a given UITraitCollection. diff --git a/Source/Layout/ASLayoutElementPrivate.h b/Source/Layout/ASLayoutElementPrivate.h index 9bc3101b06..96c520e5d7 100644 --- a/Source/Layout/ASLayoutElementPrivate.h +++ b/Source/Layout/ASLayoutElementPrivate.h @@ -37,9 +37,9 @@ extern int32_t const ASLayoutElementContextDefaultTransitionID; // Does not currently support nesting – there must be no current context. extern void ASLayoutElementPushContext(ASLayoutElementContext * context); -extern ASLayoutElementContext * _Nullable ASLayoutElementGetCurrentContext(); +extern ASLayoutElementContext * _Nullable ASLayoutElementGetCurrentContext(void); -extern void ASLayoutElementPopContext(); +extern void ASLayoutElementPopContext(void); NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index bc03fc6aa5..bf037b16ab 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -154,7 +154,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Perform a batch of updates asynchronously. This method must be called from the main thread. @@ -165,7 +165,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Triggers a relayout of all nodes. diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index 31766b366a..90a525ef83 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -32,15 +32,15 @@ BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL sele IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block); /// Dispatches the given block to the main queue if not already running on the main thread -void ASPerformBlockOnMainThread(void (^block)()); +void ASPerformBlockOnMainThread(void (^block)(void)); /// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue -void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT +void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT /// For deallocation of objects on a background thread without GCD overhead / thread explosion void ASPerformBackgroundDeallocation(id object); -CGFloat ASScreenScale(); +CGFloat ASScreenScale(void); CGSize ASFloorSizeValues(CGSize s); @@ -80,7 +80,7 @@ ASDISPLAYNODE_INLINE BOOL ASImageAlphaInfoIsOpaque(CGImageAlphaInfo info) { @param withoutAnimation Set to `YES` to perform given block without animation @param block Perform UIView geometry changes within the passed block */ -ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { +ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)(void)) { if (withoutAnimation) { [UIView performWithoutAnimation:block]; } else { diff --git a/Source/Private/ASInternalHelpers.m b/Source/Private/ASInternalHelpers.m index e0d757101b..8bc865eb30 100644 --- a/Source/Private/ASInternalHelpers.m +++ b/Source/Private/ASInternalHelpers.m @@ -60,7 +60,7 @@ IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block) } } -void ASPerformBlockOnMainThread(void (^block)()) +void ASPerformBlockOnMainThread(void (^block)(void)) { if (block == nil){ return; @@ -72,7 +72,7 @@ void ASPerformBlockOnMainThread(void (^block)()) } } -void ASPerformBlockOnBackgroundThread(void (^block)()) +void ASPerformBlockOnBackgroundThread(void (^block)(void)) { if (block == nil){ return; diff --git a/Source/Private/TextExperiment/Utility/ASTextUtilities.h b/Source/Private/TextExperiment/Utility/ASTextUtilities.h index c434cc9012..024fb269c4 100755 --- a/Source/Private/TextExperiment/Utility/ASTextUtilities.h +++ b/Source/Private/TextExperiment/Utility/ASTextUtilities.h @@ -164,13 +164,13 @@ static inline CGRect ASTextEmojiGetGlyphBoundingRectWithFontSize(CGFloat fontSiz Get the character set which should rotate in vertical form. @return The shared character set. */ -NSCharacterSet *ASTextVerticalFormRotateCharacterSet(); +NSCharacterSet *ASTextVerticalFormRotateCharacterSet(void); /** Get the character set which should rotate and move in vertical form. @return The shared character set. */ -NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet(); +NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet(void); /// Get the transform rotation. From c12509e67af00e85ee01740ad4ff52de06deefa0 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 17 Oct 2017 14:20:20 +0100 Subject: [PATCH 39/86] [ASTextKitComponents] Make sure Main Thread Checker isn't triggered during background calculations #trivial (#612) * Add unit test * Make sure TextKit components can calculate size in background without upsetting Main Thread Checker - Add a new thread-safe text view bounds. - Temporary components stack doesn't have a text view so it can be safely deallocated off main. * Add ASTextKitComponentsTextView * Remove unnecessary change * Fix minor mistake * ASTextKitComponentsTextView has only 1 initializer * Minor change * Switch to atomic property * Remove manual synthesization --- Source/ASEditableTextNode.mm | 4 +-- Source/TextKit/ASTextKitComponents.h | 9 +++-- Source/TextKit/ASTextKitComponents.mm | 48 +++++++++++++++++++-------- Tests/ASTextKitTests.mm | 29 +++++++++++++++- 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index 98351f0761..404049fcf3 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -75,7 +75,7 @@ See issue: https://github.com/facebook/AsyncDisplayKit/issues/1063 */ -@interface ASPanningOverriddenUITextView : UITextView +@interface ASPanningOverriddenUITextView : ASTextKitComponentsTextView { BOOL _shouldBlockPanGesture; } @@ -215,7 +215,7 @@ ASDN::MutexLocker l(_textKitLock); // Create and configure the placeholder text view. - _placeholderTextKitComponents.textView = [[UITextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; + _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; _placeholderTextKitComponents.textView.userInteractionEnabled = NO; _placeholderTextKitComponents.textView.accessibilityElementsHidden = YES; configureTextView(_placeholderTextKitComponents.textView); diff --git a/Source/TextKit/ASTextKitComponents.h b/Source/TextKit/ASTextKitComponents.h index 223abf5c10..20acc2c472 100644 --- a/Source/TextKit/ASTextKitComponents.h +++ b/Source/TextKit/ASTextKitComponents.h @@ -20,6 +20,12 @@ NS_ASSUME_NONNULL_BEGIN +@interface ASTextKitComponentsTextView : UITextView +- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder __unavailable; +- (instancetype)init __unavailable; +@end + AS_SUBCLASSING_RESTRICTED @interface ASTextKitComponents : NSObject @@ -52,14 +58,13 @@ AS_SUBCLASSING_RESTRICTED */ - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth; - - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth forMaxNumberOfLines:(NSInteger)numberOfLines; @property (nonatomic, strong, readonly) NSTextStorage *textStorage; @property (nonatomic, strong, readonly) NSTextContainer *textContainer; @property (nonatomic, strong, readonly) NSLayoutManager *layoutManager; -@property (nonatomic, strong, nullable) UITextView *textView; +@property (nonatomic, strong, nullable) ASTextKitComponentsTextView *textView; @end diff --git a/Source/TextKit/ASTextKitComponents.mm b/Source/TextKit/ASTextKitComponents.mm index 6b3955eb75..7e4a7ae68f 100644 --- a/Source/TextKit/ASTextKitComponents.mm +++ b/Source/TextKit/ASTextKitComponents.mm @@ -20,6 +20,37 @@ #import +@interface ASTextKitComponentsTextView () +@property (atomic, assign) CGRect threadSafeBounds; +@end + +@implementation ASTextKitComponentsTextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer +{ + self = [super initWithFrame:frame textContainer:textContainer]; + if (self) { + _threadSafeBounds = self.bounds; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + ASDisplayNodeAssertMainThread(); + [super setFrame:frame]; + self.threadSafeBounds = self.bounds; +} + +- (void)setBounds:(CGRect)bounds +{ + ASDisplayNodeAssertMainThread(); + [super setBounds:bounds]; + self.threadSafeBounds = bounds; +} + +@end + @interface ASTextKitComponents () // read-write redeclarations @@ -27,9 +58,6 @@ @property (nonatomic, strong, readwrite) NSTextContainer *textContainer; @property (nonatomic, strong, readwrite) NSLayoutManager *layoutManager; -// Indicates whether or not this object must be deallocated on main thread. Defaults to YES. -@property (nonatomic, assign) BOOL requiresMainThreadDeallocation; - @end @implementation ASTextKitComponents @@ -61,8 +89,6 @@ components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. [components.layoutManager addTextContainer:components.textContainer]; - components.requiresMainThreadDeallocation = YES; - return components; } @@ -70,11 +96,9 @@ - (void)dealloc { - if (_requiresMainThreadDeallocation) { - ASDisplayNodeAssertMainThread(); - } // Nil out all delegates to prevent crash if (_textView) { + ASDisplayNodeAssertMainThread(); _textView.delegate = nil; } _layoutManager.delegate = nil; @@ -88,10 +112,8 @@ // If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation. // Otherwise, we create a temporary stack to size for `constrainedWidth`. - if (CGRectGetWidth(components.textView.bounds) != constrainedWidth) { + if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) { components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; - // The temporary stack can be deallocated off main - components.requiresMainThreadDeallocation = NO; } // Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:). @@ -112,9 +134,7 @@ // Always use temporary stack in case of threading issues components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; - // The temporary stack can be deallocated off main - components.requiresMainThreadDeallocation = NO; - + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by - usedRectForTextContainer:). [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; diff --git a/Tests/ASTextKitTests.mm b/Tests/ASTextKitTests.mm index a39a333b10..6872061787 100644 --- a/Tests/ASTextKitTests.mm +++ b/Tests/ASTextKitTests.mm @@ -20,11 +20,14 @@ #import -#import #import +#import +#import #import #import +#import + @interface ASTextKitTests : XCTestCase @end @@ -201,4 +204,28 @@ static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize XCTAssert([renderer rectsForTextRange:NSMakeRange(0, attributedString.length) measureOption:ASTextKitRendererMeasureOptionBlock].count > 0); } +- (void)testTextKitComponentsCanCalculateSizeInBackground +{ + NSAttributedString *attributedString = + [[NSAttributedString alloc] + initWithString:@"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers Odd Future master cleanse tattooed four dollar toast small batch kale chips leggings meh photo booth occupy irony. " attributes:@{ASTextKitEntityAttributeName : [[ASTextKitEntityAttribute alloc] initWithEntity:@"entity"]}]; + ASTextKitComponents *components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:CGSizeZero]; + components.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:components.textContainer]; + components.textView.frame = CGRectMake(0, 0, 20, 1000); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Components deallocated in background"]; + + ASPerformBlockOnBackgroundThread(^{ + // Use an autorelease pool here to ensure temporary components are (and can be) released in background + @autoreleasepool { + [components sizeForConstrainedWidth:100]; + [components sizeForConstrainedWidth:50 forMaxNumberOfLines:5]; + } + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + @end From dce7ab3a9b17e46beaa1b5204c7479075ed77a30 Mon Sep 17 00:00:00 2001 From: Max Wang Date: Tue, 17 Oct 2017 07:03:53 -0700 Subject: [PATCH 40/86] [ASTextNode] Implement an example comparing ASTextNode 1 & 2 behavior. (#570) * fix SIMULATE_WEB_RESPONSE not imported #449 * add constraint to using catching for layout * Add TextNode example to test Yoga Layout * update Yoga version * add debugging log * fix lisence * clean up * clean up * fix lisence warning * add shared scheme * change sdk version * revert some metadata * Merge FlexLayoutExample to TextStressText. Add flags to control different TextNode used. * clean up * fix lisence and syntax * clean up * remove xcworkspacedata * Tiny coding style changes * Another tiny change related to code style --- examples_extra/TextStressTest/Podfile | 2 +- .../Sample.xcodeproj/project.pbxproj | 23 +++- .../TextStressTest/Sample/AppDelegate.m | 36 +++++-- .../Sample/CollectionViewController.h | 17 +++ .../Sample/CollectionViewController.m | 67 ++++++++++++ .../TextStressTest/Sample/TabBarController.h | 16 +++ .../TextStressTest/Sample/TabBarController.m | 19 ++++ .../TextStressTest/Sample/TextCellNode.h | 17 +++ .../TextStressTest/Sample/TextCellNode.m | 100 ++++++++++++++++++ 9 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 examples_extra/TextStressTest/Sample/CollectionViewController.h create mode 100644 examples_extra/TextStressTest/Sample/CollectionViewController.m create mode 100644 examples_extra/TextStressTest/Sample/TabBarController.h create mode 100644 examples_extra/TextStressTest/Sample/TabBarController.m create mode 100644 examples_extra/TextStressTest/Sample/TextCellNode.h create mode 100644 examples_extra/TextStressTest/Sample/TextCellNode.m diff --git a/examples_extra/TextStressTest/Podfile b/examples_extra/TextStressTest/Podfile index 922ff50ec1..6670022698 100644 --- a/examples_extra/TextStressTest/Podfile +++ b/examples_extra/TextStressTest/Podfile @@ -1,6 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target 'Sample' do - pod 'Texture', :path => '../..' + pod 'Texture/Yoga', :path => '../..' end diff --git a/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj b/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj index 3453463d27..86a3f35f70 100644 --- a/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; 92F1263CECFE3FFCC7A5F936 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8EC8300ABAAEA079224272A /* libPods-Sample.a */; }; + C081EE8D1F85AEEC00F0B5F1 /* TabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */; }; + C081EE921F85AFB800F0B5F1 /* CollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */; }; + C081EE931F85AFB800F0B5F1 /* TextCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE911F85AFB800F0B5F1 /* TextCellNode.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -29,6 +32,12 @@ 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; A950870A2154F92D5DC91F1A /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; + C081EE8B1F85AEEC00F0B5F1 /* TabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TabBarController.h; sourceTree = ""; }; + C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TabBarController.m; sourceTree = ""; }; + C081EE8E1F85AFB800F0B5F1 /* CollectionViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CollectionViewController.h; sourceTree = ""; }; + C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CollectionViewController.m; sourceTree = ""; }; + C081EE901F85AFB800F0B5F1 /* TextCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextCellNode.h; sourceTree = ""; }; + C081EE911F85AFB800F0B5F1 /* TextCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextCellNode.m; sourceTree = ""; }; E8EC8300ABAAEA079224272A /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -68,11 +77,17 @@ 05E2128319D4DB510098F589 /* Sample */ = { isa = PBXGroup; children = ( + C081EE8E1F85AFB800F0B5F1 /* CollectionViewController.h */, + C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */, + C081EE901F85AFB800F0B5F1 /* TextCellNode.h */, + C081EE911F85AFB800F0B5F1 /* TextCellNode.m */, 05E2128819D4DB510098F589 /* AppDelegate.h */, 05E2128919D4DB510098F589 /* AppDelegate.m */, 05E2128B19D4DB510098F589 /* ViewController.h */, 05E2128C19D4DB510098F589 /* ViewController.m */, 05E2128419D4DB510098F589 /* Supporting Files */, + C081EE8B1F85AEEC00F0B5F1 /* TabBarController.h */, + C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */, ); path = Sample; sourceTree = ""; @@ -181,13 +196,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 96436DA0C1AFF84D8041B522 /* [CP] Embed Pods Frameworks */ = { @@ -227,8 +245,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C081EE931F85AFB800F0B5F1 /* TextCellNode.m in Sources */, 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + C081EE8D1F85AEEC00F0B5F1 /* TabBarController.m in Sources */, 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + C081EE921F85AFB800F0B5F1 /* CollectionViewController.m in Sources */, 05E2128719D4DB510098F589 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/examples_extra/TextStressTest/Sample/AppDelegate.m b/examples_extra/TextStressTest/Sample/AppDelegate.m index a8e5594780..5be641c029 100644 --- a/examples_extra/TextStressTest/Sample/AppDelegate.m +++ b/examples_extra/TextStressTest/Sample/AppDelegate.m @@ -1,25 +1,39 @@ -/* This file provided by Facebook is for non-commercial testing and evaluation - * purposes only. Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ +// +// AppDelegate.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 "AppDelegate.h" +#import "TabBarController.h" +#import "CollectionViewController.h" #import "ViewController.h" + @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; - self.window.rootViewController = [[ViewController alloc] init]; + + ViewController *viewController = [[ViewController alloc] init]; + viewController.tabBarItem.title = @"TextStress"; + + CollectionViewController *cvc = [[CollectionViewController alloc] init]; + cvc.tabBarItem.title = @"Flexbox"; + + TabBarController *tabBarController = [[TabBarController alloc] init]; + tabBarController.viewControllers = @[cvc, viewController]; + + self.window.rootViewController = tabBarController; [self.window makeKeyAndVisible]; return YES; } diff --git a/examples_extra/TextStressTest/Sample/CollectionViewController.h b/examples_extra/TextStressTest/Sample/CollectionViewController.h new file mode 100644 index 0000000000..159b2fa1c9 --- /dev/null +++ b/examples_extra/TextStressTest/Sample/CollectionViewController.h @@ -0,0 +1,17 @@ +// +// CollectionViewController.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 +#import + +@interface CollectionViewController : ASViewController +@end diff --git a/examples_extra/TextStressTest/Sample/CollectionViewController.m b/examples_extra/TextStressTest/Sample/CollectionViewController.m new file mode 100644 index 0000000000..d93005236b --- /dev/null +++ b/examples_extra/TextStressTest/Sample/CollectionViewController.m @@ -0,0 +1,67 @@ +// +// CollectionViewController.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 "CollectionViewController.h" +#import "TextCellNode.h" + +@interface CollectionViewController() +{ + ASCollectionNode *_collectionNode; + NSArray *_labels; + TextCellNode *_cellNode; +} + +@end + +@implementation CollectionViewController + +- (instancetype)init +{ + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; + CGRect rect = [[UIApplication sharedApplication] statusBarFrame]; + _collectionNode.contentInset = UIEdgeInsetsMake(rect.size.height, 0, 0, 0); + self = [super initWithNode:_collectionNode]; + if (self) { + _collectionNode.delegate = self; + _collectionNode.dataSource = self; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + _collectionNode.backgroundColor = [UIColor whiteColor]; + _labels = @[@"Fight of the Living Dead: Experiment Fight of the Living Dead: Experiment", @"S1 • E1"]; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return 1; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + _cellNode = [[TextCellNode alloc] initWithText1:_labels[0] text2:_labels[1]]; + return _cellNode; + }; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat width = collectionNode.view.bounds.size.width; + return ASSizeRangeMake(CGSizeMake(width, 0.0f), CGSizeMake(width, CGFLOAT_MAX)); +} + +@end diff --git a/examples_extra/TextStressTest/Sample/TabBarController.h b/examples_extra/TextStressTest/Sample/TabBarController.h new file mode 100644 index 0000000000..5a25bb04ac --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TabBarController.h @@ -0,0 +1,16 @@ +// +// TabBarController.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 TabBarController : ASTabBarController +@end diff --git a/examples_extra/TextStressTest/Sample/TabBarController.m b/examples_extra/TextStressTest/Sample/TabBarController.m new file mode 100644 index 0000000000..a7905cb235 --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TabBarController.m @@ -0,0 +1,19 @@ +// +// TabBarController.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 "TabBarController.h" + +@interface TabBarController () +@end + +@implementation TabBarController +@end diff --git a/examples_extra/TextStressTest/Sample/TextCellNode.h b/examples_extra/TextStressTest/Sample/TextCellNode.h new file mode 100644 index 0000000000..c4b35cfaf4 --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TextCellNode.h @@ -0,0 +1,17 @@ +// +// TextCellNode.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 TextCellNode : ASCellNode +- (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2; +@end diff --git a/examples_extra/TextStressTest/Sample/TextCellNode.m b/examples_extra/TextStressTest/Sample/TextCellNode.m new file mode 100644 index 0000000000..13bb7694cb --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TextCellNode.m @@ -0,0 +1,100 @@ +// +// TextCellNode.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 "TextCellNode.h" +#import +#import + +#ifndef USE_ASTEXTNODE_2 +#define USE_ASTEXTNODE_2 1 +#endif + +@interface TextCellNode() +{ +#if USE_ASTEXTNODE_2 + ASTextNode2 *_label1; + ASTextNode2 *_label2; +#else + ASTextNode *_label1; + ASTextNode *_label2; +#endif +} +@end + +@implementation TextCellNode + +- (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2 +{ + self = [super init]; + if (self) { + self.automaticallyManagesSubnodes = YES; + self.clipsToBounds = YES; +#if USE_ASTEXTNODE_2 + _label1 = [[ASTextNode2 alloc] init]; + _label2 = [[ASTextNode2 alloc] init]; +#else + _label1 = [[ASTextNode alloc] init]; + _label2 = [[ASTextNode alloc] init]; +#endif + + _label1.attributedText = [[NSAttributedString alloc] initWithString:text1]; + _label2.attributedText = [[NSAttributedString alloc] initWithString:text2]; + + _label1.maximumNumberOfLines = 1; + _label1.truncationMode = NSLineBreakByTruncatingTail; + _label2.maximumNumberOfLines = 1; + _label2.truncationMode = NSLineBreakByTruncatingTail; + + [self simpleSetupYogaLayout]; + } + return self; +} + +/** + This is to text a row with two labels, the first should be truncated with "...". + Layout is like: [l1Container[_label1], label2]. + This shows a bug of ASTextNode2. + */ +- (void)simpleSetupYogaLayout +{ + [self.style yogaNodeCreateIfNeeded]; + [_label1.style yogaNodeCreateIfNeeded]; + [_label2.style yogaNodeCreateIfNeeded]; + + _label1.style.flexGrow = 0; + _label1.style.flexShrink = 1; + _label1.backgroundColor = [UIColor lightGrayColor]; + + _label2.style.flexGrow = 0; + _label2.style.flexShrink = 0; + _label2.backgroundColor = [UIColor greenColor]; + + ASDisplayNode *l1Container = [ASDisplayNode yogaVerticalStack]; + + // TODO(fix ASTextNode2): next two line will show the bug of TextNode2 + // which works for ASTextNode though + // see discussion here: https://github.com/TextureGroup/Texture/pull/553 + l1Container.style.alignItems = ASStackLayoutAlignItemsCenter; + _label1.style.alignSelf = ASStackLayoutAlignSelfStart; + + l1Container.style.flexGrow = 0; + l1Container.style.flexShrink = 1; + + l1Container.yogaChildren = @[_label1]; + + self.style.justifyContent = ASStackLayoutJustifyContentSpaceBetween; + self.style.alignItems = ASStackLayoutAlignItemsStart; + self.style.flexDirection = ASStackLayoutDirectionHorizontal; + self.yogaChildren = @[l1Container, _label2]; +} + +@end From 526a7cfb5338a6fcbd96cae6e20dfd5a89cb82fa Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 18 Oct 2017 15:49:58 -0700 Subject: [PATCH 41/86] Fix name clash with YYText (#623) --- .../TextExperiment/Component/ASTextDebugOption.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Private/TextExperiment/Component/ASTextDebugOption.m b/Source/Private/TextExperiment/Component/ASTextDebugOption.m index e823d328b1..2e0a706eaa 100755 --- a/Source/Private/TextExperiment/Component/ASTextDebugOption.m +++ b/Source/Private/TextExperiment/Component/ASTextDebugOption.m @@ -16,14 +16,14 @@ static pthread_mutex_t _sharedDebugLock; static CFMutableSetRef _sharedDebugTargets = nil; static ASTextDebugOption *_sharedDebugOption = nil; -static const void* _sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { +static const void* _as_sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { return value; } -static void _sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { +static void _as_sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { } -void _sharedDebugSetFunction(const void *value, void *context) { +void _as_sharedDebugSetFunction(const void *value, void *context) { id target = (__bridge id)(value); [target setDebugOption:_sharedDebugOption]; } @@ -33,8 +33,8 @@ static void _initSharedDebug() { dispatch_once(&onceToken, ^{ pthread_mutex_init(&_sharedDebugLock, NULL); CFSetCallBacks callbacks = kCFTypeSetCallBacks; - callbacks.retain = _sharedDebugSetRetain; - callbacks.release = _sharedDebugSetRelease; + callbacks.retain = _as_sharedDebugSetRetain; + callbacks.release = _as_sharedDebugSetRelease; _sharedDebugTargets = CFSetCreateMutable(CFAllocatorGetDefault(), 0, &callbacks); }); } @@ -43,7 +43,7 @@ static void _setSharedDebugOption(ASTextDebugOption *option) { _initSharedDebug(); pthread_mutex_lock(&_sharedDebugLock); _sharedDebugOption = option.copy; - CFSetApplyFunction(_sharedDebugTargets, _sharedDebugSetFunction, NULL); + CFSetApplyFunction(_sharedDebugTargets, _as_sharedDebugSetFunction, NULL); pthread_mutex_unlock(&_sharedDebugLock); } From 919ec8d32b31000de0c310815dee96c8849002f6 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 20 Oct 2017 09:23:07 -0700 Subject: [PATCH 42/86] Check if we need to do a batch update (#624) * Check if we need to do a batch update If we've changed our leading screens for batch fetching, we may need to batch fetch. * Add CHANGELOG entry --- CHANGELOG.md | 1 + Source/ASCollectionView.mm | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb407e1ba..f6d33cd931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) +- [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) ## 2.5 diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 0c706faa46..70dff45aab 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1585,7 +1585,10 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { - _leadingScreensForBatching = leadingScreensForBatching; + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + [self _checkForBatchFetching]; + } } - (CGFloat)leadingScreensForBatching From d31af734df4beeae83dda7d6083c5febf4ba27e5 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 20 Oct 2017 15:01:38 -0700 Subject: [PATCH 43/86] Dispatch batch update to main #trivial (#626) * Dispatch batch update to main * TableView too --- Source/ASCollectionView.mm | 4 +++- Source/ASTableView.mm | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 70dff45aab..ef9df71a5c 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1587,7 +1587,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; { if (_leadingScreensForBatching != leadingScreensForBatching) { _leadingScreensForBatching = leadingScreensForBatching; - [self _checkForBatchFetching]; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); } } diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 4606726f23..1458a47810 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -1318,7 +1318,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { - _leadingScreensForBatching = leadingScreensForBatching; + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); + } } - (BOOL)automaticallyAdjustsContentOffset From 53147f0ce6c2ac27db858331cd355149072f30b3 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Sun, 22 Oct 2017 17:36:52 -0700 Subject: [PATCH 44/86] Updating podspec to 2.5.1 --- Texture.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Texture.podspec b/Texture.podspec index 6bc7c16212..ec743417d5 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '2.5' + spec.version = '2.5.1' spec.license = { :type => 'BSD and Apache 2', } spec.homepage = 'http://texturegroup.org' spec.authors = { 'Huy Nguyen' => 'huy@pinterest.com', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'schneider@pinterest.com', 'Adlai Holler' => 'adlai@pinterest.com' } From 77795318101af180efed8cc1b3514cd210072f63 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Sun, 22 Oct 2017 17:38:31 -0700 Subject: [PATCH 45/86] Update CHANGELOG for 2.5.1 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d33cd931..de2296dec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## master - * Add your own contributions to the next release on the line below this with your name. - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) + +## 2.5.1 - [ASVideoNode] Fix unreleased time observer. [Flo Vouin](https://github.com/flovouin) - [PINCache] Set a default .byteLimit to reduce disk usage and startup time. [#595](https://github.com/TextureGroup/Texture/pull/595) [Scott Goodson](https://github.com/appleguy) - [ASNetworkImageNode] Fix deadlock in GIF handling. [#582](https://github.com/TextureGroup/Texture/pull/582) [Garrett Moon](https://github.com/garrettmoon) From 63842e1a39ee988b03206393f9c17fef428d14fb Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Sun, 22 Oct 2017 17:42:57 -0700 Subject: [PATCH 46/86] Fixing changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de2296dec9..3a79c97d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## master * Add your own contributions to the next release on the line below this with your name. - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) +- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) +- [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) ## 2.5.1 - [ASVideoNode] Fix unreleased time observer. [Flo Vouin](https://github.com/flovouin) @@ -9,8 +11,6 @@ - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) -- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) -- [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) ## 2.5 From 68f3468d919f6b876e24c24b57468c0860300bf3 Mon Sep 17 00:00:00 2001 From: appleguy Date: Tue, 24 Oct 2017 05:12:52 -0700 Subject: [PATCH 47/86] [ASCollectionView] Improve performance and behavior of rotation / bounds changes. (#431) * [ASCollectionView] Improve performance and behavior of rotation / bounds changes. See #430 for details. * Edit CHANGELOG.md * [ASDataController] Implement -relayoutAllNodesWithInvalidationBlock:, to flush the ASMainSerialQueue before -invalidateLayout is called. * Don't set download results if no longer in preload range. (#606) Good catch by @djblake, if you scroll fast enough, you leave images set on the image node because didExitPreloadRange (which would have cleared it) was already called. * Animated WebP support (#605) * Updating to support animated WebP * Fix a deadlock with display link * Fix playhead issue. * Fix up timing on iOS 10 and above * Don't redraw the same frame over and over * Clear out layer contents if we're an animated GIF on exit range * Clear out cover image on exit of visible range * Don't set cover image if we're no longer in display range. * Don't clear out image if we're not an animated image * Only set image if we're not already animating * Get rid of changes to podfile * Add CHANGELOG entry * Update license * Update PINRemoteImage * Remove commented out lines in example * [ASDataController] Add nullable specifier to invalidationBlock for relayout of nodes. --- CHANGELOG.md | 1 + Source/ASCollectionView.mm | 11 ++++++----- Source/ASTableView.mm | 4 ++-- Source/Details/ASDataController.h | 5 ++++- Source/Details/ASDataController.mm | 9 ++++++++- Tests/ASTableViewTests.mm | 4 ++-- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a79c97d97..b4bb3a65e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) - [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index ef9df71a5c..da31222ec2 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -367,7 +367,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (void)relayoutItems { - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:^{ + [self.collectionViewLayout invalidateLayout]; + }]; } - (BOOL)isProcessingUpdates @@ -2276,10 +2278,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; BOOL changedInNonScrollingDirection = (fixedHorizontally && newBounds.size.width != lastUsedSize.width) || (fixedVertically && newBounds.size.height != lastUsedSize.height); if (changedInNonScrollingDirection) { - [_dataController relayoutAllNodes]; - [_dataController waitUntilAllUpdatesAreProcessed]; - // We need to ensure the size requery is done before we update our layout. - [self.collectionViewLayout invalidateLayout]; + [_dataController relayoutAllNodesWithInvalidationBlock:^{ + [self.collectionViewLayout invalidateLayout]; + }]; } } diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 1458a47810..32e4979778 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -553,7 +553,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)relayoutItems { - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -760,7 +760,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; _nodesConstrainedWidth = constrainedWidth; [self beginUpdates]; - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } else { if (_cellsForLayoutUpdates.count > 0) { diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 7590700e48..29a13f8466 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -244,8 +244,11 @@ extern NSString * const ASCollectionInvalidUpdateException; * * @discussion Used to respond to a change in size of the containing view * (e.g. ASTableView or ASCollectionView after an orientation change). + * + * The invalidationBlock is called after flushing the ASMainSerialQueue, which ensures that any in-progress + * layout calculations have been applied. The block will not be called if data hasn't been loaded. */ -- (void)relayoutAllNodes; +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock; /** * Re-measures given nodes in the backing store. diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index f42d013069..c8b489e295 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -759,7 +759,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; } } -- (void)relayoutAllNodes +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { ASDisplayNodeAssertMainThread(); if (!_initialReloadDataHasBeenCalled) { @@ -770,6 +770,13 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _visibleMap LOG(@"Edit Command - relayoutRows"); [self _scheduleBlockOnMainSerialQueue:^{ + // Because -invalidateLayout doesn't trigger any operations by itself, and we answer queries from UICollectionView using layoutThatFits:, + // we invalidate the layout before we have updated all of the cells. Any cells that the collection needs the size of immediately will get + // -layoutThatFits: with a new constraint, on the main thread, and synchronously calculate them. Meanwhile, relayoutAllNodes will update + // the layout of any remaining nodes on background threads (and fast-return for any nodes that the UICV got to first). + if (invalidationBlock) { + invalidationBlock(); + } [self _relayoutAllNodes]; }]; } diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index f26d96b119..939c910fb1 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -37,10 +37,10 @@ @implementation ASTestDataController -- (void)relayoutAllNodes +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { _numberOfAllNodesRelayouts++; - [super relayoutAllNodes]; + [super relayoutAllNodesWithInvalidationBlock:invalidationBlock]; } @end From 7fffebe293540599748a1ecf96a90fc802e8294e Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Tue, 24 Oct 2017 08:15:40 -0700 Subject: [PATCH 48/86] Update to 2.6 to indicate Xcode upgrade required. --- CHANGELOG.md | 1 + Texture.podspec | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bb3a65e9..e42ea465c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) - [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Texture.podspec b/Texture.podspec index ec743417d5..73e9047728 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '2.5.1' + spec.version = '2.6' spec.license = { :type => 'BSD and Apache 2', } spec.homepage = 'http://texturegroup.org' spec.authors = { 'Huy Nguyen' => 'huy@pinterest.com', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'schneider@pinterest.com', 'Adlai Holler' => 'adlai@pinterest.com' } From d00a58580accaacd9cff9f52f0a4f3650d5765d7 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Tue, 24 Oct 2017 08:20:41 -0700 Subject: [PATCH 49/86] Xcode update line should be in 2.6 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42ea465c1..d1386609be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ ## master * Add your own contributions to the next release on the line below this with your name. -- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) - [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) - [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) +## 2.6 +- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) + ## 2.5.1 - [ASVideoNode] Fix unreleased time observer. [Flo Vouin](https://github.com/flovouin) - [PINCache] Set a default .byteLimit to reduce disk usage and startup time. [#595](https://github.com/TextureGroup/Texture/pull/595) [Scott Goodson](https://github.com/appleguy) From 3b91fbaab238b7380e7a2067ba9dcb1e4efb089e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 24 Oct 2017 16:50:50 +0100 Subject: [PATCH 50/86] Update "Getting Started" page (#633) --- docs/_docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/getting-started.md b/docs/_docs/getting-started.md index 8b11e57cff..ae96e6cfb2 100755 --- a/docs/_docs/getting-started.md +++ b/docs/_docs/getting-started.md @@ -42,7 +42,7 @@ Texture's layout engine is both one of its most powerful and one of its most uni

Advanced Developer Features

-Texture offers a variety of advanced developer features that cannot be found in UIKit or Foundation. Our developers have found that AsyncDisplyKit allows simplifications in their architecture and improves developer velocity. +Texture offers a variety of advanced developer features that cannot be found in UIKit or Foundation. Our developers have found that Texture allows simplifications in their architecture and improves developer velocity. (Full list coming soon!) From baf1ea2db449c9dbee5051a6bfb42cb8ad78effd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?r=20=C3=A9=20m=20i=20=EF=A3=BF?= Date: Tue, 24 Oct 2017 20:40:28 +0200 Subject: [PATCH 51/86] introduce tests for the ASNavigationViewController (#627) --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++ Tests/ASNavigationControllerTests.m | 56 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 Tests/ASNavigationControllerTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 068938a4d2..15867e86ec 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -307,6 +307,7 @@ B350625C1B010F070018CF92 /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */; }; C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A0A1E60BEB400626263 /* ASDisplayNode+Convenience.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */; }; @@ -783,6 +784,7 @@ B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutManager.h; path = TextKit/ASLayoutManager.h; sourceTree = ""; }; B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASLayoutManager.m; path = TextKit/ASLayoutManager.m; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASNavigationControllerTests.m; sourceTree = ""; }; BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Convenience.h"; sourceTree = ""; }; CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASDisplayNode+Convenience.m"; sourceTree = ""; }; @@ -1157,6 +1159,7 @@ children = ( CC583ABF1EF9BAB400134156 /* Common */, CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */, CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, @@ -2130,6 +2133,7 @@ CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */, CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, + BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, 695BE2551DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm in Sources */, CCA221D31D6FA7EF00AF6A0F /* ASViewControllerTests.m in Sources */, diff --git a/Tests/ASNavigationControllerTests.m b/Tests/ASNavigationControllerTests.m new file mode 100644 index 0000000000..80f6ba87fa --- /dev/null +++ b/Tests/ASNavigationControllerTests.m @@ -0,0 +1,56 @@ +// +// ASNavigationControllerTests.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import "ASNavigationController.h" + +@interface ASNavigationControllerTests : XCTestCase +@end + +@implementation ASNavigationControllerTests + +- (void)testSetViewControllers { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController, secondController]; + ASNavigationController *navigationController = [ASNavigationController new]; + [navigationController setViewControllers:@[firstController, secondController]]; + XCTAssertEqual(navigationController.topViewController, secondController); + XCTAssertEqual(navigationController.visibleViewController, secondController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +- (void)testPopViewController { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController]; + ASNavigationController *navigationController = [ASNavigationController new]; + [navigationController setViewControllers:@[firstController, secondController]]; + [navigationController popViewControllerAnimated:false]; + XCTAssertEqual(navigationController.topViewController, firstController); + XCTAssertEqual(navigationController.visibleViewController, firstController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +- (void)testPushViewController { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController, secondController]; + ASNavigationController *navigationController = [[ASNavigationController new] initWithRootViewController:firstController]; + [navigationController pushViewController:secondController animated:false]; + XCTAssertEqual(navigationController.topViewController, secondController); + XCTAssertEqual(navigationController.visibleViewController, secondController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +@end From 128700f82d857858fd1f30c7fc441e649e7bf613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?r=20=C3=A9=20m=20i=20=EF=A3=BF?= Date: Tue, 24 Oct 2017 20:40:48 +0200 Subject: [PATCH 52/86] [Tests] Add test scrollToPageAtIndex ASPagerNode (#629) * add scrollToPageAtIndex for the ASPagerNode * update convention code --- Tests/ASPagerNodeTests.m | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Tests/ASPagerNodeTests.m b/Tests/ASPagerNodeTests.m index 05396387b9..6f71577685 100644 --- a/Tests/ASPagerNodeTests.m +++ b/Tests/ASPagerNodeTests.m @@ -50,7 +50,8 @@ @implementation ASPagerNodeTestController -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Populate these immediately so that they're not unexpectedly nil during tests. @@ -74,7 +75,8 @@ @implementation ASPagerNodeTests -- (void)testPagerReturnsIndexOfPages { +- (void)testPagerReturnsIndexOfPages +{ ASPagerNodeTestController *testController = [self testController]; ASCellNode *cellNode = [testController.pagerNode nodeForPageAtIndex:0]; @@ -82,7 +84,8 @@ XCTAssertEqual([testController.pagerNode indexOfPageWithNode:cellNode], 0); } -- (void)testPagerReturnsNotFoundForCellThatDontExistInPager { +- (void)testPagerReturnsNotFoundForCellThatDontExistInPager +{ ASPagerNodeTestController *testController = [self testController]; ASCellNode *badNode = [[ASCellNode alloc] init]; @@ -90,7 +93,17 @@ XCTAssertEqual([testController.pagerNode indexOfPageWithNode:badNode], NSNotFound); } -- (ASPagerNodeTestController *)testController { +- (void)testScrollPageToIndex +{ + ASPagerNodeTestController *testController = [self testController]; + testController.pagerNode.frame = CGRectMake(0, 0, 500, 500); + [testController.pagerNode scrollToPageAtIndex:1 animated:false]; + + XCTAssertEqual(testController.pagerNode.currentPageIndex, 1); +} + +- (ASPagerNodeTestController *)testController +{ ASPagerNodeTestController *testController = [[ASPagerNodeTestController alloc] initWithNibName:nil bundle:nil]; UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; [window makeKeyAndVisible]; From 8317c11e42a9310679cfedd09c34484d78c50b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?r=20=C3=A9=20m=20i=20=EF=A3=BF?= Date: Wed, 25 Oct 2017 11:53:37 +0200 Subject: [PATCH 53/86] [Tests] Introducing tests for the ASTabBarController (#628) * introducing tests for the ASTabBarController * Update ASTabBarControllerTests.m --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++ Tests/ASTabBarControllerTests.m | 45 +++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 Tests/ASTabBarControllerTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 15867e86ec..cefcdea8e4 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -307,6 +307,7 @@ B350625C1B010F070018CF92 /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */; }; BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */; }; C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -784,6 +785,7 @@ B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutManager.h; path = TextKit/ASLayoutManager.h; sourceTree = ""; }; B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASLayoutManager.m; path = TextKit/ASLayoutManager.m; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASTabBarControllerTests.m; sourceTree = ""; }; BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASNavigationControllerTests.m; sourceTree = ""; }; BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Convenience.h"; sourceTree = ""; }; @@ -1161,6 +1163,7 @@ CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */, CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */, @@ -2135,6 +2138,7 @@ F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, + BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.m in Sources */, 695BE2551DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm in Sources */, CCA221D31D6FA7EF00AF6A0F /* ASViewControllerTests.m in Sources */, 058D0A38195D057000B7D73C /* ASDisplayLayerTests.m in Sources */, diff --git a/Tests/ASTabBarControllerTests.m b/Tests/ASTabBarControllerTests.m new file mode 100644 index 0000000000..0e6d9b3d31 --- /dev/null +++ b/Tests/ASTabBarControllerTests.m @@ -0,0 +1,45 @@ +// +// ASTabBarControllerTests.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 "ASTabBarController.h" +#import "ASViewController.h" + +@interface ASTabBarControllerTests: XCTestCase + +@end + +@implementation ASTabBarControllerTests + +- (void)testTabBarControllerSelectIndex { + ASViewController *firstViewController = [ASViewController new]; + ASViewController *secondViewController = [ASViewController new]; + NSArray *viewControllers = @[firstViewController, secondViewController]; + ASTabBarController *tabBarController = [ASTabBarController new]; + [tabBarController setViewControllers:viewControllers]; + [tabBarController setSelectedIndex:1]; + XCTAssertTrue([tabBarController.viewControllers isEqualToArray:viewControllers]); + XCTAssertEqual(tabBarController.selectedViewController, secondViewController); +} + +- (void)testTabBarControllerSelectViewController { + ASViewController *firstViewController = [ASViewController new]; + ASViewController *secondViewController = [ASViewController new]; + NSArray *viewControllers = @[firstViewController, secondViewController]; + ASTabBarController *tabBarController = [ASTabBarController new]; + [tabBarController setViewControllers:viewControllers]; + [tabBarController setSelectedViewController:secondViewController]; + XCTAssertTrue([tabBarController.viewControllers isEqualToArray:viewControllers]); + XCTAssertEqual(tabBarController.selectedViewController, secondViewController); +} + +@end From af99ff5ef24a9d5ffaaaa83195434b0f0fb3df49 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 25 Oct 2017 15:57:30 -0700 Subject: [PATCH 54/86] Have ASNetworkImageNode report whether images were cached or not (#639) * Have ASNetworkImageNode report whether images were cached or not. * Update changelog * Add fileURL case --- CHANGELOG.md | 1 + Source/ASNetworkImageNode.h | 27 +++++ Source/ASNetworkImageNode.mm | 40 +++++-- Source/Details/ASPINRemoteImageDownloader.m | 109 +++++++++++--------- 4 files changed, 119 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1386609be..ed264d5115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) - [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) +- [ASNetworkImageNode] New delegate callback to tell the consumer whether the image was loaded from cache or download. [Adlai Holler](https://github.com/Adlai-Holler) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index dc911bede6..455806cd27 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -130,6 +130,21 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - + +typedef NS_ENUM(NSInteger, ASNetworkImageSource) { + ASNetworkImageSourceUnspecified = 0, + ASNetworkImageSourceSynchronousCache, + ASNetworkImageSourceAsynchronousCache, + ASNetworkImageSourceFileURL, + ASNetworkImageSourceDownload, +}; + +/// A struct that carries details about ASNetworkImageNode's image loads. +typedef struct { + /// The source from which the image was loaded. + ASNetworkImageSource imageSource; +} ASNetworkImageNodeDidLoadInfo; + /** * The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to * notifications such as finished decoding and downloading an image. @@ -137,6 +152,18 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASNetworkImageNodeDelegate @optional +/** + * Notification that the image node finished downloading an image, with additional info. + * If implemented, this method will be called instead of `imageNode:didLoadImage:`. + * + * @param imageNode The sender. + * @param image The newly-loaded image. + * @param info Misc information about the image load. + * + * @discussion Called on a background queue. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageNodeDidLoadInfo)info; + /** * Notification that the image node finished downloading an image. * diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 2b22ea5261..0a7ee0fbde 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -56,6 +56,7 @@ unsigned int delegateDidFailWithError:1; unsigned int delegateDidFinishDecoding:1; unsigned int delegateDidLoadImage:1; + unsigned int delegateDidLoadImageWithInfo:1; } _delegateFlags; @@ -305,6 +306,7 @@ _delegateFlags.delegateDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)]; _delegateFlags.delegateDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]; _delegateFlags.delegateDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)]; + _delegateFlags.delegateDidLoadImageWithInfo = [delegate respondsToSelector:@selector(imageNode:didLoadImage:info:)]; } - (id)delegate @@ -353,8 +355,18 @@ if (result) { [self _locked_setCurrentImageQuality:1.0]; [self _locked__setImage:result]; - _imageLoaded = YES; + + // Call out to the delegate. + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker l(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceSynchronousCache; + [_delegate imageNode:self didLoadImage:result info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASDN::MutexUnlocker l(__instanceLock__); + [_delegate imageNode:self didLoadImage:result]; + } break; } } @@ -688,14 +700,19 @@ [self _locked_setCurrentImageQuality:1.0]; - if (_delegateFlags.delegateDidLoadImage) { + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceFileURL; + [delegate imageNode:self didLoadImage:self.image info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { ASDN::MutexUnlocker u(__instanceLock__); [delegate imageNode:self didLoadImage:self.image]; } }); } else { __weak __typeof__(self) weakSelf = self; - auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier) { + auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSource imageSource) { __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { @@ -732,7 +749,12 @@ strongSelf->_cacheUUID = nil; if (imageContainer != nil) { - if (strongSelf->_delegateFlags.delegateDidLoadImage) { + if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = imageSource; + [delegate imageNode:strongSelf didLoadImage:strongSelf.image info:info]; + } else if (strongSelf->_delegateFlags.delegateDidLoadImage) { ASDN::MutexUnlocker u(strongSelf->__instanceLock__); [delegate imageNode:strongSelf didLoadImage:strongSelf.image]; } @@ -763,10 +785,12 @@ } if ([imageContainer asdk_image] == nil && _downloader != nil) { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); + }]; } else { as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); - finished(imageContainer, nil, nil); + finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache); } }; @@ -780,7 +804,9 @@ completion:completion]; } } else { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); + }]; } } } diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 05107f8e00..b57b72102d 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -202,15 +202,11 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; callbackQueue:(dispatch_queue_t)callbackQueue completion:(ASImageCacherCompletion)completion { - // We do not check the cache here and instead check it in downloadImageWithURL to avoid checking the cache twice. - // If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { - completion(nil); - } else { - dispatch_async(callbackQueue, ^{ - completion(nil); - }); - } + [[self sharedPINRemoteImageManager] imageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + completion(result.image); + }]; + }]; } - (void)cachedImageWithURLs:(NSArray *)URLs @@ -256,51 +252,38 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion { - PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { - if (downloadProgress == nil) { return; } - - /// If we're targeting the main queue and we're on the main thread, call immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { - downloadProgress(completedBytes / (CGFloat)totalBytes); - } else { - dispatch_async(callbackQueue, ^{ - downloadProgress(completedBytes / (CGFloat)totalBytes); - }); - } - }; - - PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { - /// If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { + if (downloadProgress == nil) { return; } + + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + downloadProgress(completedBytes / (CGFloat)totalBytes); + }]; + }; + + PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ #if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation, result.error, result.UUID); + } else { + completion(result.image, result.error, result.UUID); + } #else - completion(result.image, result.error, result.UUID); + completion(result.image, result.error, result.UUID); #endif - } else { - dispatch_async(callbackQueue, ^{ -#if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } -#else - completion(result.image, result.error, result.UUID); -#endif - }); - } - }; - - return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs - options:PINRemoteImageManagerDownloadOptionsSkipDecode - progressImage:nil - progressDownload:progressDownload - completion:imageCompletion]; + }]; + }; + + // add "IgnoreCache" option since we have a caching API so we already checked it, not worth checking again. + // PINRemoteImage is responsible for coalescing downloads, and even if it wasn't, the tiny probability of + // extra downloads isn't worth the effort of rechecking caches every single time. In order to provide + // feedback to the consumer about whether images are cached, we can't simply make the cache a no-op and + // check the cache as part of this download. + return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs + options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache + progressImage:nil + progressDownload:progressDownload + completion:imageCompletion]; } - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier @@ -369,5 +352,29 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; return nil; } +#pragma mark - Private + +/** + * If on main thread and queue is main, perform now. + * If queue is nil, assert and perform now. + * Otherwise, dispatch async to queue. + */ ++ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)())work +{ + if (work == nil) { + // No need to assert here, really. We aren't expecting any feedback from this method. + return; + } + + if (ASDisplayNodeThreadIsMain() && queue == dispatch_get_main_queue()) { + work(); + } else if (queue == nil) { + ASDisplayNodeFailAssert(@"Callback queue should not be nil."); + work(); + } else { + dispatch_async(queue, work); + } +} + @end #endif From 63efdbde8f958fcf5cf09a8df19e96cb90843a54 Mon Sep 17 00:00:00 2001 From: appleguy Date: Tue, 31 Oct 2017 06:20:58 -0700 Subject: [PATCH 55/86] [ASCollectionView] Call -invalidateFlowLayoutDelegateMetrics when rotating. #trivial (#616) * [ASCollectionView] Ensure -invalidateFlowLayoutDelegateMetrics is called for UIKit passthrough cells. This allows rotation to work properly when rotating UIKit passthrough cells that need to change width. * [ASCollectionView] No need to verify node is still in model to handle view-only notifications. --- Source/ASCollectionView.mm | 51 +++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index da31222ec2..339728d83a 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -369,6 +369,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; { [_dataController relayoutAllNodesWithInvalidationBlock:^{ [self.collectionViewLayout invalidateLayout]; + [self invalidateFlowLayoutDelegateMetrics]; }]; } @@ -1165,9 +1166,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; { if (_asyncDelegateFlags.interopWillDisplayCell) { ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:modelIndexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; } } @@ -1226,9 +1226,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; { if (_asyncDelegateFlags.interopDidEndDisplayingCell) { ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:modelIndexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; } } @@ -1271,10 +1270,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopWillDisplaySupplementaryView) { - ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:modelIndexPath]; + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:indexPath]; } } @@ -1312,10 +1310,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)rawView forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopdidEndDisplayingSupplementaryView) { - ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:modelIndexPath]; + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:indexPath]; } } @@ -2253,18 +2250,17 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; */ - (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds { - if (_hasDataControllerLayoutDelegate) { - // Let the layout delegate handle bounds changes if it's available. - return; - } - if (self.collectionViewLayout == nil) { - return; - } + CGSize newSize = newBounds.size; CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; - if (CGSizeEqualToSize(lastUsedSize, newBounds.size)) { + if (CGSizeEqualToSize(lastUsedSize, newSize)) { return; } - _lastBoundsSizeUsedForMeasuringNodes = newBounds.size; + if (_hasDataControllerLayoutDelegate || self.collectionViewLayout == nil) { + // Let the layout delegate handle bounds changes if it's available. If no layout, it will init in the new state. + return; + } + + _lastBoundsSizeUsedForMeasuringNodes = newSize; // Laying out all nodes is expensive. // We only need to do this if the bounds changed in the non-scrollable direction. @@ -2272,15 +2268,14 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; // appearance update, we do not need to relayout all nodes. // For a more permanent fix to the unsafety mentioned above, see https://github.com/facebook/AsyncDisplayKit/pull/2182 ASScrollDirection scrollDirection = self.scrollableDirections; - BOOL fixedVertically = (ASScrollDirectionContainsVerticalDirection(scrollDirection) == NO); + BOOL fixedVertically = (ASScrollDirectionContainsVerticalDirection (scrollDirection) == NO); BOOL fixedHorizontally = (ASScrollDirectionContainsHorizontalDirection(scrollDirection) == NO); - BOOL changedInNonScrollingDirection = (fixedHorizontally && newBounds.size.width != lastUsedSize.width) || (fixedVertically && newBounds.size.height != lastUsedSize.height); + BOOL changedInNonScrollingDirection = (fixedHorizontally && newSize.width != lastUsedSize.width) || + (fixedVertically && newSize.height != lastUsedSize.height); if (changedInNonScrollingDirection) { - [_dataController relayoutAllNodesWithInvalidationBlock:^{ - [self.collectionViewLayout invalidateLayout]; - }]; + [self relayoutItems]; } } From d8c2a8edd2536a51e0aa0624e9aea53e3ed50599 Mon Sep 17 00:00:00 2001 From: Ha Hyun soo Date: Tue, 31 Oct 2017 22:25:37 +0900 Subject: [PATCH 56/86] [Documentation] Update Inversion Docs (#647) * Update docs append inversion example about swift language * apply github comment --- docs/_docs/inversion.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/_docs/inversion.md b/docs/_docs/inversion.md index 2231c20b95..5dd18140dd 100755 --- a/docs/_docs/inversion.md +++ b/docs/_docs/inversion.md @@ -23,8 +23,10 @@ When this is enabled, developers only have to take one more step to have full in self.tableNode.view.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, inset, 0); - + + ### Example using `ASDimension` @@ -68,8 +70,10 @@ self.rightStack.style.flexBasis = ASDimensionMake(@"60%"); self.leftStack.style.flexBasis = ASDimensionMake("40%") self.rightStack.style.flexBasis = ASDimensionMake("60%") -horizontalStack.children = [self.leftStack, self.rightStack]] +horizontalStack.children = [self.leftStack, self.rightStack] + + ## Sizes (`CGSize`, `ASLayoutSize`) @@ -187,5 +191,4 @@ func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec -
The `constrainedSize` passed to an `ASDisplayNode` subclass' `layoutSpecThatFits:` method is the minimum and maximum sizes that the node should fit in. The minimum and maximum `CGSize`s contained in `constrainedSize` can be used to size the node's layout elements. From d8cda8d74afba5c6ad6d98b4f33e048040966552 Mon Sep 17 00:00:00 2001 From: Erekle Date: Thu, 30 Nov 2017 16:25:06 +0400 Subject: [PATCH 71/86] [iOS11] Update project settings and fix errors (#676) * [iOS11] Update project settings and fix errors * update changelog * resolve comments --- AsyncDisplayKit.xcodeproj/project.pbxproj | 18 ++++++ CHANGELOG.md | 1 + Source/ASCollectionView.mm | 4 +- Source/ASDisplayNode.h | 2 +- Source/ASImageNode+AnimatedImage.mm | 2 +- Source/ASTextNode.mm | 2 +- Source/Base/ASLog.h | 43 ++++++++++-- Source/Details/ASDataController.h | 2 +- Source/Details/ASPINRemoteImageDownloader.m | 2 +- Source/Details/ASTraitCollection.m | 40 ++++++++---- Source/Details/UIView+ASConvenience.h | 2 +- Source/Private/ASTableView+Undeprecated.h | 2 +- Source/Private/_ASPendingState.mm | 18 ++++-- Tests/ASDisplayLayerTests.m | 4 +- Tests/ASDisplayNodeTests.mm | 72 ++++++++++++++------- Tests/ASLayoutFlatteningTests.m | 14 ++-- 16 files changed, 162 insertions(+), 66 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 28ee692cc4..fb9888c5fa 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -2443,14 +2443,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -2488,14 +2494,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -2631,14 +2643,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; diff --git a/CHANGELOG.md b/CHANGELOG.md index fa04a6c773..472f12134e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [iOS11] Update project settings and fix errors [Eke](https://github.com/Eke) [#676](https://github.com/TextureGroup/Texture/pull/676) - [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index c1751baf7a..2811f62715 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -273,7 +273,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; // Experiments done by Instagram show that this option being YES (default) // when unused causes a significant hit to scroll performance. // https://github.com/Instagram/IGListKit/issues/318 - if (AS_AT_LEAST_IOS10) { + if (AS_AVAILABLE_IOS(10)) { super.prefetchingEnabled = NO; } @@ -1972,7 +1972,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; /// The UIKit version of this method is only available on iOS >= 9 - (NSArray *)asdk_indexPathsForVisibleSupplementaryElementsOfKind:(NSString *)kind { - if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_9_0) { + if (AS_AVAILABLE_IOS(9)) { return [self indexPathsForVisibleSupplementaryElementsOfKind:kind]; } diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index bb81387b3f..feecc3ad94 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -702,7 +702,7 @@ extern NSInteger const ASDefaultDrawingPriority; */ @property (nonatomic, assign) UIViewContentMode contentMode; // default=UIViewContentModeScaleToFill @property (nonatomic, copy) NSString *contentsGravity; // Use .contentMode in preference when possible. -@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute; // default=Unspecified +@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); // default=Unspecified @property (nonatomic, nullable) CGColorRef shadowColor; // default=opaque rgb black @property (nonatomic, assign) CGFloat shadowOpacity; // default=0.0 diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 75e5d5f653..355ab472ef 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -316,7 +316,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; CFTimeInterval timeBetweenLastFire; if (self.lastDisplayLinkFire == 0) { timeBetweenLastFire = 0; - } else if (AS_AT_LEAST_IOS10){ + } else if (AS_AVAILABLE_IOS(10)){ timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp; } else { timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire; diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 6caaec8cd9..e5338f5217 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -539,7 +539,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } - (id)_linkAttributeValueAtPoint:(CGPoint)point - attributeName:(out NSString **)attributeNameOut + attributeName:(out NSString * __autoreleasing *)attributeNameOut range:(out NSRange *)rangeOut inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index 5de8ab939e..be4bff3685 100644 --- a/Source/Base/ASLog.h +++ b/Source/Base/ASLog.h @@ -119,11 +119,44 @@ ASDISPLAYNODE_EXTERN_C_END * The logging macros are not guarded by deployment-target checks like the activity macros are, but they are * only available on iOS >= 9 at runtime, so just make them conditional. */ -#define as_log_create(subsystem, category) (AS_AT_LEAST_IOS9 ? os_log_create(subsystem, category) : (os_log_t)0) -#define as_log_debug(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_debug(log, format, ##__VA_ARGS__) : (void)0) -#define as_log_info(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_info(log, format, ##__VA_ARGS__) : (void)0) -#define as_log_error(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_error(log, format, ##__VA_ARGS__) : (void)0) -#define as_log_fault(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_fault(log, format, ##__VA_ARGS__) : (void)0) + +#define as_log_create(subsystem, category) ({ \ +os_log_t __val; \ +if (AS_AVAILABLE_IOS(9)) { \ + __val = os_log_create(subsystem, category); \ +} else { \ + __val = (os_log_t)0; \ +} \ +__val; \ +}) + +#define as_log_debug(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_debug(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_info(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_info(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_error(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_error(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_fault(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_fault(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ #if ASEnableVerboseLogging #define as_log_verbose(log, format, ...) as_log_debug(log, format, ##__VA_ARGS__) diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 29a13f8466..acc98bceea 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -248,7 +248,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * The invalidationBlock is called after flushing the ASMainSerialQueue, which ensures that any in-progress * layout calculations have been applied. The block will not be called if data hasn't been loaded. */ -- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock; +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)(void))invalidationBlock; /** * Re-measures given nodes in the backing store. diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index b57b72102d..34cd1c7b77 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -359,7 +359,7 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; * If queue is nil, assert and perform now. * Otherwise, dispatch async to queue. */ -+ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)())work ++ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)(void))work { if (work == nil) { // No need to assert here, really. We aren't expecting any feedback from this method. diff --git a/Source/Details/ASTraitCollection.m b/Source/Details/ASTraitCollection.m index 0c845e9f63..04eaea608e 100644 --- a/Source/Details/ASTraitCollection.m +++ b/Source/Details/ASTraitCollection.m @@ -48,7 +48,7 @@ ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITra environmentTraitCollection.horizontalSizeClass = traitCollection.horizontalSizeClass; environmentTraitCollection.verticalSizeClass = traitCollection.verticalSizeClass; environmentTraitCollection.userInterfaceIdiom = traitCollection.userInterfaceIdiom; - if (AS_AT_LEAST_IOS9) { + if (AS_AVAILABLE_IOS(9)) { environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability; } return environmentTraitCollection; @@ -67,17 +67,28 @@ BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTr // Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceIdiom(UIUserInterfaceIdiom idiom) { - switch (idiom) { - case UIUserInterfaceIdiomTV: - return @"TV"; - case UIUserInterfaceIdiomPad: - return @"Pad"; - case UIUserInterfaceIdiomPhone: - return @"Phone"; - case UIUserInterfaceIdiomCarPlay: - return @"CarPlay"; - default: - return @"Unspecified"; + if (AS_AVAILABLE_IOS(9)) { + switch (idiom) { + case UIUserInterfaceIdiomTV: + return @"TV"; + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + case UIUserInterfaceIdiomCarPlay: + return @"CarPlay"; + default: + return @"Unspecified"; + } + } else { + switch (idiom) { + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + default: + return @"Unspecified"; + } } } @@ -167,7 +178,10 @@ NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection trai + (instancetype)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection containerSize:(CGSize)windowSize { - UIForceTouchCapability forceTouch = AS_AT_LEAST_IOS9 ? traitCollection.forceTouchCapability : UIForceTouchCapabilityUnknown; + UIForceTouchCapability forceTouch = UIForceTouchCapabilityUnknown; + if(AS_AVAILABLE_IOS(9)) { + forceTouch = traitCollection.forceTouchCapability; + } return [self traitCollectionWithDisplayScale:traitCollection.displayScale userInterfaceIdiom:traitCollection.userInterfaceIdiom horizontalSizeClass:traitCollection.horizontalSizeClass diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 109bf78128..452dcee6e5 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -71,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) CGRect bounds; @property (nonatomic, assign) CGRect frame; // Only for use with nodes wrapping synchronous views @property (nonatomic, assign) UIViewContentMode contentMode; -@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute; +@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); @property (nonatomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; @property (nonatomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch; @property (nonatomic, assign, getter=asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; diff --git a/Source/Private/ASTableView+Undeprecated.h b/Source/Private/ASTableView+Undeprecated.h index bab43adf06..7ddc9c28e7 100644 --- a/Source/Private/ASTableView+Undeprecated.h +++ b/Source/Private/ASTableView+Undeprecated.h @@ -146,7 +146,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UITableView's version. */ --(void)reloadDataWithCompletion:(void (^ _Nullable)())completion; +-(void)reloadDataWithCompletion:(void (^ _Nullable)(void))completion; /** * Reload everything from scratch, destroying the working range and all cached nodes. diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 9bb4dc1a65..4374401d87 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -141,7 +141,7 @@ typedef struct { NSArray *accessibilityHeaderElements; CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; - UISemanticContentAttribute semanticContentAttribute; + UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); ASPendingStateFlags _flags; } @@ -295,7 +295,9 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; accessibilityActivationPoint = CGPointZero; accessibilityPath = nil; edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerRightEdge | kCALayerTopEdge | kCALayerBottomEdge); - semanticContentAttribute = UISemanticContentAttributeUnspecified; + if (AS_AVAILABLE_IOS(9)) { + semanticContentAttribute = UISemanticContentAttributeUnspecified; + } return self; } @@ -573,7 +575,7 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; _flags.setAsyncTransactionContainer = YES; } -- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute { +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AVAILABLE(ios(9.0), tvos(9.0)) { semanticContentAttribute = attribute; _flags.setSemanticContentAttribute = YES; } @@ -1049,8 +1051,10 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; if (flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); - if (flags.setSemanticContentAttribute) { - view.semanticContentAttribute = semanticContentAttribute; + if (AS_AVAILABLE_IOS(9)) { + if (flags.setSemanticContentAttribute) { + view.semanticContentAttribute = semanticContentAttribute; + } } if (flags.setIsAccessibilityElement) @@ -1211,7 +1215,9 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; pendingState.allowsGroupOpacity = layer.allowsGroupOpacity; pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; - pendingState.semanticContentAttribute = view.semanticContentAttribute; + if (AS_AVAILABLE_IOS(9)) { + pendingState.semanticContentAttribute = view.semanticContentAttribute; + } pendingState.isAccessibilityElement = view.isAccessibilityElement; pendingState.accessibilityLabel = view.accessibilityLabel; pendingState.accessibilityHint = view.accessibilityHint; diff --git a/Tests/ASDisplayLayerTests.m b/Tests/ASDisplayLayerTests.m index 1abcc71541..1e5b19abac 100644 --- a/Tests/ASDisplayLayerTests.m +++ b/Tests/ASDisplayLayerTests.m @@ -148,7 +148,7 @@ typedef NS_ENUM(NSUInteger, _ASDisplayLayerTestDelegateClassModes) { // for _ASDisplayLayerTestDelegateModeClassDisplay @property (nonatomic, assign) NSUInteger displayCount; -@property (nonatomic, copy) UIImage *(^displayLayerBlock)(); +@property (nonatomic, copy) UIImage *(^displayLayerBlock)(void); // for _ASDisplayLayerTestDelegateModeClassDrawInContext @property (nonatomic, assign) NSUInteger drawRectCount; @@ -472,7 +472,7 @@ static _ASDisplayLayerTestDelegateClassModes _class_modes; layer1.displaysAsynchronously = YES; dispatch_semaphore_t displayAsyncLayer1Sema = dispatch_semaphore_create(0); - layer1Delegate.displayLayerBlock = ^(_ASDisplayLayer *asyncLayer) { + layer1Delegate.displayLayerBlock = ^UIImage *{ dispatch_semaphore_wait(displayAsyncLayer1Sema, DISPATCH_TIME_FOREVER); return bogusImage(); }; diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 75346ff857..df4adbec67 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -711,46 +711,54 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Setup CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* outer node's coordinate space to inner node's coordinate space node.frame = CGRectMake(100, 100, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(105, 105), correctPoint = CGPointMake(95, 95); + originalPoint = CGPointMake(105, 105); + correctPoint = CGPointMake(95, 95); convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* inner node's coordinate space to outer node's coordinate space node.frame = CGRectMake(100, 100, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(15, 15); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(15, 15); convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in inner node's coordinate space *TO* outer node's coordinate space node.frame = CGRectMake(100, 100, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(95, 95), correctPoint = CGPointMake(105, 105); + originalPoint = CGPointMake(95, 95); + correctPoint = CGPointMake(105, 105); convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in outer node's coordinate space *TO* inner node's coordinate space node.frame = CGRectMake(0, 0, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(-5, -5); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(-5, -5); convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); } @@ -764,7 +772,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Setup CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* outer node's coordinate space to inner node's coordinate space @@ -773,12 +782,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(20, 20, 100, 100); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(42, 42), correctPoint = CGPointMake(36, 36); + originalPoint = CGPointMake(42, 42); + correctPoint = CGPointMake(36, 36); convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* inner node's coordinate space to outer node's coordinate space @@ -787,12 +798,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(-1000, -1000, 1337, 1337); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(11, 11); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(11, 11); convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in inner node's coordinate space *TO* outer node's coordinate space @@ -801,12 +814,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(20, 20, 100, 100); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(36, 36), correctPoint = CGPointMake(42, 42); + originalPoint = CGPointMake(36, 36); + correctPoint = CGPointMake(42, 42); convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in outer node's coordinate space *TO* inner node's coordinate space @@ -815,7 +830,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(-1000, -1000, 1337, 1337); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(11, 11), correctPoint = CGPointMake(5, 5); + originalPoint = CGPointMake(11, 11); + correctPoint = CGPointMake(5, 5); convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); } @@ -828,7 +844,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Setup CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* outer node's coordinate space to inner node's coordinate space @@ -836,12 +853,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.75, 1); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(42, 42), correctPoint = CGPointMake(51, 56); + originalPoint = CGPointMake(42, 42); + correctPoint = CGPointMake(51, 56); convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* inner node's coordinate space to outer node's coordinate space @@ -849,12 +868,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.3, 0.3); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(55, 55), correctPoint = CGPointMake(1, 1); + originalPoint = CGPointMake(55, 55); + correctPoint = CGPointMake(1, 1); convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in inner node's coordinate space *TO* outer node's coordinate space @@ -862,12 +883,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.75, 1); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(51, 56), correctPoint = CGPointMake(42, 42); + originalPoint = CGPointMake(51, 56); + correctPoint = CGPointMake(42, 42); convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in outer node's coordinate space *TO* inner node's coordinate space @@ -875,7 +898,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.3, 0.3); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(1, 1), correctPoint = CGPointMake(55, 55); + originalPoint = CGPointMake(1, 1); + correctPoint = CGPointMake(55, 55); convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); } diff --git a/Tests/ASLayoutFlatteningTests.m b/Tests/ASLayoutFlatteningTests.m index 56f76c5d4a..fb04a95090 100644 --- a/Tests/ASLayoutFlatteningTests.m +++ b/Tests/ASLayoutFlatteningTests.m @@ -42,9 +42,9 @@ static ASLayout *layout(id element, NSArray *sublay NSMutableArray *layoutSpecs = [NSMutableArray array]; NSMutableArray *indirectSubnodes = [NSMutableArray array]; - ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; - ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; - ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)(void) = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)(void) = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; NSArray *sublayouts = @[ layout(subnode(), @[ @@ -118,7 +118,7 @@ static ASLayout *layout(id element, NSArray *sublay @autoreleasepool { ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; NSMutableArray *subnodes = [NSMutableArray array]; - ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, rootNode, @[ @@ -148,9 +148,9 @@ static ASLayout *layout(id element, NSArray *sublay NSMutableArray *indirectSubnodes = [NSMutableArray array]; NSMutableArray *reusedLayouts = [NSMutableArray array]; - ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; - ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; - ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)(void) = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)(void) = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; ASLayout *(^reusedLayout)(ASDisplayNode *) = ^ASLayout *(ASDisplayNode *subnode) { [reusedLayouts addObject:layout(subnode, @[])]; return [reusedLayouts lastObject]; }; /* From b01fac35b6a35de2871accd579ac44ada429d811 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 1 Dec 2017 13:11:17 +0000 Subject: [PATCH 72/86] [ASDisplayNode+Layout] Ensure a pending layout is applied once (#695) Before: - Even if a pending layout was applied before, it'll be unnecessarily applied again in next layout passes and cause `-calculatedLayoutDidChange` being called multiple times. After: - If a pending layout was applied, the calculated layout will not be ignored but reused, if possible, in next layout passes. Test plan: testSetNeedsLayoutAndNormalLayoutPass in #424. --- CHANGELOG.md | 1 + Source/ASDisplayNode+Layout.mm | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 472f12134e..99f6954570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - [API CHANGES] `ASPerformMainThreadDeallocation` and `ASPerformBackgroundDeallocation` functions take `id *` instead of `id` and they're now more reliable. Also, in Swift, `ASDeallocQueue.sharedDeallocationQueue() -> ASDeallocQueue.sharedDeallocationQueue`. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/651) - [Collection/Table] Added direct support for mapping section indexes between data spaces. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/660) - [ASCornerLayoutSpec] New layout spec class for declarative corner element layout. [#657](https://github.com/TextureGroup/Texture/pull/657) [huangkun](https://github.com/huang-kun) +- [Layout] Fix an issue that causes a pending layout to be applied multiple times. [Huy Nguyen](https://github.com/nguyenhuy) [#695](https://github.com/TextureGroup/Texture/pull/695) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index f91016ac4f..6d291c8300 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -300,14 +300,16 @@ ASLayoutElementStyleExtensibilityForwarding CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); - // Prefer _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout (if exists, it's the newest) - // If there is no _pending, check if _calculated is valid to reuse (avoiding recalculation below). - if (_pendingDisplayNodeLayout == nullptr || _pendingDisplayNodeLayout->version < _layoutVersion) { - if (_calculatedDisplayNodeLayout->version >= _layoutVersion - && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove == YES - || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { - return; - } + // Prefer a newer and not yet applied _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout + // If there is no such _pending, check if _calculated is valid to reuse (avoiding recalculation below). + BOOL pendingLayoutIsPreferred = (_pendingDisplayNodeLayout != nullptr + && _pendingDisplayNodeLayout->version >= _layoutVersion + && _pendingDisplayNodeLayout->version > _calculatedDisplayNodeLayout->version); // _pending is not yet applied + BOOL calculatedLayoutIsReusable = (_calculatedDisplayNodeLayout->version >= _layoutVersion + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))); + if (!pendingLayoutIsPreferred && calculatedLayoutIsReusable) { + return; } as_activity_create_for_scope("Update node layout for current bounds"); From bccde6cf0f26d71931f91d6d10fdd7a195a89669 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 1 Dec 2017 15:41:06 +0000 Subject: [PATCH 73/86] [ASScrollNode] Fix small bugs and add unit tests (#637) * Add unit tests for ASScrollNode * Make sure ASScrollNode's size is clamped against its size range * Invalidate ASScrollNode's calculated layout if its scrollable directions changed * Update comment * Update CHANGELOG * Address Adlai's comments --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + CHANGELOG.md | 2 + Source/ASScrollNode.mm | 11 +- Tests/ASScrollNodeTests.m | 139 ++++++++++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 Tests/ASScrollNodeTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index fb9888c5fa..025344d7e0 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -443,6 +443,7 @@ E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */; }; E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */; }; E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */; }; E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */; }; E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -936,6 +937,7 @@ E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutCache.mm; sourceTree = ""; }; E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutDefines.m; sourceTree = ""; }; E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDefines.h; sourceTree = ""; }; + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASScrollNodeTests.m; sourceTree = ""; }; E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionFlowLayoutDelegate.m; sourceTree = ""; }; E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutContext.h; sourceTree = ""; }; @@ -1219,6 +1221,7 @@ E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, @@ -2177,6 +2180,7 @@ CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, + E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.m in Sources */, CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.m in Sources */, CC583AD91EF9BDC600134156 /* ASDisplayNode+OCMock.m in Sources */, 697B315A1CFE4B410049936F /* ASEditableTextNodeTests.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f6954570..3263e31d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - [Collection/Table] Added direct support for mapping section indexes between data spaces. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/660) - [ASCornerLayoutSpec] New layout spec class for declarative corner element layout. [#657](https://github.com/TextureGroup/Texture/pull/657) [huangkun](https://github.com/huang-kun) - [Layout] Fix an issue that causes a pending layout to be applied multiple times. [Huy Nguyen](https://github.com/nguyenhuy) [#695](https://github.com/TextureGroup/Texture/pull/695) +- [ASScrollNode] Ensure the node respects the given size range while calculating its layout. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) +- [ASScrollNode] Invalidate the node's calculated layout if its scrollable directions changed. Also add unit tests for the class. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASScrollNode.mm b/Source/ASScrollNode.mm index 7d1beaa3d2..3ff0088095 100644 --- a/Source/ASScrollNode.mm +++ b/Source/ASScrollNode.mm @@ -99,10 +99,12 @@ // To understand this code, imagine we're containing a horizontal stack set within a vertical table node. // Our parentSize is fixed ~375pt width, but 0 - INF height. Our stack measures 1000pt width, 50pt height. // In this case, we want our scrollNode.bounds to be 375pt wide, and 50pt high. ContentSize 1000pt, 50pt. - // We can achieve this behavior by: 1. Always set contentSize to layout.size. 2. Set bounds to parentSize, + // We can achieve this behavior by: + // 1. Always set contentSize to layout.size. + // 2. Set bounds to a size that is calculated by clamping parentSize against constrained size, // unless one dimension is not defined, in which case adopt the contentSize for that dimension. _contentCalculatedSizeFromLayout = layout.size; - CGSize selfSize = parentSize; + CGSize selfSize = ASSizeRangeClamp(constrainedSize, parentSize); if (ASPointsValidForLayout(selfSize.width) == NO) { selfSize.width = _contentCalculatedSizeFromLayout.width; } @@ -161,7 +163,10 @@ - (void)setScrollableDirections:(ASScrollDirection)scrollableDirections { ASDN::MutexLocker l(__instanceLock__); - _scrollableDirections = scrollableDirections; + if (_scrollableDirections != scrollableDirections) { + _scrollableDirections = scrollableDirections; + [self setNeedsLayout]; + } } @end diff --git a/Tests/ASScrollNodeTests.m b/Tests/ASScrollNodeTests.m new file mode 100644 index 0000000000..56b3fd731b --- /dev/null +++ b/Tests/ASScrollNodeTests.m @@ -0,0 +1,139 @@ +// +// ASScrollNodeTests.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import "ASXCTExtensions.h" + +@interface ASScrollNodeTests : XCTestCase + +@property (nonatomic) ASScrollNode *scrollNode; +@property (nonatomic) ASDisplayNode *subnode; + +@end + +@implementation ASScrollNodeTests + +- (void)setUp +{ + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + self.subnode = subnode; + + self.scrollNode = [[ASScrollNode alloc] init]; + self.scrollNode.scrollableDirections = ASScrollDirectionVerticalDirections; + self.scrollNode.automaticallyManagesContentSize = YES; + self.scrollNode.automaticallyManagesSubnodes = YES; + self.scrollNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [[ASWrapperLayoutSpec alloc] initWithLayoutElement:subnode]; + }; + [self.scrollNode view]; +} + +- (void)testSubnodeLayoutCalculatedWithUnconstrainedMaxSizeInScrollableDirection +{ + CGSize parentSize = CGSizeMake(100, 100); + ASSizeRange sizeRange = ASSizeRangeMake(parentSize); + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + + ASSizeRange subnodeSizeRange = sizeRange; + subnodeSizeRange.max.height = CGFLOAT_MAX; + XCTAssertEqual(self.scrollNode.scrollableDirections, ASScrollDirectionVerticalDirections); + ASXCTAssertEqualSizeRanges(self.subnode.constrainedSizeForCalculatedLayout, subnodeSizeRange); + + // Same test for horizontal scrollable directions + self.scrollNode.scrollableDirections = ASScrollDirectionHorizontalDirections; + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + + subnodeSizeRange = sizeRange; + subnodeSizeRange.max.width = CGFLOAT_MAX; + + ASXCTAssertEqualSizeRanges(self.subnode.constrainedSizeForCalculatedLayout, subnodeSizeRange); +} + +- (void)testAutomaticallyManagesContentSizeUnderflow +{ + CGSize subnodeSize = CGSizeMake(100, 100); + CGSize parentSize = CGSizeMake(100, 200); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, parentSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeOverflow +{ + CGSize subnodeSize = CGSizeMake(100, 500); + CGSize parentSize = CGSizeMake(100, 200); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, parentSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithSizeRangeSmallerThanParentSize +{ + CGSize subnodeSize = CGSizeMake(100, 100); + CGSize parentSize = CGSizeMake(100, 500); + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(100, 100), CGSizeMake(100, 200)); + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, sizeRange.max); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithSizeRangeBiggerThanParentSize +{ + CGSize subnodeSize = CGSizeMake(100, 200); + CGSize parentSize = CGSizeMake(100, 100); + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(100, 150)); + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, sizeRange.min); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithInvalidCalculatedSizeForLayout +{ + CGSize subnodeSize = CGSizeMake(100, 200); + CGSize parentSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, subnodeSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +@end From 0dc7002f0bd74642aa06be449742729bc2044251 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 1 Dec 2017 09:05:47 -0800 Subject: [PATCH 74/86] Add unit tests for the layout engine (#424) * Build testing platform & tests for the layout engine * Add our license header to debugbreak. * Remove thing * Address review comments * Beef up the logging * Update -[ASLayout isEqual:] * testLayoutTransitionWithAsyncMeasurement passes now * Disable testASetNeedsLayoutInterferingWithTheCurrentTransition * Fix build errors --- AsyncDisplayKit.xcodeproj/project.pbxproj | 18 + CHANGELOG.md | 1 + Source/ASDisplayNode+Layout.mm | 1 + Source/ASDisplayNode.mm | 3 +- Source/Details/_ASDisplayLayer.mm | 2 + Source/Layout/ASLayout.mm | 8 +- .../Private/ASDisplayNode+FrameworkPrivate.h | 23 + Tests/ASLayoutEngineTests.mm | 517 ++++++++++++++++++ Tests/ASLayoutTestNode.h | 42 ++ Tests/ASLayoutTestNode.mm | 92 ++++ Tests/ASTLayoutFixture.h | 61 +++ Tests/ASTLayoutFixture.mm | 134 +++++ Tests/Common/ASTestCase.h | 5 + Tests/Common/OCMockObject+ASAdditions.h | 29 +- Tests/Common/OCMockObject+ASAdditions.m | 102 +++- Tests/Common/debugbreak.h | 146 +++++ 16 files changed, 1176 insertions(+), 8 deletions(-) create mode 100644 Tests/ASLayoutEngineTests.mm create mode 100644 Tests/ASLayoutTestNode.h create mode 100644 Tests/ASLayoutTestNode.mm create mode 100644 Tests/ASTLayoutFixture.h create mode 100644 Tests/ASTLayoutFixture.mm create mode 100644 Tests/Common/debugbreak.h diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 025344d7e0..f8d60fb322 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -405,6 +405,9 @@ CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; }; CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */; }; CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */; }; + CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */; }; + CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */; }; + CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */; }; CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -899,6 +902,12 @@ CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = ""; }; CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = ""; }; CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIntegerMapTests.m; sourceTree = ""; }; + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutEngineTests.mm; sourceTree = ""; }; + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTestNode.h; sourceTree = ""; }; + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTestNode.mm; sourceTree = ""; }; + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = debugbreak.h; sourceTree = ""; }; + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTLayoutFixture.h; sourceTree = ""; }; + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTLayoutFixture.mm; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = ""; }; @@ -1175,6 +1184,11 @@ CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */, + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */, + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */, + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */, + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */, + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */, CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */, CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */, @@ -1573,6 +1587,7 @@ CC583ABF1EF9BAB400134156 /* Common */ = { isa = PBXGroup; children = ( + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */, CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.m */, CC583AC11EF9BAB400134156 /* ASTestCase.h */, CC583AC21EF9BAB400134156 /* ASTestCase.m */, @@ -2187,6 +2202,7 @@ ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */, CC8B05D61D73836400F54286 /* ASPerformanceTestContext.m in Sources */, CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.m in Sources */, + CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */, 69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */, ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */, 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */, @@ -2195,12 +2211,14 @@ 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */, 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */, ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, + CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */, 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.m in Sources */, 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */, AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.m in Sources */, 254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */, 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */, CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */, + CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */, 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */, DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m in Sources */, 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3263e31d2c..430bc73e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - [Layout] Fix an issue that causes a pending layout to be applied multiple times. [Huy Nguyen](https://github.com/nguyenhuy) [#695](https://github.com/TextureGroup/Texture/pull/695) - [ASScrollNode] Ensure the node respects the given size range while calculating its layout. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) - [ASScrollNode] Invalidate the node's calculated layout if its scrollable directions changed. Also add unit tests for the class. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) +- Add new unit testing to the layout engine. [Adlai Holler](https://github.com/Adlai-Holler) [#424](https://github.com/TextureGroup/Texture/pull/424) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 6d291c8300..dd0a2e2adf 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -85,6 +85,7 @@ layout = [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; + as_log_verbose(ASLayoutLog(), "Established pending layout for %@ in %s", self, sel_getName(_cmd)); _pendingDisplayNodeLayout = std::make_shared(layout, constrainedSize, parentSize, version); ASDisplayNodeAssertNotNil(layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newly calculated layout should not be nil! %@", self); } diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index c5bdd88323..95e9276679 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -2762,7 +2762,8 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { } } - ASDisplayNodeLogEvent(self, @"setHierarchyState: oldState = %@, newState = %@", NSStringFromASHierarchyState(oldState), NSStringFromASHierarchyState(newState)); + ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState)); + as_log_verbose(ASNodeLog(), "%s%@ %@", sel_getName(_cmd), NSStringFromASHierarchyStateChange(oldState, newState), self); } - (void)willEnterHierarchy diff --git a/Source/Details/_ASDisplayLayer.mm b/Source/Details/_ASDisplayLayer.mm index 7ceba1c79d..ba8b02ab4f 100644 --- a/Source/Details/_ASDisplayLayer.mm +++ b/Source/Details/_ASDisplayLayer.mm @@ -25,6 +25,7 @@ #import #import #import +#import @implementation _ASDisplayLayer { @@ -93,6 +94,7 @@ - (void)setNeedsLayout { ASDisplayNodeAssertMainThread(); + as_log_verbose(ASNodeLog(), "%s on %@", sel_getName(_cmd), self); [super setNeedsLayout]; } #endif diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 4e8d2f5026..919a520c95 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -23,6 +23,7 @@ #import #import +#import #import #import #import @@ -281,11 +282,12 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( } if (!CGSizeEqualToSize(_size, layout.size)) return NO; - if (!CGPointEqualToPoint(_position, layout.position)) return NO; + + if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position)) + || CGPointEqualToPoint(self.position, layout.position))) return NO; if (_layoutElement != layout.layoutElement) return NO; - NSArray *sublayouts = layout.sublayouts; - if (sublayouts != _sublayouts && (sublayouts == nil || _sublayouts == nil || ![_sublayouts isEqual:sublayouts])) { + if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) { return NO; } diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index d4c18a6e95..dbb5207879 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -99,6 +99,29 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; } +#define HIERARCHY_STATE_DELTA(Name) ({ \ + if ((oldState & ASHierarchyState##Name) != (newState & ASHierarchyState##Name)) { \ + [changes appendFormat:@"%c%s ", (newState & ASHierarchyState##Name ? '+' : '-'), #Name]; \ + } \ +}) + +__unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarchyState oldState, ASHierarchyState newState) +{ + if (oldState == newState) { + return @"{ }"; + } + + NSMutableString *changes = [NSMutableString stringWithString:@"{ "]; + HIERARCHY_STATE_DELTA(Rasterized); + HIERARCHY_STATE_DELTA(RangeManaged); + HIERARCHY_STATE_DELTA(TransitioningSupernodes); + HIERARCHY_STATE_DELTA(LayoutPending); + [changes appendString:@"}"]; + return changes; +} + +#undef HIERARCHY_STATE_DELTA + @interface ASDisplayNode () { @protected diff --git a/Tests/ASLayoutEngineTests.mm b/Tests/ASLayoutEngineTests.mm new file mode 100644 index 0000000000..550dffa477 --- /dev/null +++ b/Tests/ASLayoutEngineTests.mm @@ -0,0 +1,517 @@ +// +// ASLayoutEngineTests.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import "ASLayoutTestNode.h" +#import "ASXCTExtensions.h" +#import "ASTLayoutFixture.h" + +@interface ASLayoutEngineTests : ASTestCase + +@end + +@implementation ASLayoutEngineTests { + ASLayoutTestNode *nodeA; + ASLayoutTestNode *nodeB; + ASLayoutTestNode *nodeC; + ASLayoutTestNode *nodeD; + ASLayoutTestNode *nodeE; + ASTLayoutFixture *fixture1; + ASTLayoutFixture *fixture2; + ASTLayoutFixture *fixture3; + ASTLayoutFixture *fixture4; + + // fixtures 1 and 3 share the same exact node A layout spec block. + // we don't want the infra to call -setNeedsLayout when we switch fixtures + // so we need to use the same exact block. + ASLayoutSpecBlock fixture1and3NodeALayoutSpecBlock; + + UIWindow *window; + UIViewController *vc; + NSArray *allNodes; + NSTimeInterval verifyDelay; + // See -stubCalculatedLayoutDidChange. + BOOL stubbedCalculatedLayoutDidChange; +} + +- (void)setUp +{ + [super setUp]; + verifyDelay = 3; + window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; + vc = [[UIViewController alloc] init]; + nodeA = [ASLayoutTestNode new]; + nodeA.backgroundColor = [UIColor redColor]; + + // NOTE: nodeB has flexShrink, the others don't + nodeB = [ASLayoutTestNode new]; + nodeB.style.flexShrink = 1; + nodeB.backgroundColor = [UIColor orangeColor]; + + nodeC = [ASLayoutTestNode new]; + nodeC.backgroundColor = [UIColor yellowColor]; + nodeD = [ASLayoutTestNode new]; + nodeD.backgroundColor = [UIColor greenColor]; + nodeE = [ASLayoutTestNode new]; + nodeE.backgroundColor = [UIColor blueColor]; + allNodes = @[ nodeA, nodeB, nodeC, nodeD, nodeE ]; + ASSetDebugNames(nodeA, nodeB, nodeC, nodeD, nodeE); + ASLayoutSpecBlock b = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeD ]]; + }; + fixture1and3NodeALayoutSpecBlock = b; + fixture1 = [self createFixture1]; + fixture2 = [self createFixture2]; + fixture3 = [self createFixture3]; + fixture4 = [self createFixture4]; + + nodeA.frame = vc.view.bounds; + nodeA.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [vc.view addSubnode:nodeA]; + + window.rootViewController = vc; + [window makeKeyAndVisible]; +} + +- (void)tearDown +{ + nodeA.layoutSpecBlock = nil; + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + [super tearDown]; +} + +- (void)testFirstLayoutPassWhenInWindow +{ + [self runFirstLayoutPassWithFixture:fixture1]; +} + +- (void)testSetNeedsLayoutAndNormalLayoutPass +{ + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // skip nodeB because its layout doesn't change. + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread(); + }]; + OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread(); + } + + [window layoutIfNeeded]; + [self verifyFixture:fixture2]; +} + +/** + * Transition from fixture1 to Fixture2 on node A. + * + * Expect A and D to calculate once off main, and + * to receive calculatedLayoutDidChange on main, + * then to get the measurement completion call on main, + * then to get animateLayoutTransition: and didCompleteLayoutTransition: on main. + */ +- (void)testLayoutTransitionWithAsyncMeasurement +{ + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // Expect A, C, E to calculate new layouts off-main + // dispatch_once onto main to run our injectedMainThread work while the transition calculates. + __block dispatch_block_t injectedMainThreadWork = nil; + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .offMainThread() + .andDo(^(NSInvocation *inv) { + // On first calculateLayoutThatFits, schedule our injected main thread work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + injectedMainThreadWork(); + }); + }); + }); + }]; + } + + // The code in this section is designed to move in time order, all on the main thread: + + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + + // Trigger the layout transition. + __block dispatch_block_t measurementCompletionBlock = nil; + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{ + measurementCompletionBlock(); + }]; + + // This block will get run after bg layout calculate starts, but before measurementCompletion + __block BOOL injectedMainThreadWorkDone = NO; + injectedMainThreadWork = ^{ + injectedMainThreadWorkDone = YES; + + [window layoutIfNeeded]; + + // Ensure we're still on the old layout. We should stay on this until the transition completes. + [self verifyFixture:fixture1]; + }; + + measurementCompletionBlock = ^{ + XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran."); + }; + + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + + [self verifyFixture:fixture2]; +} + +/** + * Start at fixture 1. + * Trigger an async transition to fixture 2. + * While it's measuring, on main switch to fixture 4 (setNeedsLayout A, D) and run a CA layout pass. + * + * Correct behavior, we end up at fixture 4 since it's newer. + * Current incorrect behavior, we end up at fixture 2 and we remeasure surviving node C. + * Note: incorrect behavior likely introduced by the early check in __layout added in + * https://github.com/facebookarchive/AsyncDisplayKit/pull/2657 + */ +- (void)DISABLE_testASetNeedsLayoutInterferingWithTheCurrentTransition +{ + static BOOL enforceCorrectBehavior = NO; + + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // Expect A, C, E to calculate new layouts off-main + // dispatch_once onto main to run our injectedMainThread work while the transition calculates. + __block dispatch_block_t injectedMainThreadWork = nil; + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .offMainThread() + .andDo(^(NSInvocation *inv) { + // On first calculateLayoutThatFits, schedule our injected main thread work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + injectedMainThreadWork(); + }); + }); + }); + }]; + } + + // The code in this section is designed to move in time order, all on the main thread: + + // With the current behavior, the transition will continue and complete. + if (!enforceCorrectBehavior) { + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + } + + // Trigger the layout transition. + __block dispatch_block_t measurementCompletionBlock = nil; + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{ + measurementCompletionBlock(); + }]; + + // Injected block will get run on main after bg layout calculate starts, but before measurementCompletion + __block BOOL injectedMainThreadWorkDone = NO; + injectedMainThreadWork = ^{ + as_log_verbose(OS_LOG_DEFAULT, "Begin injectedMainThreadWork"); + injectedMainThreadWorkDone = YES; + + [fixture4 apply]; + as_log_verbose(OS_LOG_DEFAULT, "Did apply new fixture"); + + if (enforceCorrectBehavior) { + // Correct measurement behavior here is unclear, may depend on whether the layouts which + // are common to both fixture2 and fixture4 are available from the cache. + } else { + // Incorrect behavior: nodeC will get measured against its new bounds on main. + auto cPendingSize = [fixture2 layoutForNode:nodeC].size; + OCMExpect([nodeC.mock calculateLayoutThatFits:ASSizeRangeMake(cPendingSize)]).onMainThread(); + } + [window layoutIfNeeded]; + as_log_verbose(OS_LOG_DEFAULT, "End injectedMainThreadWork"); + }; + + measurementCompletionBlock = ^{ + XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran."); + }; + + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + + // Incorrect behavior: The transition will "win" even though its transitioning to stale data. + if (enforceCorrectBehavior) { + [self verifyFixture:fixture4]; + } else { + [self verifyFixture:fixture2]; + } +} + +/** + * Start on fixture 3 where nodeB is force-shrunk via multipass layout. + * Apply fixture 1, which just changes nodeB's size and calls -setNeedsLayout on it. + * + * This behavior is currently broken. See implementation for correct behavior and incorrect behavior. + */ +- (void)testCallingSetNeedsLayoutOnANodeThatWasSubjectToMultipassLayout +{ + static BOOL const enforceCorrectBehavior = NO; + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture3]; + + // Switch to fixture 1, updating nodeB's desired size and calling -setNeedsLayout + // Now nodeB will fit happily into the stack. + [fixture1 apply]; + + if (enforceCorrectBehavior) { + /* + * Correct behavior: nodeB is remeasured against the first (unconstrained) size + * and when it's discovered that now nodeB fits, nodeA will re-layout and we'll + * end up correctly at fixture1. + */ + OCMExpect([nodeB.mock calculateLayoutThatFits:[fixture3 firstSizeRangeForNode:nodeB]]); + + [fixture1 withSizeRangesForNode:nodeA block:^(ASSizeRange sizeRange) { + OCMExpect([nodeA.mock calculateLayoutThatFits:sizeRange]); + }]; + + [window layoutIfNeeded]; + [self verifyFixture:fixture1]; + } else { + /* + * Incorrect behavior: nodeB is remeasured against the second (fixed-width) constraint. + * The returned value (8) is clamped to the fixed with (7), and then compared to the previous + * width (7) and we decide not to propagate up the invalidation, and we stay stuck on the old + * layout (fixture3). + */ + OCMExpect([nodeB.mock calculateLayoutThatFits:nodeB.constrainedSizeForCalculatedLayout]); + [window layoutIfNeeded]; + [self verifyFixture:fixture3]; + } +} + +#pragma mark - Helpers + +- (void)verifyFixture:(ASTLayoutFixture *)fixture +{ + auto expected = fixture.layout; + + // Ensure expected == frames + auto frames = [fixture.rootNode currentLayoutBasedOnFrames]; + if (![expected isEqual:frames]) { + XCTFail(@"\n*** Layout verification failed – frames don't match expected. ***\nGot:\n%@\nExpected:\n%@", [frames recursiveDescription], [expected recursiveDescription]); + } + + // Ensure expected == calculatedLayout + auto calculated = fixture.rootNode.calculatedLayout; + if (![expected isEqual:calculated]) { + XCTFail(@"\n*** Layout verification failed – calculated layout doesn't match expected. ***\nGot:\n%@\nExpected:\n%@", [calculated recursiveDescription], [expected recursiveDescription]); + } +} + +/** + * Stubs calculatedLayoutDidChange for all nodes. + * + * It's not really a core layout engine method, and it's also + * currently bugged and gets called a lot so for most + * tests its better not to have expectations about it littered around. + * https://github.com/TextureGroup/Texture/issues/422 + */ +- (void)stubCalculatedLayoutDidChange +{ + stubbedCalculatedLayoutDidChange = YES; + for (ASLayoutTestNode *node in allNodes) { + OCMStub([node.mock calculatedLayoutDidChange]); + } +} + +/** + * Fixture 1: A basic horizontal stack, all single-pass. + * + * [A: HorizStack([B, C, D])]. B is (1x1), C is (2x1), D is (1x1) + */ +- (ASTLayoutFixture *)createFixture1 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +/** + * Fixture 2: A simple transition away from fixture 1. + * + * [A: HorizStack([B, C, E])]. B is (1x1), C is (4x1), E is (1x1) + * + * From fixture 1: + * B survives with same layout + * C survives with new layout + * D is removed + * E joins with first layout + */ +- (ASTLayoutFixture *)createFixture2 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{4,1} position:{3,0} sublayouts:nil]; + + // nodeE + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE]; + auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutE ]]; + fixture.layout = layoutA; + + ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeE ]]; + }; + [fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA]; + return fixture; +} + +/** + * Fixture 3: Multipass stack layout + * + * [A: HorizStack([B, C, D])]. B is (7x1), C is (2x1), D is (1x1) + * + * nodeB (which has flexShrink=1) will return 8x1 for its size during the first + * stack pass, and it'll be subject to a second pass where it returns 7x1. + * + */ +- (ASTLayoutFixture *)createFixture3 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB wants 8,1 but it will settle for 7,1 + [fixture setReturnedSize:{8,1} forNode:nodeB]; + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + [fixture addSizeRange:{{7, 0}, {7, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{7,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{7,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +/** + * Fixture 4: A different simple transition away from fixture 1. + * + * [A: HorizStack([B, D, E])]. B is (1x1), D is (2x1), E is (1x1) + * + * From fixture 1: + * B survives with same layout + * C is removed + * D survives with new layout + * E joins with first layout + * + * From fixture 2: + * B survives with same layout + * C is removed + * D joins with first layout + * E survives with same layout + */ +- (ASTLayoutFixture *)createFixture4 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeE + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE]; + auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutD, layoutE ]]; + fixture.layout = layoutA; + + ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeD, nodeE ]]; + }; + [fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA]; + return fixture; +} + +- (void)runFirstLayoutPassWithFixture:(ASTLayoutFixture *)fixture +{ + [fixture apply]; + for (ASLayoutTestNode *node in fixture.allNodes) { + [fixture withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread(); + }]; + + if (!stubbedCalculatedLayoutDidChange) { + OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread(); + } + } + + // Trigger CA layout pass. + [window layoutIfNeeded]; + + // Make sure it went through. + [self verifyFixture:fixture]; +} + +@end diff --git a/Tests/ASLayoutTestNode.h b/Tests/ASLayoutTestNode.h new file mode 100644 index 0000000000..66fafee149 --- /dev/null +++ b/Tests/ASLayoutTestNode.h @@ -0,0 +1,42 @@ +// +// ASLayoutTestNode.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 +#import + +@interface ASLayoutTestNode : ASDisplayNode + +/** + * Mocking ASDisplayNodes directly isn't very safe because when you pump mock objects + * into the guts of the framework, bad things happen e.g. direct-ivar-access on mock + * objects will return garbage data. + * + * Instead we create a strict mock for each node, and forward a selected set of calls to it. + */ +@property (nonatomic, strong, readonly) id mock; + +/** + * The size that this node will return in calculateLayoutThatFits (if it doesn't have a layoutSpecBlock). + * + * Changing this value will call -setNeedsLayout on the node. + */ +@property (nonatomic) CGSize testSize; + +/** + * Generate a layout based on the frame of this node and its subtree. + * + * The root layout will be unpositioned. This is so that the returned layout can be directly + * compared to `calculatedLayout` + */ +- (ASLayout *)currentLayoutBasedOnFrames; + +@end diff --git a/Tests/ASLayoutTestNode.mm b/Tests/ASLayoutTestNode.mm new file mode 100644 index 0000000000..3a112422a7 --- /dev/null +++ b/Tests/ASLayoutTestNode.mm @@ -0,0 +1,92 @@ +// +// ASLayoutTestNode.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASLayoutTestNode.h" +#import +#import "OCMockObject+ASAdditions.h" + +@implementation ASLayoutTestNode + +- (instancetype)init +{ + if (self = [super init]) { + _mock = OCMStrictClassMock([ASDisplayNode class]); + + // If errors occur (e.g. unexpected method) we need to quickly figure out + // which node is at fault, so we inject the node name into the mock instance + // description. + __weak __typeof(self) weakSelf = self; + [_mock setModifyDescriptionBlock:^(id mock, NSString *baseDescription){ + return [NSString stringWithFormat:@"Mock(%@)", weakSelf.description]; + }]; + } + return self; +} + +- (ASLayout *)currentLayoutBasedOnFrames +{ + return [self _currentLayoutBasedOnFramesForRootNode:YES]; +} + +- (ASLayout *)_currentLayoutBasedOnFramesForRootNode:(BOOL)isRootNode +{ + auto sublayouts = [NSMutableArray array]; + for (ASLayoutTestNode *subnode in self.subnodes) { + [sublayouts addObject:[subnode _currentLayoutBasedOnFramesForRootNode:NO]]; + } + CGPoint rootPosition = isRootNode ? ASPointNull : self.frame.origin; + return [ASLayout layoutWithLayoutElement:self size:self.frame.size position:rootPosition sublayouts:sublayouts]; +} + +- (void)setTestSize:(CGSize)testSize +{ + if (!CGSizeEqualToSize(testSize, _testSize)) { + _testSize = testSize; + [self setNeedsLayout]; + } +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + [_mock calculateLayoutThatFits:constrainedSize]; + + // If we have a layout spec block, or no test size, return super. + if (self.layoutSpecBlock || CGSizeEqualToSize(self.testSize, CGSizeZero)) { + return [super calculateLayoutThatFits:constrainedSize]; + } else { + // Interestingly, the infra will auto-clamp sizes from calculateSizeThatFits, but not from calculateLayoutThatFits. + auto size = ASSizeRangeClamp(constrainedSize, self.testSize); + return [ASLayout layoutWithLayoutElement:self size:size]; + } +} + +#pragma mark - Forwarding to mock + +- (void)calculatedLayoutDidChange +{ + [_mock calculatedLayoutDidChange]; + [super calculatedLayoutDidChange]; +} + +- (void)didCompleteLayoutTransition:(id)context +{ + [_mock didCompleteLayoutTransition:context]; + [super didCompleteLayoutTransition:context]; +} + +- (void)animateLayoutTransition:(id)context +{ + [_mock animateLayoutTransition:context]; + [super animateLayoutTransition:context]; +} + +@end diff --git a/Tests/ASTLayoutFixture.h b/Tests/ASTLayoutFixture.h new file mode 100644 index 0000000000..ef590220a4 --- /dev/null +++ b/Tests/ASTLayoutFixture.h @@ -0,0 +1,61 @@ +// +// ASTLayoutFixture.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 +#import +#import "ASTestCase.h" +#import "ASLayoutTestNode.h" + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTLayoutFixture : NSObject + +/// The correct layout. The root should be unpositioned (same as -calculatedLayout). +@property (nonatomic, strong, nullable) ASLayout *layout; + +/// The layoutSpecBlocks for non-leaf nodes. +@property (nonatomic, strong, readonly) NSMapTable *layoutSpecBlocks; + +@property (nonatomic, strong, readonly) ASLayoutTestNode *rootNode; + +@property (nonatomic, strong, readonly) NSSet *allNodes; + +/// Get the (correct) layout for the specified node. +- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node; + +/// Add this to the list of expected size ranges for the given node. +- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node; + +/// If you have a node that wants a size different than it gets, set it here. +/// For any leaf nodes that you don't call this on, the node will return the correct size +/// based on the fixture's layout. This is useful for triggering multipass stack layout. +- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node; + +/// Get the first expected size range for the node. +- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node; + +/// Enumerate all the size ranges for the node. +- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange sizeRange))block; + +/// Configure the nodes for this fixture. Set testSize on leaf nodes, layoutSpecBlock on container nodes. +- (void)apply; + +@end + +@interface ASLayout (TestHelpers) + +@property (nonatomic, readonly) NSArray *allNodes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/ASTLayoutFixture.mm b/Tests/ASTLayoutFixture.mm new file mode 100644 index 0000000000..bdddbe5bf2 --- /dev/null +++ b/Tests/ASTLayoutFixture.mm @@ -0,0 +1,134 @@ +// +// ASTLayoutFixture.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTLayoutFixture.h" + +@interface ASTLayoutFixture () + +/// The size ranges against which nodes are expected to be measured. +@property (nonatomic, strong, readonly) NSMapTable *> *sizeRanges; + +/// The overridden returned sizes for nodes where you want to trigger multipass layout. +@property (nonatomic, strong, readonly) NSMapTable *returnedSizes; + +@end + +@implementation ASTLayoutFixture + +- (instancetype)init +{ + if (self = [super init]) { + _sizeRanges = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + _layoutSpecBlocks = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + _returnedSizes = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + + } + return self; +} + +- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node +{ + auto ranges = [_sizeRanges objectForKey:node]; + if (ranges == nil) { + ranges = [NSMutableArray array]; + [_sizeRanges setObject:ranges forKey:node]; + } + [ranges addObject:[NSValue valueWithBytes:&sizeRange objCType:@encode(ASSizeRange)]]; +} + +- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node +{ + [_returnedSizes setObject:[NSValue valueWithCGSize:size] forKey:node]; +} + +- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node +{ + auto val = [_sizeRanges objectForKey:node].firstObject; + ASSizeRange r; + [val getValue:&r]; + return r; +} + +- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange))block +{ + for (NSValue *value in [_sizeRanges objectForKey:node]) { + ASSizeRange r; + [value getValue:&r]; + block(r); + } +} + +- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node +{ + NSMutableArray *allLayouts = [NSMutableArray array]; + [ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts]; + for (ASLayout *layout in allLayouts) { + if (layout.layoutElement == node) { + return layout; + } + } + return nil; +} + +/// A very dumb tree iteration approach. NSEnumerator or something would be way better. ++ (void)collectAllLayoutsFromLayout:(ASLayout *)layout array:(NSMutableArray *)array +{ + [array addObject:layout]; + for (ASLayout *sublayout in layout.sublayouts) { + [self collectAllLayoutsFromLayout:sublayout array:array]; + } +} + +- (ASLayoutTestNode *)rootNode +{ + return (ASLayoutTestNode *)self.layout.layoutElement; +} + +- (NSSet *)allNodes +{ + auto allLayouts = [NSMutableArray array]; + [ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts]; + return [NSSet setWithArray:[allLayouts valueForKey:@"layoutElement"]]; +} + +- (void)apply +{ + // Update layoutSpecBlock for parent nodes, set automatic subnode management + for (ASDisplayNode *node in _layoutSpecBlocks) { + auto block = [_layoutSpecBlocks objectForKey:node]; + if (node.layoutSpecBlock != block) { + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = block; + [node setNeedsLayout]; + } + } + + [self setTestSizesOfLeafNodesInLayout:self.layout]; +} + +/// Go through the given layout, and for all the leaf nodes, set their preferredSize +/// to the layout size if needed, then call -setNeedsLayout +- (void)setTestSizesOfLeafNodesInLayout:(ASLayout *)layout +{ + auto node = (ASLayoutTestNode *)layout.layoutElement; + if (layout.sublayouts.count == 0) { + auto override = [self.returnedSizes objectForKey:node]; + node.testSize = override ? override.CGSizeValue : layout.size; + } else { + node.testSize = CGSizeZero; + for (ASLayout *sublayout in layout.sublayouts) { + [self setTestSizesOfLeafNodesInLayout:sublayout]; + } + } +} + +@end diff --git a/Tests/Common/ASTestCase.h b/Tests/Common/ASTestCase.h index 4868f64d05..54231b64e6 100644 --- a/Tests/Common/ASTestCase.h +++ b/Tests/Common/ASTestCase.h @@ -12,6 +12,11 @@ #import +// Not strictly necessary, but convenient +#import +#import +#import "OCMockObject+ASAdditions.h" + NS_ASSUME_NONNULL_BEGIN @interface ASTestCase : XCTestCase diff --git a/Tests/Common/OCMockObject+ASAdditions.h b/Tests/Common/OCMockObject+ASAdditions.h index a8e7004915..f8617411bb 100644 --- a/Tests/Common/OCMockObject+ASAdditions.h +++ b/Tests/Common/OCMockObject+ASAdditions.h @@ -10,7 +10,7 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import @interface OCMockObject (ASAdditions) @@ -30,4 +30,31 @@ */ - (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... NS_REQUIRES_NIL_TERMINATION; +/// An optional block to modify description text. Only used in OCClassMockObject currently. +@property (atomic) NSString *(^modifyDescriptionBlock)(OCMockObject *object, NSString *baseDescription); + +@end + +/** + * Additional stub recorders useful in ASDK. + */ +@interface OCMStubRecorder (ASProperties) + +/** + * Add a debug-break side effect to this stub/expectation. + * + * You will usually need to jump to frame 12 "fr s 12" + */ +#define andDebugBreak() _andDebugBreak() +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDebugBreak)(void); + +#define ignoringNonObjectArgs() _ignoringNonObjectArgs() +@property (nonatomic, readonly) OCMStubRecorder *(^ _ignoringNonObjectArgs)(void); + +#define onMainThread() _onMainThread() +@property (nonatomic, readonly) OCMStubRecorder *(^ _onMainThread)(void); + +#define offMainThread() _offMainThread() +@property (nonatomic, readonly) OCMStubRecorder *(^ _offMainThread)(void); + @end diff --git a/Tests/Common/OCMockObject+ASAdditions.m b/Tests/Common/OCMockObject+ASAdditions.m index 86dcdbf9d8..50fabd5eb6 100644 --- a/Tests/Common/OCMockObject+ASAdditions.m +++ b/Tests/Common/OCMockObject+ASAdditions.m @@ -15,6 +15,8 @@ #import #import #import "ASTestCase.h" +#import +#import "debugbreak.h" @interface ASTestCase (OCMockObjectRegistering) @@ -32,9 +34,18 @@ method_exchangeImplementations(orig, new); // init <-> swizzled_init - Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init)); - Method newInit = class_getInstanceMethod(self, @selector(swizzled_init)); - method_exchangeImplementations(origInit, newInit); + { + Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init)); + Method newInit = class_getInstanceMethod(self, @selector(swizzled_init)); + method_exchangeImplementations(origInit, newInit); + } + + // (class mock) description <-> swizzled_classMockDescription + { + Method orig = class_getInstanceMethod(OCMockObject.classMockObjectClass, @selector(description)); + Method new = class_getInstanceMethod(self, @selector(swizzled_classMockDescription)); + method_exchangeImplementations(orig, new); + } } /// Since OCProtocolMockObject is private, use this method to get the class. @@ -49,6 +60,18 @@ return c; } +/// Since OCClassMockObject is private, use this method to get the class. ++ (Class)classMockObjectClass +{ + static Class c; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + c = NSClassFromString(@"OCClassMockObject"); + NSAssert(c != Nil, nil); + }); + return c; +} + /// Whether the user has opted-in to specify which optional methods are implemented for this object. - (BOOL)hasSpecifiedOptionalProtocolMethods { @@ -142,4 +165,77 @@ return self; } +- (NSString *)swizzled_classMockDescription +{ + NSString *orig = [self swizzled_classMockDescription]; + __auto_type block = self.modifyDescriptionBlock; + if (block) { + return block(self, orig); + } + return orig; +} + +- (void)setModifyDescriptionBlock:(NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock +{ + objc_setAssociatedObject(self, @selector(modifyDescriptionBlock), modifyDescriptionBlock, OBJC_ASSOCIATION_COPY); +} + +- (NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock +{ + return objc_getAssociatedObject(self, _cmd); +} + +@end + +@implementation OCMStubRecorder (ASProperties) + +@dynamic _ignoringNonObjectArgs; + +- (OCMStubRecorder *(^)(void))_ignoringNonObjectArgs +{ + id (^theBlock)(void) = ^ () + { + return [self ignoringNonObjectArgs]; + }; + return theBlock; +} + +@dynamic _onMainThread; + +- (OCMStubRecorder *(^)(void))_onMainThread +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + ASDisplayNodeAssertMainThread(); + }]; + }; + return theBlock; +} + +@dynamic _offMainThread; + +- (OCMStubRecorder *(^)(void))_offMainThread +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + ASDisplayNodeAssertNotMainThread(); + }]; + }; + return theBlock; +} + +@dynamic _andDebugBreak; + +- (OCMStubRecorder *(^)(void))_andDebugBreak +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + debug_break(); + }]; + }; + return theBlock; +} @end diff --git a/Tests/Common/debugbreak.h b/Tests/Common/debugbreak.h new file mode 100644 index 0000000000..5405e40de7 --- /dev/null +++ b/Tests/Common/debugbreak.h @@ -0,0 +1,146 @@ +// +// debugbreak.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 +// + +/* Copyright (c) 2011-2015, Scott Tsai + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef DEBUG_BREAK_H +#define DEBUG_BREAK_H + +#ifdef _MSC_VER + +#define debug_break __debugbreak + +#else + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + /* gcc optimizers consider code after __builtin_trap() dead. + * Making __builtin_trap() unsuitable for breaking into the debugger */ + DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP = 0, +}; + +#if defined(__i386__) || defined(__x86_64__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + __asm__ volatile("int $0x03"); +} +#elif defined(__thumb__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +/* FIXME: handle __THUMB_INTERWORK__ */ +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'arm-linux-tdep.c' in GDB source. + * Both instruction sequences below work. */ +#if 1 + /* 'eabi_linux_thumb_le_breakpoint' */ + __asm__ volatile(".inst 0xde01"); +#else + /* 'eabi_linux_thumb2_le_breakpoint' */ + __asm__ volatile(".inst.w 0xf7f0a000"); +#endif + + /* Known problem: + * After a breakpoint hit, can't stepi, step, or continue in GDB. + * 'step' stuck on the same instruction. + * + * Workaround: a new GDB command, + * 'debugbreak-step' is defined in debugbreak-gdb.py + * that does: + * (gdb) set $instruction_len = 2 + * (gdb) tbreak *($pc + $instruction_len) + * (gdb) jump *($pc + $instruction_len) + */ +} +#elif defined(__arm__) && !defined(__thumb__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'arm-linux-tdep.c' in GDB source, + * 'eabi_linux_arm_le_breakpoint' */ + __asm__ volatile(".inst 0xe7f001f0"); + /* Has same known problem and workaround + * as Thumb mode */ +} +#elif defined(__aarch64__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'aarch64-tdep.c' in GDB source, + * 'aarch64_default_breakpoint' */ + __asm__ volatile(".inst 0xd4200000"); +} +#else +enum { HAVE_TRAP_INSTRUCTION = 0, }; +#endif + +__attribute__((gnu_inline, always_inline)) +__inline__ static void debug_break(void) +{ + if (HAVE_TRAP_INSTRUCTION) { + trap_instruction(); + } else if (DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP) { + /* raises SIGILL on Linux x86{,-64}, to continue in gdb: + * (gdb) handle SIGILL stop nopass + * */ + __builtin_trap(); + } else { + #ifdef _WIN32 + /* SIGTRAP available only on POSIX-compliant operating systems + * use builtin trap instead */ + __builtin_trap(); + #else + raise(SIGTRAP); + #endif + } +} + +#ifdef __cplusplus +} +#endif + +#endif + +#endif From c1f517a7eb9b0407b409995cfd63104da1412d16 Mon Sep 17 00:00:00 2001 From: Andrew Yates Date: Mon, 4 Dec 2017 10:41:00 -0800 Subject: [PATCH 75/86] Correct Synchronous Concurrency Talk Link (#698) Fixes #697. --- docs/_docs/synchronous-concurrency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/synchronous-concurrency.md b/docs/_docs/synchronous-concurrency.md index 68e9aa4575..d4430b7516 100755 --- a/docs/_docs/synchronous-concurrency.md +++ b/docs/_docs/synchronous-concurrency.md @@ -12,7 +12,7 @@ By setting this property to YES, the main thread will be blocked until display h Using this option does not eliminate all of the performance advantages of Texture. Normally, a given node has been preloading and is almost done when it reaches the screen, so the blocking time is very short. Even if the rangeTuningParameters are set to 0 this option outperforms UIKit. While the main thread is waiting, all subnode display executes concurrently, thus synchronous concurrency. -See the NSSpain 2015 talk video for a visual walkthrough of this behavior. +See the NSSpain 2015 talk video for a visual walkthrough of this behavior.
SwiftObjective-C From 0b6d41f8726c80653c36ea41bf6dd55d2a56efd3 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Mon, 4 Dec 2017 14:56:04 -0800 Subject: [PATCH 76/86] A couple performance tweaks for animated images #trivial (#634) * A couple performance tweaks for animated images * @nguyenhuy's comments * Avoid calling animatedImageData twice. Thanks @maicki. * Fix call to background deallocation * Good catch by @Adlai-Holler --- Source/ASImageNode+AnimatedImage.mm | 6 ++ Source/ASNetworkImageNode.mm | 104 ++++++++++++++++------------ 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 355ab472ef..246ee49cc2 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -55,6 +55,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } id previousAnimatedImage = _animatedImage; + _animatedImage = animatedImage; if (animatedImage != nil) { @@ -80,6 +81,11 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } [self animatedImageSet:_animatedImage previousAnimatedImage:previousAnimatedImage]; + + // Animated image can take while to dealloc, do it off the main queue + if (previousAnimatedImage != nil) { + ASPerformBackgroundDeallocation(&previousAnimatedImage); + } } - (void)animatedImageSet:(id )newAnimatedImage previousAnimatedImage:(id )previousAnimatedImage diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 0a7ee0fbde..6b4a40a891 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -713,55 +713,67 @@ } else { __weak __typeof__(self) weakSelf = self; auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSource imageSource) { - - __typeof__(self) strongSelf = weakSelf; - if (strongSelf == nil) { - return; - } - - as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); - - // Grab the lock for the rest of the block - ASDN::MutexLocker l(strongSelf->__instanceLock__); - - //Getting a result back for a different download identifier, download must not have been successfully canceled - if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { - return; - } + ASPerformBlockOnBackgroundThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } - //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) - if (ASInterfaceStateIncludesPreload(self->_interfaceState) == NO) { - return; - } - - if (imageContainer != nil) { - [strongSelf _locked_setCurrentImageQuality:1.0]; - if ([imageContainer asdk_animatedImageData] && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) { - id animatedImage = [strongSelf->_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; - [strongSelf _locked_setAnimatedImage:animatedImage]; - } else { - [strongSelf _locked__setImage:[imageContainer asdk_image]]; + as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); + + // Grab the lock for the rest of the block + ASDN::MutexLocker l(strongSelf->__instanceLock__); + + //Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; } - strongSelf->_imageLoaded = YES; - } - - strongSelf->_downloadIdentifier = nil; - strongSelf->_cacheUUID = nil; - - if (imageContainer != nil) { - if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) { - ASDN::MutexUnlocker u(strongSelf->__instanceLock__); - ASNetworkImageNodeDidLoadInfo info = {}; - info.imageSource = imageSource; - [delegate imageNode:strongSelf didLoadImage:strongSelf.image info:info]; - } else if (strongSelf->_delegateFlags.delegateDidLoadImage) { - ASDN::MutexUnlocker u(strongSelf->__instanceLock__); - [delegate imageNode:strongSelf didLoadImage:strongSelf.image]; + + //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) + if (ASInterfaceStateIncludesPreload(self->_interfaceState) == NO) { + return; } - } else if (error && strongSelf->_delegateFlags.delegateDidFailWithError) { - ASDN::MutexUnlocker u(strongSelf->__instanceLock__); - [delegate imageNode:strongSelf didFailWithError:error]; - } + + if (imageContainer != nil) { + [strongSelf _locked_setCurrentImageQuality:1.0]; + NSData *animatedImageData = [imageContainer asdk_animatedImageData]; + if (animatedImageData && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) { + id animatedImage = [strongSelf->_downloader animatedImageWithData:animatedImageData]; + [strongSelf _locked_setAnimatedImage:animatedImage]; + } else { + [strongSelf _locked__setImage:[imageContainer asdk_image]]; + } + strongSelf->_imageLoaded = YES; + } + + strongSelf->_downloadIdentifier = nil; + strongSelf->_cacheUUID = nil; + + ASPerformBlockOnMainThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + // Grab the lock for the rest of the block + ASDN::MutexLocker l(strongSelf->__instanceLock__); + + if (imageContainer != nil) { + if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = imageSource; + [delegate imageNode:strongSelf didLoadImage:strongSelf.image info:info]; + } else if (strongSelf->_delegateFlags.delegateDidLoadImage) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + [delegate imageNode:strongSelf didLoadImage:strongSelf.image]; + } + } else if (error && strongSelf->_delegateFlags.delegateDidFailWithError) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + [delegate imageNode:strongSelf didFailWithError:error]; + } + }); + }); }; // As the _cache and _downloader is only set once in the intializer we don't have to use a From 008a1ce2084717f627e758988b565e2008c28452 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Tue, 5 Dec 2017 13:55:19 -0800 Subject: [PATCH 77/86] Revert Adds support for specifying a quality indexed array of URLs (#699) * Revert "Adds support for specifying a quality indexed array of URLs (#557)" This reverts commit 3c77d4a5da44c46c7b80b2a627c95389b7d6352d. * Add CHANGELOG entry --- CHANGELOG.md | 1 + Source/ASNetworkImageNode.h | 17 +- Source/ASNetworkImageNode.mm | 180 ++++++++------------ Source/Details/ASImageProtocols.h | 30 +--- Source/Details/ASPINRemoteImageDownloader.m | 39 +---- 5 files changed, 87 insertions(+), 180 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430bc73e74..ab351bffeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASNetworkImageNode] Deprecates .URLs in favor of .URL [Garrett Moon](https://github.com/garrettmoon) [#699](https://github.com/TextureGroup/Texture/pull/699) - [iOS11] Update project settings and fix errors [Eke](https://github.com/Eke) [#676](https://github.com/TextureGroup/Texture/pull/676) - [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index 455806cd27..c4c83214b6 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -83,13 +83,16 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, strong, readwrite) NSURL *URL; /** - * An array of URLs of increasing cost to download. - * - * @discussion By setting an array of URLs, the image property of this node will be managed internally. This means previously - * directly set images to the image property will be cleared out and replaced by the placeholder () image - * while loading and the final image after the new image data was downloaded and processed. - */ -@property (nullable, nonatomic, strong, readwrite) NSArray *URLs; + * An array of URLs of increasing cost to download. + * + * @discussion By setting an array of URLs, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. + * + * @deprecated This API has been removed for now due to the increased complexity to the class that it brought. + * Please use .URL instead. + */ +@property (nullable, nonatomic, strong, readwrite) NSArray *URLs ASDISPLAYNODE_DEPRECATED_MSG("Please use URL instead."); /** * Download and display a new image. diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 6b4a40a891..d844b66466 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -37,7 +37,7 @@ // Only access any of these with __instanceLock__. __weak id _delegate; - NSArray *_URLs; + NSURL *_URL; UIImage *_defaultImage; NSUUID *_cacheUUID; @@ -67,7 +67,6 @@ unsigned int downloaderImplementsSetPriority:1; unsigned int downloaderImplementsAnimatedImage:1; unsigned int downloaderImplementsCancelWithResume:1; - unsigned int downloaderImplementsDownloadURLs:1; } _downloaderFlags; // Immutable and set on init only. We don't need to lock in this case. @@ -75,7 +74,6 @@ struct { unsigned int cacheSupportsClearing:1; unsigned int cacheSupportsSynchronousFetch:1; - unsigned int cacheSupportsCachedURLs:1; } _cacheFlags; } @@ -97,11 +95,9 @@ _downloaderFlags.downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)]; _downloaderFlags.downloaderImplementsAnimatedImage = [downloader respondsToSelector:@selector(animatedImageWithData:)]; _downloaderFlags.downloaderImplementsCancelWithResume = [downloader respondsToSelector:@selector(cancelImageDownloadWithResumePossibilityForIdentifier:)]; - _downloaderFlags.downloaderImplementsDownloadURLs = [downloader respondsToSelector:@selector(downloadImageWithURLs:callbackQueue:downloadProgress:completion:)]; _cacheFlags.cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; _cacheFlags.cacheSupportsSynchronousFetch = [cache respondsToSelector:@selector(synchronouslyFetchedCachedImageWithURL:)]; - _cacheFlags.cacheSupportsCachedURLs = [cache respondsToSelector:@selector(cachedImageWithURLs:callbackQueue:completion:)]; _shouldCacheImage = YES; _shouldRenderProgressImages = YES; @@ -139,8 +135,8 @@ BOOL shouldCancelAndClear = imageWasSetExternally && (imageWasSetExternally != _imageWasSetExternally); _imageWasSetExternally = imageWasSetExternally; if (shouldCancelAndClear) { - ASDisplayNodeAssert(_URLs == nil || _URLs.count == 0, @"Directly setting an image on an ASNetworkImageNode causes it to behave like an ASImageNode instead of an ASNetworkImageNode. If this is what you want, set the URL to nil first."); - _URLs = nil; + ASDisplayNodeAssertNil(_URL, @"Directly setting an image on an ASNetworkImageNode causes it to behave like an ASImageNode instead of an ASNetworkImageNode. If this is what you want, set the URL to nil first."); + _URL = nil; [self _locked_cancelDownloadAndClearImageWithResumePossibility:NO]; } @@ -159,40 +155,29 @@ [super _locked_setImage:image]; } +// Deprecated +- (void)setURLs:(NSArray *)URLs +{ + [self setURL:[URLs firstObject]]; +} + +// Deprecated +- (NSArray *)URLs +{ + return @[self.URL]; +} + - (void)setURL:(NSURL *)URL { - if (URL) { - [self setURLs:@[URL]]; - } else { - [self setURLs:nil]; - } + [self setURL:URL resetToDefault:YES]; } - (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset -{ - if (URL) { - [self setURLs:@[URL] resetToDefault:reset]; - } else { - [self setURLs:nil resetToDefault:reset]; - } -} - -- (NSURL *)URL -{ - return [self.URLs lastObject]; -} - -- (void)setURLs:(NSArray *)URLs -{ - [self setURLs:URLs resetToDefault:YES]; -} - -- (void)setURLs:(NSArray *)URLs resetToDefault:(BOOL)reset { { ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(URLs, _URLs)) { + if (ASObjectIsEqual(URL, _URL)) { return; } @@ -201,25 +186,25 @@ _imageWasSetExternally = NO; [self _locked_cancelImageDownloadWithResumePossibility:NO]; - + _imageLoaded = NO; - _URLs = URLs; + _URL = URL; - BOOL hasURL = (_URLs.count == 0); + BOOL hasURL = (_URL == nil); if (reset || hasURL) { [self _locked_setCurrentImageQuality:(hasURL ? 0.0 : 1.0)]; [self _locked__setImage:_defaultImage]; } } - + [self setNeedsPreload]; } -- (NSArray *)URLs +- (NSURL *)URL { ASDN::MutexLocker l(__instanceLock__); - return _URLs; + return _URL; } - (void)setDefaultImage:(UIImage *)defaultImage @@ -238,7 +223,7 @@ _defaultImage = defaultImage; if (!_imageLoaded) { - [self _locked_setCurrentImageQuality:((_URLs.count == 0) ? 0.0 : 1.0)]; + [self _locked_setCurrentImageQuality:((_URL == nil) ? 0.0 : 1.0)]; [self _locked__setImage:defaultImage]; } @@ -337,7 +322,7 @@ - (BOOL)placeholderShouldPersist { ASDN::MutexLocker l(__instanceLock__); - return (self.image == nil && self.animatedImage == nil && _URLs.count != 0); + return (self.image == nil && self.animatedImage == nil && _URL != nil); } /* displayWillStartAsynchronously: in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary @@ -349,25 +334,22 @@ if (asynchronously == NO && _cacheFlags.cacheSupportsSynchronousFetch) { ASDN::MutexLocker l(__instanceLock__); - if (_imageLoaded == NO && _URLs.count > 0 && _downloadIdentifier == nil) { - for (NSURL *url in [_URLs reverseObjectEnumerator]) { - UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:url] asdk_image]; - if (result) { - [self _locked_setCurrentImageQuality:1.0]; - [self _locked__setImage:result]; - _imageLoaded = YES; - - // Call out to the delegate. - if (_delegateFlags.delegateDidLoadImageWithInfo) { - ASDN::MutexUnlocker l(__instanceLock__); - ASNetworkImageNodeDidLoadInfo info = {}; - info.imageSource = ASNetworkImageSourceSynchronousCache; - [_delegate imageNode:self didLoadImage:result info:info]; - } else if (_delegateFlags.delegateDidLoadImage) { - ASDN::MutexUnlocker l(__instanceLock__); - [_delegate imageNode:self didLoadImage:result]; - } - break; + if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { + UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; + if (result) { + [self _locked_setCurrentImageQuality:1.0]; + [self _locked__setImage:result]; + _imageLoaded = YES; + + // Call out to the delegate. + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker l(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceSynchronousCache; + [_delegate imageNode:self didLoadImage:result info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASDN::MutexUnlocker l(__instanceLock__); + [_delegate imageNode:self didLoadImage:result]; } } } @@ -538,11 +520,9 @@ _imageLoaded = NO; if (_cacheFlags.cacheSupportsClearing) { - if (_URLs.count != 0) { - as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URLs); - for (NSURL *url in _URLs) { - [_cache clearFetchedImageFromCacheWithURL:url]; - } + if (_URL != nil) { + as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URL); + [_cache clearFetchedImageFromCacheWithURL:_URL]; } } } @@ -576,7 +556,7 @@ - (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier))finished { ASPerformBlockOnBackgroundThread(^{ - NSArray *urls; + NSURL *url; id downloadIdentifier; BOOL cancelAndReattempt = NO; @@ -585,34 +565,23 @@ // it and try again. { ASDN::MutexLocker l(__instanceLock__); - urls = _URLs; + url = _URL; } - if (_downloaderFlags.downloaderImplementsDownloadURLs) { - downloadIdentifier = [_downloader downloadImageWithURLs:urls - callbackQueue:dispatch_get_main_queue() - downloadProgress:NULL - completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { - if (finished != NULL) { - finished(imageContainer, error, downloadIdentifier); - } - }]; - } else { - downloadIdentifier = [_downloader downloadImageWithURL:[urls lastObject] - callbackQueue:dispatch_get_main_queue() - downloadProgress:NULL - completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { - if (finished != NULL) { - finished(imageContainer, error, downloadIdentifier); - } - }]; - } - + + downloadIdentifier = [_downloader downloadImageWithURL:url + callbackQueue:dispatch_get_main_queue() + downloadProgress:NULL + completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { + if (finished != NULL) { + finished(imageContainer, error, downloadIdentifier); + } + }]; as_log_verbose(ASImageLoadingLog(), "Downloading image for %@ url: %@", self, url); { ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(_URLs, urls)) { + if (ASObjectIsEqual(_URL, url)) { // The download we kicked off is correct, no need to do any more work. _downloadIdentifier = downloadIdentifier; } else { @@ -641,36 +610,34 @@ __weak id delegate = _delegate; BOOL delegateDidStartFetchingData = _delegateFlags.delegateDidStartFetchingData; BOOL isImageLoaded = _imageLoaded; - NSArray *URLs = _URLs; + NSURL *URL = _URL; id currentDownloadIdentifier = _downloadIdentifier; __instanceLock__.unlock(); - if (!isImageLoaded && URLs.count > 0 && currentDownloadIdentifier == nil) { + if (!isImageLoaded && URL != nil && currentDownloadIdentifier == nil) { if (delegateDidStartFetchingData) { [delegate imageNodeDidStartFetchingData:self]; } - // We only support file URLs if there is one URL currently - if (URLs.count == 1 && [URLs lastObject].isFileURL) { + if (URL.isFileURL) { dispatch_async(dispatch_get_main_queue(), ^{ ASDN::MutexLocker l(__instanceLock__); // Bail out if not the same URL anymore - if (!ASObjectIsEqual(URLs, _URLs)) { + if (!ASObjectIsEqual(URL, _URL)) { return; } - NSURL *URL = [URLs lastObject]; if (_shouldCacheImage) { - [self _locked__setImage:[UIImage imageNamed:URL.path.lastPathComponent]]; + [self _locked__setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. - UIImage *nonAnimatedImage = [UIImage imageWithContentsOfFile:URL.path]; + UIImage *nonAnimatedImage = [UIImage imageWithContentsOfFile:_URL.path]; if (nonAnimatedImage == nil) { // If we couldn't find it, execute an -imageNamed:-like search so we can find resources even if the // extension is not provided in the path. This allows the same path to work regardless of shouldCacheImage. - NSString *filename = [[NSBundle mainBundle] pathForResource:URL.path.lastPathComponent ofType:nil]; + NSString *filename = [[NSBundle mainBundle] pathForResource:_URL.path.lastPathComponent ofType:nil]; if (filename != nil) { nonAnimatedImage = [UIImage imageWithContentsOfFile:filename]; } @@ -679,7 +646,7 @@ // If the file may be an animated gif and then created an animated image. id animatedImage = nil; if (_downloaderFlags.downloaderImplementsAnimatedImage) { - NSData *data = [NSData dataWithContentsOfURL:URL]; + NSData *data = [NSData dataWithContentsOfURL:_URL]; if (data != nil) { animatedImage = [_downloader animatedImageWithData:data]; @@ -719,7 +686,7 @@ return; } - as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); + as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); // Grab the lock for the rest of the block ASDN::MutexLocker l(strongSelf->__instanceLock__); @@ -784,7 +751,7 @@ _cacheUUID = cacheUUID; __instanceLock__.unlock(); - as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ urls: %@", self, URLs); + as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ url: %@", self, URL); ASImageCacherCompletion completion = ^(id imageContainer) { // If the cache UUID changed, that means this request was cancelled. @@ -801,20 +768,13 @@ finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); }]; } else { - as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); + as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache); } }; - - if (_cacheFlags.cacheSupportsCachedURLs) { - [_cache cachedImageWithURLs:URLs - callbackQueue:dispatch_get_main_queue() - completion:completion]; - } else { - [_cache cachedImageWithURL:[URLs lastObject] - callbackQueue:dispatch_get_main_queue() - completion:completion]; - } + [_cache cachedImageWithURL:URL + callbackQueue:dispatch_get_main_queue() + completion:completion]; } else { [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); diff --git a/Source/Details/ASImageProtocols.h b/Source/Details/ASImageProtocols.h index bc671866ac..3fdf321e45 100644 --- a/Source/Details/ASImageProtocols.h +++ b/Source/Details/ASImageProtocols.h @@ -37,7 +37,7 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i @param URL The URL of the image to retrieve from the cache. @param callbackQueue The queue to call `completion` on. @param completion The block to be called when the cache has either hit or missed. - @discussion If `URL` is nil, `completion` should be invoked immediately with a nil image. This method should not block + @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image. This method should not block the calling thread as it is likely to be called from the main thread. */ - (void)cachedImageWithURL:(NSURL *)URL @@ -66,19 +66,6 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i */ - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL; -/** - @abstract Attempts to fetch an image with the given URLs from the cache in reverse order. - @param URLs The URLs of the image to retrieve from the cache. - @param callbackQueue The queue to call `completion` on. - @param completion The block to be called when the cache has either hit or missed. - @discussion If `URLs` is nil or empty, `completion` should be invoked immediately with a nil image. This method should not block - the calling thread as it is likely to be called from the main thread. - @see downloadImageWithURLs:callbackQueue:downloadProgress:completion: - */ -- (void)cachedImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - completion:(ASImageCacherCompletion)completion; - @end /** @@ -167,21 +154,6 @@ typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { - (void)setPriority:(ASImageDownloaderPriority)priority withDownloadIdentifier:(id)downloadIdentifier; -/** - @abstract Downloads an image from a list of URLs depending on previously observed network speed conditions. - @param URLs An array of URLs ordered by the cost of downloading them, the URL at index 0 being the lowest cost. - @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. - @param downloadProgress The block to be invoked when the download of `URL` progresses. - @param completion The block to be invoked when the download has completed, or has failed. - @discussion This method is likely to be called on the main thread, so any custom implementations should make sure to background any expensive download operations. - @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must - retain the identifier if you wish to use it later. - */ -- (nullable id)downloadImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress - completion:(ASImageDownloaderCompletion)completion; - @end @protocol ASAnimatedImageProtocol diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 34cd1c7b77..3b46fc3142 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -209,23 +209,6 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; }]; } -- (void)cachedImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - completion:(ASImageCacherCompletion)completion -{ - [self cachedImageWithURL:[URLs lastObject] - callbackQueue:callbackQueue - completion:^(id _Nullable imageFromCache) { - if (imageFromCache.asdk_image == nil && URLs.count > 1) { - [self cachedImageWithURLs:[URLs subarrayWithRange:NSMakeRange(0, URLs.count - 1)] - callbackQueue:callbackQueue - completion:completion]; - } else { - completion(imageFromCache); - } - }]; -} - - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL { if ([self sharedImageManagerSupportsMemoryRemoval]) { @@ -239,18 +222,6 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; callbackQueue:(dispatch_queue_t)callbackQueue downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion; -{ - NSArray *URLs = nil; - if (URL) { - URLs = @[URL]; - } - return [self downloadImageWithURLs:URLs callbackQueue:callbackQueue downloadProgress:downloadProgress completion:completion]; -} - -- (nullable id)downloadImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress - completion:(ASImageDownloaderCompletion)completion { PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { if (downloadProgress == nil) { return; } @@ -279,11 +250,11 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; // extra downloads isn't worth the effort of rechecking caches every single time. In order to provide // feedback to the consumer about whether images are cached, we can't simply make the cache a no-op and // check the cache as part of this download. - return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs - options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache - progressImage:nil - progressDownload:progressDownload - completion:imageCompletion]; + return [[self sharedPINRemoteImageManager] downloadImageWithURL:URL + options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache + progressImage:nil + progressDownload:progressDownload + completion:imageCompletion]; } - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier From 5a4d569c56f8bf871ccb1e60a1976df4e9bae945 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 8 Dec 2017 18:30:26 +0000 Subject: [PATCH 78/86] Ensure an ASM enabled node applies its pending layout when enters preload state (#706) This makes sure subnodes are inserted and start preloading right away, instead of waiting until the next layout pass of the supernode. Fixes #693. --- CHANGELOG.md | 1 + Source/ASDisplayNode.mm | 14 +++++++ Tests/ASDisplayNodeImplicitHierarchyTests.m | 44 ++++++++++++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab351bffeb..40c83558fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - [ASScrollNode] Ensure the node respects the given size range while calculating its layout. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) - [ASScrollNode] Invalidate the node's calculated layout if its scrollable directions changed. Also add unit tests for the class. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) - Add new unit testing to the layout engine. [Adlai Holler](https://github.com/Adlai-Holler) [#424](https://github.com/TextureGroup/Texture/pull/424) +- [Automatic Subnode Management] Nodes with ASM enabled now insert/delete their subnodes as soon as they enter preload state, so the subnodes can preload too. [Huy Nguyen](https://github.com/nguyenhuy) [#706](https://github.com/TextureGroup/Texture/pull/706) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 95e9276679..3a537ca4bb 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -3079,6 +3079,20 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); + + if (self.automaticallyManagesSubnodes) { + // Tell the node to apply its applicable pending layout, if any, so that its subnodes are inserted/deleted + // and start preloading right away. + // + // If this node has an up-to-date layout (and subnodes), calling layoutIfNeeded will be fast. + // + // If this node doesn't have a calculated or pending layout that fits its current bounds, a measurement pass will occur + // (see __layout and _u_measureNodeWithBoundsIfNecessary:). + // This scenario should be uncommon, and running a measurement pass here is a fine trade-off because preloading + // any time after this point would be late. + [self layoutIfNeeded]; + } + [_interfaceStateDelegate didEnterPreloadState]; } diff --git a/Tests/ASDisplayNodeImplicitHierarchyTests.m b/Tests/ASDisplayNodeImplicitHierarchyTests.m index a3a7e160f3..11e2ce4afa 100644 --- a/Tests/ASDisplayNodeImplicitHierarchyTests.m +++ b/Tests/ASDisplayNodeImplicitHierarchyTests.m @@ -2,8 +2,6 @@ // ASDisplayNodeImplicitHierarchyTests.m // Texture // -// Created by Levi McCallum on 2/1/16. -// // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // This source code is licensed under the BSD-style license found in the // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional @@ -20,6 +18,7 @@ #import #import +#import #import "ASDisplayNodeTestsHelper.h" @interface ASSpecTestDisplayNode : ASDisplayNode @@ -101,6 +100,47 @@ XCTAssertEqual(node.subnodes[4], node5); } +- (void)testInitialNodeInsertionWhenEnterPreloadState +{ + static CGSize kSize = {100, 100}; + + static NSInteger subnodeCount = 5; + NSMutableArray *subnodes = [NSMutableArray arrayWithCapacity:subnodeCount]; + for (NSInteger i = 0; i < subnodeCount; i++) { + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + // As we will involve a stack spec we have to give the nodes an intrinsic content size + subnode.style.preferredSize = kSize; + [subnodes addObject:subnode]; + } + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { + ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[subnodes[3]]]; + + ASStackLayoutSpec *stack1 = [[ASStackLayoutSpec alloc] init]; + [stack1 setChildren:@[subnodes[0], subnodes[1]]]; + + ASStackLayoutSpec *stack2 = [[ASStackLayoutSpec alloc] init]; + [stack2 setChildren:@[subnodes[2], absoluteLayout]]; + + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[stack1, stack2, subnodes[4]]]; + }; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node recursivelySetInterfaceState:ASInterfaceStatePreload]; + + // No premature view allocation + XCTAssertFalse(node.isNodeLoaded); + // Subnodes should be inserted, laid out and entered preload state + XCTAssertTrue([subnodes isEqualToArray:node.subnodes]); + for (NSInteger i = 0; i < subnodeCount; i++) { + ASDisplayNode *subnode = subnodes[i]; + XCTAssertTrue(CGSizeEqualToSize(kSize, subnode.bounds.size)); + XCTAssertTrue(ASInterfaceStateIncludesPreload(subnode.interfaceState)); + } +} + - (void)testCalculatedLayoutHierarchyTransitions { static CGSize kSize = {100, 100}; From 8300d8eb61e6794d38bb8d202c9ff59cbd89d40f Mon Sep 17 00:00:00 2001 From: Ha Hyun soo Date: Tue, 12 Dec 2017 12:47:41 +0900 Subject: [PATCH 79/86] Vingle very community - Update showcase (#711) --- docs/showcase.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/showcase.md b/docs/showcase.md index 3a55315099..64c8871a1c 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -210,7 +210,13 @@ permalink: /showcase.html
Sorted: Master Your Day - + + + +
+ Vingle + + From 46d46fdf126a9b7ee14100701687ec41956bba1d Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Mon, 18 Dec 2017 15:15:29 -0800 Subject: [PATCH 80/86] #trivial Fixes image nodes being stuck not being able to download image (#720) * #trivial Fixes image nodes being stuck not being able to download image * Clear out the _cacheUUID too even though this is not strictly necessary. --- Source/ASNetworkImageNode.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index d844b66466..2240e3ae9e 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -698,6 +698,8 @@ //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) if (ASInterfaceStateIncludesPreload(self->_interfaceState) == NO) { + self->_downloadIdentifier = nil; + self->_cacheUUID = nil; return; } From 8f194347887d01ff17d189ea60e8cb5c2a712fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Est=C3=A9banez=20Tasc=C3=B3n?= Date: Tue, 19 Dec 2017 17:56:35 +0100 Subject: [PATCH 81/86] Check for nil elements on ASTableView as well #trivial (#710) --- Source/ASCollectionView.mm | 2 +- Source/ASTableView.mm | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 2811f62715..1c884c304b 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1174,7 +1174,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; ASCellNode *cellNode = element.node; cellNode.scrollView = collectionView; - // Update the selected background view in collectionView:willDisplayCell:forItemAtIndexPath: otherwise it could be to + // Update the selected background view in collectionView:willDisplayCell:forItemAtIndexPath: otherwise it could be too // early e.g. if the selectedBackgroundView was set in didLoad() cell.selectedBackgroundView = cellNode.selectedBackgroundView; diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 4d9d01f81b..1cc369b072 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -130,6 +130,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [node __setHighlightedFromUIKit:self.highlighted]; } +- (BOOL)consumesCellNodeVisibilityEvents +{ + ASCellNode *node = self.node; + if (node == nil) { + return NO; + } + return ASSubclassOverridesSelector([ASCellNode class], [node class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:)); +} + - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; @@ -971,7 +980,14 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { ASCollectionElement *element = cell.element; - [_visibleElements addObject:element]; + if (element) { + ASDisplayNodeAssertTrue([_dataController.visibleMap elementForItemAtIndexPath:indexPath] == element); + [_visibleElements addObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplayCell: %@, %@, %@", cell, self, indexPath); + return; + } + ASCellNode *cellNode = element.node; cellNode.scrollView = tableView; @@ -991,15 +1007,22 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_rangeController setNeedsUpdate]; - if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { + if ([cell consumesCellNodeVisibilityEvents]) { [_cellsForVisibilityUpdates addObject:cell]; } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { + // Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. ASCollectionElement *element = cell.element; - [_visibleElements removeObject:element]; + if (element) { + [_visibleElements removeObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingCell: %@, %@, %@", cell, self, indexPath); + return; + } + ASCellNode *cellNode = element.node; [_rangeController setNeedsUpdate]; From eab7bea48de9ceb8b57ff96f6ec5f9f827800384 Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 19 Dec 2017 20:13:45 +0300 Subject: [PATCH 82/86] Add missing flags for ASCollectionDelegate (#718) * Add missing flags for ASCollectionDelegate * Update CHANGELOG.md --- CHANGELOG.md | 1 + Source/ASCollectionView.mm | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c83558fc..cf53b2a884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASCollectionView] Add missing flags for ASCollectionDelegate [Ilya Zheleznikov](https://github.com/ilyailya) [#718](https://github.com/TextureGroup/Texture/pull/718) - [ASNetworkImageNode] Deprecates .URLs in favor of .URL [Garrett Moon](https://github.com/garrettmoon) [#699](https://github.com/TextureGroup/Texture/pull/699) - [iOS11] Update project settings and fix errors [Eke](https://github.com/Eke) [#676](https://github.com/TextureGroup/Texture/pull/676) - [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 1c884c304b..4efe9a75d6 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -539,6 +539,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; _asyncDelegateFlags.collectionNodeShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldShowMenuForItemAtIndexPath:)]; _asyncDelegateFlags.collectionNodeCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:canPerformAction:forItemAtIndexPath:sender:)]; _asyncDelegateFlags.collectionNodePerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:performAction:forItemAtIndexPath:sender:)]; + _asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplaySupplementaryElementWithNode:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingSupplementaryElementWithNode:)]; _asyncDelegateFlags.interop = [_asyncDelegate conformsToProtocol:@protocol(ASCollectionDelegateInterop)]; if (_asyncDelegateFlags.interop) { id interopDelegate = (id)_asyncDelegate; From 7416d6a88a7319d9ee5770f9b7eeb2a2ac179f2a Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 19 Dec 2017 18:53:13 +0100 Subject: [PATCH 83/86] Add Blendle to our showcase page (#721) --- docs/showcase.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/showcase.md b/docs/showcase.md index 64c8871a1c..4b3e68233e 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -219,6 +219,16 @@ permalink: /showcase.html + + + + +
+ Blendle + + + +
From fff5aae0a5168dae66ac80e01d87cb17ad4020f6 Mon Sep 17 00:00:00 2001 From: John T McIntosh Date: Wed, 20 Dec 2017 06:49:09 -0600 Subject: [PATCH 84/86] Add support for toggling logs off and back on at runtime #trivial (#714) --- Source/Base/ASLog.h | 10 ++++++++++ Source/Base/ASLog.m | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index be4bff3685..c6aba76bac 100644 --- a/Source/Base/ASLog.h +++ b/Source/Base/ASLog.h @@ -42,6 +42,16 @@ ASDISPLAYNODE_EXTERN_C_BEGIN */ void ASDisableLogging(void); +/** + * Restore logging that has been runtime-disabled via ASDisableLogging(). + * + * Logging can be disabled at runtime using the ASDisableLogging() function. + * This command restores logging to the level provided in the build + * configuration. This can be used in conjunction with ASDisableLogging() + * to allow logging to be toggled off and back on at runtime. + */ +void ASEnableLogging(void); + /// Log for general node events e.g. interfaceState, didLoad. #define ASNodeLogEnabled 1 os_log_t ASNodeLog(void); diff --git a/Source/Base/ASLog.m b/Source/Base/ASLog.m index 8e65c4b55b..e1c42ea79d 100644 --- a/Source/Base/ASLog.m +++ b/Source/Base/ASLog.m @@ -16,10 +16,11 @@ static atomic_bool __ASLogEnabled = ATOMIC_VAR_INIT(YES); void ASDisableLogging() { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - atomic_store(&__ASLogEnabled, NO); - }); + atomic_store(&__ASLogEnabled, NO); +} + +void ASEnableLogging() { + atomic_store(&__ASLogEnabled, YES); } ASDISPLAYNODE_INLINE BOOL ASLoggingIsEnabled() { From 4dec51ca37842be31c8e8dd97abbc206ef1fb0c2 Mon Sep 17 00:00:00 2001 From: Stephen Williams Date: Thu, 21 Dec 2017 01:53:13 +1300 Subject: [PATCH 85/86] Fix ASDKgram example #trivial (#700) - Fix an insta-crash that's caused by Webservice.load method to call its completion block off the main thread. - Fix incorrect http status code check. - Bump the deployment target to get the project compiling. --- .../ASDKgram-Swift.xcodeproj/project.pbxproj | 4 ++-- .../ASDKgram-Swift/Webservice.swift | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj index 8753319557..23ef18b40e 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj @@ -428,7 +428,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -478,7 +478,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift index 4021e53c59..830d78eb0e 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift @@ -25,12 +25,13 @@ final class WebService { URLSession.shared.dataTask(with: resource.url) { data, response, error in // Check for errors in responses. let result = self.checkForNetworkErrors(data, response, error) - - switch result { - case .success(let data): - completion(resource.parse(data)) - case .failure(let error): - completion(.failure(error)) + DispatchQueue.main.async { + switch result { + case .success(let data): + completion(resource.parse(data)) + case .failure(let error): + completion(.failure(error)) + } } }.resume() } @@ -49,7 +50,7 @@ extension WebService { } } - if let response = response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode <= 299 { + if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 { return .failure((.invalidStatusCode("Request returned status code other than 2xx \(response)"))) } From 131619de96190415aec89987d4493220c6d0305d Mon Sep 17 00:00:00 2001 From: appleguy Date: Thu, 21 Dec 2017 16:17:25 -0800 Subject: [PATCH 86/86] Reimplement ASRectTable using unordered_map to avoid obscure NSMapTable exception. (#719) * Reimplement ASRectTable using unordered_map to avoid obscure NSMapTable exception. The new class is called ASRectMap, which patterns alongside ASIntegerMap in both name and implementation. After some pretty detailed investigation, including study of open-source reimplementations of Foundation, the best lead I've found on the NSMapTable exception is that some NSPointerFunction types are not fully supported. Strangely, the ones being used do seem to work fine almost all of the time. The main concern is the Struct memory type, which is not officially re-declared in NSMapTable, and as such the documentation claims that there may exist some combinations of NSPointerFunction that are not supported. Because the exception is occurring frequently enough to be a concern (in the hundreds to low thousands, though only 50 a day) - I decided to replace NSMapTable entirely in order to ensure full correctness. "*** -[NSMapTable initWithKeyPointerFunctions:valuePointerFunctions:capacity:] Requested configuration not supported." * Fix Xcode project --- AsyncDisplayKit.xcodeproj/project.pbxproj | 24 +++--- CHANGELOG.md | 1 + Source/Layout/ASLayout.mm | 10 +-- Source/Private/ASRectMap.h | 52 ++++++++++++ Source/Private/ASRectMap.mm | 78 ++++++++++++++++++ Source/Private/ASRectTable.h | 73 ----------------- Source/Private/ASRectTable.m | 80 ------------------- .../{ASRectTableTests.m => ASRectMapTests.m} | 14 ++-- 8 files changed, 155 insertions(+), 177 deletions(-) create mode 100644 Source/Private/ASRectMap.h create mode 100644 Source/Private/ASRectMap.mm delete mode 100644 Source/Private/ASRectTable.h delete mode 100644 Source/Private/ASRectTable.m rename Tests/{ASRectTableTests.m => ASRectMapTests.m} (78%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index f8d60fb322..3a4b1c5342 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -315,7 +315,6 @@ C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A0A1E60BEB400626263 /* ASDisplayNode+Convenience.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */; }; - CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */; }; CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A141E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m */; }; CC051F1F1D7A286A006434CB /* ASCALayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC051F1E1D7A286A006434CB /* ASCALayerTests.m */; }; @@ -431,6 +430,9 @@ DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; }; + E52AC9BA1FEA90EB00AA4040 /* ASRectMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */; }; + E52AC9BB1FEA90EB00AA4040 /* ASRectMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E52AC9C01FEA916C00AA4040 /* ASRectMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */; }; E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; @@ -454,8 +456,6 @@ E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */; }; - E5ABAC7B1E8564EE007AC15C /* ASRectTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E5ABAC791E8564EE007AC15C /* ASRectTable.h */; }; - E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */; }; E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */; }; E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -800,7 +800,6 @@ BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Convenience.h"; sourceTree = ""; }; CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASDisplayNode+Convenience.m"; sourceTree = ""; }; - CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTableTests.m; sourceTree = ""; }; CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit+IGListKitMethods.h"; sourceTree = ""; }; CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "AsyncDisplayKit+IGListKitMethods.m"; sourceTree = ""; }; CC051F1E1D7A286A006434CB /* ASCALayerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCALayerTests.m; sourceTree = ""; }; @@ -931,6 +930,9 @@ E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutFlatteningTests.m; sourceTree = ""; }; E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; + E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRectMap.mm; sourceTree = ""; }; + E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRectMap.h; sourceTree = ""; }; + E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectMapTests.m; sourceTree = ""; }; E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASPagerNode+Beta.h"; sourceTree = ""; }; E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = ""; }; @@ -954,8 +956,6 @@ E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDelegate.h; sourceTree = ""; }; E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayout.h; sourceTree = ""; }; E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayout.mm; sourceTree = ""; }; - E5ABAC791E8564EE007AC15C /* ASRectTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRectTable.h; sourceTree = ""; }; - E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTable.m; sourceTree = ""; }; E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASElementMap.h; sourceTree = ""; }; E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASElementMap.m; sourceTree = ""; }; E5B225261F1790B5001E1431 /* ASHashing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; @@ -1179,7 +1179,6 @@ CC583ABF1EF9BAB400134156 /* Common */, CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */, - CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, @@ -1196,6 +1195,7 @@ CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */, CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */, CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */, + E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */, 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.m */, DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */, DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, @@ -1383,8 +1383,8 @@ CCA282B31E9EA7310037E8B7 /* ASTipsController.m */, CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */, CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */, - E5ABAC791E8564EE007AC15C /* ASRectTable.h */, - E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */, + E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */, + E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */, CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */, CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, 6947B0BB1E36B4E30007C478 /* Layout */, @@ -1857,6 +1857,7 @@ E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */, + E52AC9BB1FEA90EB00AA4040 /* ASRectMap.h in Headers */, E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, @@ -1918,7 +1919,6 @@ B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */, CCA282C81E9EB64B0037E8B7 /* ASDisplayNodeTipState.h in Headers */, 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */, - E5ABAC7B1E8564EE007AC15C /* ASRectTable.h in Headers */, B35062261B010EFD0018CF92 /* ASRangeController.h in Headers */, 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */, DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */, @@ -2165,7 +2165,6 @@ 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */, 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */, - CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, @@ -2211,6 +2210,7 @@ 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */, 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */, ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, + E52AC9C01FEA916C00AA4040 /* ASRectMapTests.m in Sources */, CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */, 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.m in Sources */, 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */, @@ -2239,6 +2239,7 @@ 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.m in Sources */, CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */, CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.m in Sources */, + E52AC9BA1FEA90EB00AA4040 /* ASRectMap.mm in Sources */, 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.m in Sources */, B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, @@ -2351,7 +2352,6 @@ 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */, 254C6B851BF94F8A003EC431 /* ASTextKitAttributes.mm in Sources */, 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */, - E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */, CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.m in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index cf53b2a884..35e4f360bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASRectMap] Replace implementation of ASRectTable with a simpler one based on unordered_map.[Scott Goodson](https://github.com/appleguy) [#719](https://github.com/TextureGroup/Texture/pull/719) - [ASCollectionView] Add missing flags for ASCollectionDelegate [Ilya Zheleznikov](https://github.com/ilyailya) [#718](https://github.com/TextureGroup/Texture/pull/718) - [ASNetworkImageNode] Deprecates .URLs in favor of .URL [Garrett Moon](https://github.com/garrettmoon) [#699](https://github.com/TextureGroup/Texture/pull/699) - [iOS11] Update project settings and fix errors [Eke](https://github.com/Eke) [#676](https://github.com/TextureGroup/Texture/pull/676) diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 919a520c95..f6fbe91279 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -26,7 +26,7 @@ #import #import #import -#import +#import CGPoint const ASPointNull = {NAN, NAN}; @@ -86,7 +86,7 @@ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsFlattened(ASLayout *la */ @property (nonatomic, strong) NSMutableArray> *sublayoutLayoutElements; -@property (nonatomic, strong, readonly) ASRectTable, id> *elementToRectTable; +@property (nonatomic, strong, readonly) ASRectMap *elementToRectMap; @end @@ -143,9 +143,9 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( _sublayouts = sublayouts != nil ? [sublayouts copy] : @[]; if (_sublayouts.count > 0) { - _elementToRectTable = [ASRectTable rectTableForWeakObjectPointers]; + _elementToRectMap = [ASRectMap rectMapForWeakObjectPointers]; for (ASLayout *layout in sublayouts) { - [_elementToRectTable setRect:layout.frame forKey:layout.layoutElement]; + [_elementToRectMap setRect:layout.frame forKey:layout.layoutElement]; } } @@ -303,7 +303,7 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( - (CGRect)frameForElement:(id)layoutElement { - return _elementToRectTable ? [_elementToRectTable rectForKey:layoutElement] : CGRectNull; + return _elementToRectMap ? [_elementToRectMap rectForKey:layoutElement] : CGRectNull; } - (CGRect)frame diff --git a/Source/Private/ASRectMap.h b/Source/Private/ASRectMap.h new file mode 100644 index 0000000000..fd756b3331 --- /dev/null +++ b/Source/Private/ASRectMap.h @@ -0,0 +1,52 @@ +// +// ASRectMap.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 +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A category for indexing weak pointers to CGRects. Similar to ASIntegerMap. + */ +@interface ASRectMap : NSObject + +/** + * Creates a new rect map. The keys are never retained. + */ ++ (ASRectMap *)rectMapForWeakObjectPointers; + +/** + * Retrieves the rect for a given key, or CGRectNull if the key is not found. + * + * @param key An object to lookup the rect for. + */ +- (CGRect)rectForKey:(id)key; + +/** + * Sets the given rect for the associated key. Key *will not be retained!* + * + * @param rect The rect to store as value. + * @param key The key to use for the rect. + */ +- (void)setRect:(CGRect)rect forKey:(id)key; + +/** + * Removes the rect for the given key, if one exists. + * + * @param key The key to remove. + */ +- (void)removeRectForKey:(id)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASRectMap.mm b/Source/Private/ASRectMap.mm new file mode 100644 index 0000000000..cb76810806 --- /dev/null +++ b/Source/Private/ASRectMap.mm @@ -0,0 +1,78 @@ +// +// ASRectMap.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASRectMap.h" +#import "ASObjectDescriptionHelpers.h" +#import +#import + +@implementation ASRectMap { + std::unordered_map _map; +} + ++ (ASRectMap *)rectMapForWeakObjectPointers +{ + return [[self alloc] init]; +} + +- (CGRect)rectForKey:(id)key +{ + auto result = _map.find((__bridge void *)key); + if (result != _map.end()) { + // result->first is the key; result->second is the value, a CGRect. + return result->second; + } else { + return CGRectNull; + } +} + +- (void)setRect:(CGRect)rect forKey:(id)key +{ + if (key) { + _map[(__bridge void *)key] = rect; + } +} + +- (void)removeRectForKey:(id)key +{ + if (key) { + _map.erase((__bridge void *)key); + } +} + +- (id)copyWithZone:(NSZone *)zone +{ + ASRectMap *copy = [ASRectMap rectMapForWeakObjectPointers]; + copy->_map = _map; + return copy; +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + // { ptr1->rect1 ptr2->rect2 ptr3->rect3 } + NSMutableString *str = [NSMutableString string]; + for (auto it = _map.begin(); it != _map.end(); it++) { + [str appendFormat:@" %@->%@", it->first, NSStringFromCGRect(it->second)]; + } + [result addObject:@{ @"ASRectMap": str }]; + + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMakeWithoutObject([self propertiesForDescription]); +} + +@end diff --git a/Source/Private/ASRectTable.h b/Source/Private/ASRectTable.h deleted file mode 100644 index af47f59b6a..0000000000 --- a/Source/Private/ASRectTable.h +++ /dev/null @@ -1,73 +0,0 @@ -// -// ASRectTable.h -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * An alias for an NSMapTable created to store rects. - * - * You should not call -objectForKey:, -setObject:forKey:, or -allObjects - * on these objects. - */ -typedef NSMapTable ASRectTable; - -/** - * A category for creating & using map tables meant for storing CGRects. - * - * This category is private, so name collisions are not worth worrying about. - */ -@interface NSMapTable (ASRectTableMethods) - -/** - * Creates a new rect table with (NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) for keys. - */ -+ (ASRectTable *)rectTableForStrongObjectPointers; - -/** - * Creates a new rect table with (NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) for keys. - */ -+ (ASRectTable *)rectTableForWeakObjectPointers; - -/** - * Retrieves the rect for a given key, or CGRectNull if the key is not found. - * - * @param key An object to lookup the rect for. - */ -- (CGRect)rectForKey:(KeyType)key; - -/** - * Sets the given rect for the associated key. - * - * @param rect The rect to store as value. - * @param key The key to use for the rect. - */ -- (void)setRect:(CGRect)rect forKey:(KeyType)key; - -/** - * Removes the rect for the given key, if one exists. - * - * @param key The key to remove. - */ -- (void)removeRectForKey:(KeyType)key; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASRectTable.m b/Source/Private/ASRectTable.m deleted file mode 100644 index 5a98a3f702..0000000000 --- a/Source/Private/ASRectTable.m +++ /dev/null @@ -1,80 +0,0 @@ -// -// ASRectTable.m -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import "ASRectTable.h" - -__attribute__((const)) -static NSUInteger ASRectSize(const void *ptr) -{ - return sizeof(CGRect); -} - -@implementation NSMapTable (ASRectTableMethods) - -+ (NSMapTable *)rectTableWithKeyPointerFunctions:(NSPointerFunctions *)keyFuncs -{ - static NSPointerFunctions *cgRectFuncs; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - cgRectFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStructPersonality | NSPointerFunctionsCopyIn | NSPointerFunctionsMallocMemory]; - cgRectFuncs.sizeFunction = &ASRectSize; - }); - - return [[NSMapTable alloc] initWithKeyPointerFunctions:keyFuncs valuePointerFunctions:cgRectFuncs capacity:0]; -} - -+ (NSMapTable *)rectTableForStrongObjectPointers -{ - static NSPointerFunctions *strongObjectPointerFuncs; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - strongObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; - }); - return [self rectTableWithKeyPointerFunctions:strongObjectPointerFuncs]; -} - -+ (NSMapTable *)rectTableForWeakObjectPointers -{ - static NSPointerFunctions *weakObjectPointerFuncs; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - weakObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality]; - }); - return [self rectTableWithKeyPointerFunctions:weakObjectPointerFuncs]; -} - -- (CGRect)rectForKey:(id)key -{ - CGRect *ptr = (__bridge CGRect *)[self objectForKey:key]; - if (ptr == NULL) { - return CGRectNull; - } - return *ptr; -} - -- (void)setRect:(CGRect)rect forKey:(id)key -{ - __unsafe_unretained id obj = (__bridge id)▭ - [self setObject:obj forKey:key]; -} - -- (void)removeRectForKey:(id)key -{ - [self removeObjectForKey:key]; -} - -@end diff --git a/Tests/ASRectTableTests.m b/Tests/ASRectMapTests.m similarity index 78% rename from Tests/ASRectTableTests.m rename to Tests/ASRectMapTests.m index ca897a3571..357f3879a6 100644 --- a/Tests/ASRectTableTests.m +++ b/Tests/ASRectMapTests.m @@ -1,5 +1,5 @@ // -// ASRectTableTests.m +// ASRectMapTests.m // Texture // // Created by Adlai Holler on 2/24/17. @@ -8,17 +8,17 @@ #import -#import "ASRectTable.h" +#import "ASRectMap.h" #import "ASXCTExtensions.h" -@interface ASRectTableTests : XCTestCase +@interface ASRectMapTests : XCTestCase @end -@implementation ASRectTableTests +@implementation ASRectMapTests - (void)testThatItStoresRects { - ASRectTable *table = [ASRectTable rectTableForWeakObjectPointers]; + ASRectMap *table = [ASRectMap rectMapForWeakObjectPointers]; NSObject *key0 = [[NSObject alloc] init]; NSObject *key1 = [[NSObject alloc] init]; ASXCTAssertEqualRects([table rectForKey:key0], CGRectNull); @@ -35,13 +35,13 @@ - (void)testCopying { - ASRectTable *table = [ASRectTable rectTableForWeakObjectPointers]; + ASRectMap *table = [ASRectMap rectMapForWeakObjectPointers]; NSObject *key = [[NSObject alloc] init]; ASXCTAssertEqualRects([table rectForKey:key], CGRectNull); CGRect rect0 = CGRectMake(0, 0, 100, 100); CGRect rect1 = CGRectMake(0, 0, 50, 50); [table setRect:rect0 forKey:key]; - ASRectTable *copy = [table copy]; + ASRectMap *copy = [table copy]; [copy setRect:rect1 forKey:key]; ASXCTAssertEqualRects([table rectForKey:key], rect0);